Няма описание

Database.php 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. <?php
  2. /*******************************************************************************
  3. * Copyright (c) 2019, Code Atlantic LLC
  4. ******************************************************************************/
  5. if ( ! defined( 'ABSPATH' ) ) {
  6. exit;
  7. }
  8. abstract class PUM_Abstract_Database {
  9. /**
  10. * @var static
  11. */
  12. public static $instance;
  13. /**
  14. * The name of our database table
  15. */
  16. public $table_name = '';
  17. /**
  18. * The version of our database table
  19. */
  20. public $version = 1;
  21. /**
  22. * The name of the primary column
  23. */
  24. public $primary_key = 'ID';
  25. /**
  26. * Get things started
  27. */
  28. public function __construct() {
  29. global $wpdb;
  30. $current_db_version = $this->get_installed_version();
  31. if ( ! $current_db_version || $current_db_version < $this->version ) {
  32. // Install the table.
  33. @$this->create_table();
  34. if ( $wpdb->get_var( "SHOW TABLES LIKE '{$this->table_name()}'" ) == $this->table_name() ) {
  35. $this->update_db_version();
  36. }
  37. }
  38. $wpdb->{$this->table_name} = $this->table_name();
  39. }
  40. /**
  41. * Gets db version from new or old source.
  42. *
  43. * @return float
  44. */
  45. public function get_installed_version() {
  46. // Get list of all current db table versions.
  47. $db_versions = get_option( 'pum_db_versions', [] );
  48. // #1 If it exists in new pum_db_vers[] option, move on.
  49. if ( isset( $db_versions[ $this->table_name ] ) ) {
  50. return (float) $db_versions[ $this->table_name ];
  51. }
  52. // #2 Else look for old key, if exists, migrate and delete.
  53. $db_version_old_key = get_option( $this->table_name . '_db_version' );
  54. if ( $db_version_old_key ) {
  55. if ( $db_version_old_key > 0 ) {
  56. $db_versions[ $this->table_name ] = (float) $db_version_old_key;
  57. update_option( 'pum_db_versions', $db_versions );
  58. }
  59. delete_option( $this->table_name . '_db_version' );
  60. }
  61. return (float) $db_version_old_key;
  62. }
  63. public function update_db_version() {
  64. // Get list of all current db table versions.
  65. $db_versions = get_option( 'pum_db_versions', [] );
  66. $db_versions[ $this->table_name ] = (float) $this->version;
  67. update_option( 'pum_db_versions', $db_versions );
  68. }
  69. /**
  70. * Create the table
  71. */
  72. abstract public function create_table();
  73. /**
  74. * @return static
  75. * @throws \Exception
  76. */
  77. public static function instance() {
  78. $class = get_called_class();
  79. if ( ! isset( self::$instance[ $class ] ) ) {
  80. self::$instance[ $class ] = new $class;
  81. }
  82. return self::$instance[ $class ];
  83. }
  84. /**
  85. * Retrieve a row by the primary key
  86. *
  87. * @param $row_id
  88. *
  89. * @return object
  90. */
  91. public function get( $row_id ) {
  92. global $wpdb;
  93. return $this->prepare_result( $wpdb->get_row( "SELECT * FROM {$this->table_name()} WHERE $this->primary_key = $row_id LIMIT 1;" ) );
  94. }
  95. /**
  96. * @param object|array $result
  97. *
  98. * @return object|array
  99. */
  100. public function prepare_result( $result ) {
  101. if ( ! $result || ( ! is_array( $result ) && ! is_object( $result ) ) ) {
  102. return $result;
  103. }
  104. if ( is_object( $result ) ) {
  105. $vars = get_object_vars( $result );
  106. foreach ( $vars as $key => $value ) {
  107. if ( is_string( $value ) ) {
  108. $result->$key = maybe_unserialize( $value );
  109. }
  110. }
  111. } elseif ( is_array( $result ) ) {
  112. foreach ( $result as $key => $value ) {
  113. if ( is_string( $value ) ) {
  114. $result[ $key ] = maybe_unserialize( $value );
  115. }
  116. }
  117. }
  118. return $result;
  119. }
  120. /**
  121. * @param array|object[] $results
  122. *
  123. * @return mixed
  124. */
  125. public function prepare_results( $results ) {
  126. foreach ( $results as $key => $result ) {
  127. $results[ $key ] = $this->prepare_result( $result );
  128. }
  129. return $results;
  130. }
  131. /**
  132. * @return string
  133. */
  134. public function table_name() {
  135. global $wpdb;
  136. return $wpdb->prefix . $this->table_name;
  137. }
  138. /**
  139. * Retrieve a row by a specific column / value
  140. *
  141. * @param $column
  142. * @param $row_id
  143. *
  144. * @return object
  145. */
  146. public function get_by( $column, $row_id ) {
  147. global $wpdb;
  148. return $this->prepare_result( $wpdb->get_row( "SELECT * FROM {$this->table_name()} WHERE $column = '$row_id' LIMIT 1;" ) );
  149. }
  150. /**
  151. * Retrieve a specific column's value by the primary key
  152. *
  153. * @param $column
  154. * @param $row_id
  155. *
  156. * @return string
  157. */
  158. public function get_column( $column, $row_id ) {
  159. global $wpdb;
  160. return $wpdb->get_var( "SELECT $column FROM {$this->table_name()} WHERE $this->primary_key = $row_id LIMIT 1;" );
  161. }
  162. /**
  163. * Retrieve a specific column's value by the the specified column / value
  164. *
  165. * @param $column
  166. * @param $column_where
  167. * @param $column_value
  168. *
  169. * @return string
  170. */
  171. public function get_column_by( $column, $column_where, $column_value ) {
  172. global $wpdb;
  173. return $wpdb->get_var( "SELECT $column FROM {$this->table_name()} WHERE $column_where = '$column_value' LIMIT 1;" );
  174. }
  175. /**
  176. * Insert a new row
  177. *
  178. * @param $data
  179. * @param string $type
  180. *
  181. * @return int
  182. */
  183. public function insert( $data ) {
  184. global $wpdb;
  185. // Set default values
  186. $data = wp_parse_args( $data, $this->get_column_defaults() );
  187. do_action( 'pum_pre_insert_' . $this->table_name, $data );
  188. // Initialise column format array
  189. $column_formats = $this->get_columns();
  190. // Force fields to lower case
  191. $data = array_change_key_case( $data );
  192. // White list columns
  193. $data = array_intersect_key( $data, $column_formats );
  194. // Reorder $column_formats to match the order of columns given in $data
  195. $data_keys = array_keys( $data );
  196. $column_formats = array_merge( array_flip( $data_keys ), $column_formats );
  197. foreach ( $data as $key => $value ) {
  198. if ( is_array( $value ) ) {
  199. $data[ $key ] = maybe_serialize( $value );
  200. }
  201. }
  202. $wpdb->insert( $this->table_name(), $data, $column_formats );
  203. do_action( 'pum_post_insert_' . $this->table_name, $wpdb->insert_id, $data );
  204. return $wpdb->insert_id;
  205. }
  206. /**
  207. * Default column values
  208. *
  209. * @return array
  210. */
  211. public function get_column_defaults() {
  212. return array();
  213. }
  214. /**
  215. * Whitelist of columns
  216. *
  217. * @return array
  218. */
  219. public function get_columns() {
  220. return array();
  221. }
  222. /**
  223. * Update a row
  224. *
  225. * @param $row_id
  226. * @param array $data
  227. * @param string $where
  228. *
  229. * @return bool
  230. */
  231. public function update( $row_id, $data = array(), $where = '' ) {
  232. global $wpdb;
  233. // Row ID must be positive integer
  234. $row_id = absint( $row_id );
  235. if ( empty( $row_id ) ) {
  236. return false;
  237. }
  238. if ( empty( $where ) ) {
  239. $where = $this->primary_key;
  240. }
  241. // Initialise column format array
  242. $column_formats = $this->get_columns();
  243. // Force fields to lower case
  244. $data = array_change_key_case( $data );
  245. // White list columns
  246. $data = array_intersect_key( $data, $column_formats );
  247. foreach ( $data as $key => $value ) {
  248. if ( is_array( $value ) ) {
  249. $data[ $key ] = maybe_serialize( $value );
  250. }
  251. }
  252. // Reorder $column_formats to match the order of columns given in $data
  253. $data_keys = array_keys( $data );
  254. $column_formats = array_merge( array_flip( $data_keys ), $column_formats );
  255. if ( false === $wpdb->update( $this->table_name(), $data, array( $where => $row_id ), $column_formats ) ) {
  256. return false;
  257. }
  258. return true;
  259. }
  260. /**
  261. * Delete a row identified by the primary key
  262. *
  263. * @param int $row_id
  264. *
  265. * @return bool
  266. */
  267. public function delete( $row_id = 0 ) {
  268. global $wpdb;
  269. // Row ID must be positive integer
  270. $row_id = absint( $row_id );
  271. if ( empty( $row_id ) ) {
  272. return false;
  273. }
  274. if ( false === $wpdb->query( $wpdb->prepare( "DELETE FROM {$this->table_name()} WHERE $this->primary_key = %d", $row_id ) ) ) {
  275. return false;
  276. }
  277. return true;
  278. }
  279. /**
  280. * Delete a row identified by the primary key
  281. *
  282. * @param $column
  283. * @param $row_id
  284. *
  285. * @return bool
  286. */
  287. public function delete_by( $column, $row_id ) {
  288. global $wpdb;
  289. if ( empty( $row_id ) ) {
  290. return false;
  291. }
  292. if ( false === $wpdb->query( $wpdb->prepare( "DELETE FROM {$this->table_name()} WHERE $column = '%s'", $row_id ) ) ) {
  293. return false;
  294. }
  295. return true;
  296. }
  297. /**
  298. * Prepare query.
  299. *
  300. * @param $query
  301. * @param array $args
  302. *
  303. * @return string
  304. */
  305. public function prepare_query( $query, $args = array() ) {
  306. if ( $args['orderby'] ) {
  307. $query .= " ORDER BY {$args['orderby']} {$args['order']}";
  308. }
  309. $query .= " LIMIT {$args['limit']}";
  310. if ( $args['offset'] ) {
  311. $query .= " OFFSET {$args['offset']}";
  312. }
  313. $query .= ';';
  314. return $query;
  315. }
  316. /**
  317. * @param array $args
  318. * @param string $return_type
  319. *
  320. * @return array|mixed|object[]
  321. */
  322. public function query( $args = array(), $return_type = OBJECT ) {
  323. global $wpdb;
  324. $args = wp_parse_args( $args, array(
  325. 'fields' => '*',
  326. 'page' => null,
  327. 'limit' => null,
  328. 'offset' => null,
  329. 's' => null,
  330. 'orderby' => null,
  331. 'order' => null,
  332. ) );
  333. $columns = $this->get_columns();
  334. $fields = $args['fields'];
  335. if ( $fields == '*' ) {
  336. $fields = array_keys( $columns );
  337. } else {
  338. $fields = explode( ',', $args['fields'] );
  339. $fields = array_map( 'trim', $fields );
  340. $fields = array_map( 'sanitize_text_field', $fields );
  341. }
  342. $select_fields = implode( '`, `', $fields );
  343. // Begin building query.
  344. $query = "SELECT `$select_fields` FROM {$this->table_name()}";
  345. // Set up $values array for wpdb::prepare
  346. $values = array();
  347. // Define an empty WHERE clause to start from.
  348. $where = "WHERE 1=1";
  349. // Build search query.
  350. if ( $args['s'] && ! empty( $args['s'] ) ) {
  351. $search = wp_unslash( trim( $args['s'] ) );
  352. $search_where = array();
  353. foreach ( $columns as $key => $type ) {
  354. if ( in_array( $key, $fields ) ) {
  355. if ( $type == '%s' || ( $type == '%d' && is_numeric( $search ) ) ) {
  356. $values[] = '%' . $wpdb->esc_like( $search ) . '%';
  357. $search_where[] = "`$key` LIKE '%s'";
  358. }
  359. }
  360. }
  361. if ( ! empty( $search_where ) ) {
  362. $where .= ' AND (' . join( ' OR ', $search_where ) . ')';
  363. }
  364. }
  365. $query .= " $where";
  366. if ( ! empty( $args['orderby'] ) ) {
  367. $query .= " ORDER BY %s";
  368. $values[] = wp_unslash( trim( $args['orderby'] ) );
  369. switch ( $args['order'] ) {
  370. case 'asc':
  371. case 'ASC':
  372. $query .= " ASC";
  373. break;
  374. case 'desc':
  375. case 'DESC':
  376. default:
  377. $query .= " DESC";
  378. break;
  379. }
  380. }
  381. if ( ! empty( $args['limit'] ) ) {
  382. $query .= " LIMIT %d";
  383. $values[] = absint( $args['limit'] );
  384. }
  385. // Pagination.
  386. if ( $args['page'] >= 1 ) {
  387. $args['offset'] = ( $args['page'] * $args['limit'] ) - $args['limit'];
  388. }
  389. if ( ! empty( $args['offset'] ) ) {
  390. $query .= " OFFSET %d";
  391. $values[] = absint( $args['offset'] );
  392. }
  393. if ( strpos( $query, '%s' ) || strpos( $query, '%d' ) ) {
  394. $query = $wpdb->prepare( $query, $values );
  395. }
  396. return $this->prepare_results( $wpdb->get_results( $query, $return_type ) );
  397. }
  398. /**
  399. * Queries for total rows
  400. *
  401. * @param $args
  402. *
  403. * @return int
  404. */
  405. public function total_rows( $args ) {
  406. // TODO REVIEW this can probably be done more efficiently. Look at how we do it for DB models.
  407. $args['limit'] = null;
  408. $args['offset'] = null;
  409. $args['page'] = null;
  410. $results = $this->query( $args );
  411. return $results ? count( $results ) : 0;
  412. }
  413. }