/includes/abstracts/abstract-wc-data.php

  1. <?php 
  2. if ( ! defined( 'ABSPATH' ) ) { 
  3. exit; 
  4.  
  5. /** 
  6. * Abstract WC Data Class 
  7. * 
  8. * Implemented by classes using the same CRUD(s) pattern. 
  9. * 
  10. * @version 2.6.0 
  11. * @package WooCommerce/Abstracts 
  12. * @category Abstract Class 
  13. * @author WooThemes 
  14. */ 
  15. abstract class WC_Data { 
  16.  
  17. /** 
  18. * ID for this object. 
  19. * 
  20. * @since 3.0.0 
  21. * @var int 
  22. */ 
  23. protected $id = 0; 
  24.  
  25. /** 
  26. * Core data for this object. Name value pairs (name + default value). 
  27. * 
  28. * @since 3.0.0 
  29. * @var array 
  30. */ 
  31. protected $data = array(); 
  32.  
  33. /** 
  34. * Core data changes for this object. 
  35. * 
  36. * @since 3.0.0 
  37. * @var array 
  38. */ 
  39. protected $changes = array(); 
  40.  
  41. /** 
  42. * This is false until the object is read from the DB. 
  43. * 
  44. * @since 3.0.0 
  45. * @var bool 
  46. */ 
  47. protected $object_read = false; 
  48.  
  49. /** 
  50. * This is the name of this object type. 
  51. * 
  52. * @since 3.0.0 
  53. * @var string 
  54. */ 
  55. protected $object_type = 'data'; 
  56.  
  57. /** 
  58. * Extra data for this object. Name value pairs (name + default value). 
  59. * Used as a standard way for sub classes (like product types) to add 
  60. * additional information to an inherited class. 
  61. * 
  62. * @since 3.0.0 
  63. * @var array 
  64. */ 
  65. protected $extra_data = array(); 
  66.  
  67. /** 
  68. * Set to _data on construct so we can track and reset data if needed. 
  69. * 
  70. * @since 3.0.0 
  71. * @var array 
  72. */ 
  73. protected $default_data = array(); 
  74.  
  75. /** 
  76. * Contains a reference to the data store for this class. 
  77. * 
  78. * @since 3.0.0 
  79. * @var object 
  80. */ 
  81. protected $data_store; 
  82.  
  83. /** 
  84. * Stores meta in cache for future reads. 
  85. * A group must be set to to enable caching. 
  86. * 
  87. * @since 3.0.0 
  88. * @var string 
  89. */ 
  90. protected $cache_group = ''; 
  91.  
  92. /** 
  93. * Stores additional meta data. 
  94. * 
  95. * @since 3.0.0 
  96. * @var array 
  97. */ 
  98. protected $meta_data = null; 
  99.  
  100. /** 
  101. * Default constructor. 
  102. * 
  103. * @param int|object|array $read ID to load from the DB (optional) or already queried data. 
  104. */ 
  105. public function __construct( $read = 0 ) { 
  106. $this->data = array_merge( $this->data, $this->extra_data ); 
  107. $this->default_data = $this->data; 
  108.  
  109. /** 
  110. * Only store the object ID to avoid serializing the data object instance. 
  111. * 
  112. * @return array 
  113. */ 
  114. public function __sleep() { 
  115. return array( 'id' ); 
  116.  
  117. /** 
  118. * Re-run the constructor with the object ID. 
  119. * 
  120. * If the object no longer exists, remove the ID. 
  121. */ 
  122. public function __wakeup() { 
  123. try { 
  124. $this->__construct( absint( $this->id ) ); 
  125. } catch ( Exception $e ) { 
  126. $this->set_id( 0 ); 
  127. $this->set_object_read( true ); 
  128.  
  129. /** 
  130. * When the object is cloned, make sure meta is duplicated correctly. 
  131. * 
  132. * @since 3.0.2 
  133. */ 
  134. public function __clone() { 
  135. $this->maybe_read_meta_data(); 
  136. if ( ! empty( $this->meta_data ) ) { 
  137. foreach ( $this->meta_data as $array_key => $meta ) { 
  138. $this->meta_data[ $array_key ] = clone $meta; 
  139. if ( ! empty( $meta->id ) ) { 
  140. unset( $this->meta_data[ $array_key ]->id ); 
  141.  
  142. /** 
  143. * Get the data store. 
  144. * 
  145. * @since 3.0.0 
  146. * @return object 
  147. */ 
  148. public function get_data_store() { 
  149. return $this->data_store; 
  150.  
  151. /** 
  152. * Returns the unique ID for this object. 
  153. * 
  154. * @since 2.6.0 
  155. * @return int 
  156. */ 
  157. public function get_id() { 
  158. return $this->id; 
  159.  
  160. /** 
  161. * Delete an object, set the ID to 0, and return result. 
  162. * 
  163. * @since 2.6.0 
  164. * @param bool $force_delete 
  165. * @return bool result 
  166. */ 
  167. public function delete( $force_delete = false ) { 
  168. if ( $this->data_store ) { 
  169. $this->data_store->delete( $this, array( 'force_delete' => $force_delete ) ); 
  170. $this->set_id( 0 ); 
  171. return true; 
  172. return false; 
  173.  
  174. /** 
  175. * Save should create or update based on object existence. 
  176. * 
  177. * @since 2.6.0 
  178. * @return int 
  179. */ 
  180. public function save() { 
  181. if ( $this->data_store ) { 
  182. // Trigger action before saving to the DB. Allows you to adjust object props before save. 
  183. do_action( 'woocommerce_before_' . $this->object_type . '_object_save', $this, $this->data_store ); 
  184.  
  185. if ( $this->get_id() ) { 
  186. $this->data_store->update( $this ); 
  187. } else { 
  188. $this->data_store->create( $this ); 
  189. return $this->get_id(); 
  190.  
  191. /** 
  192. * Change data to JSON format. 
  193. * 
  194. * @since 2.6.0 
  195. * @return string Data in JSON format. 
  196. */ 
  197. public function __toString() { 
  198. return json_encode( $this->get_data() ); 
  199.  
  200. /** 
  201. * Returns all data for this object. 
  202. * 
  203. * @since 2.6.0 
  204. * @return array 
  205. */ 
  206. public function get_data() { 
  207. return array_merge( array( 'id' => $this->get_id() ), $this->data, array( 'meta_data' => $this->get_meta_data() ) ); 
  208.  
  209. /** 
  210. * Returns array of expected data keys for this object. 
  211. * 
  212. * @since 3.0.0 
  213. * @return array 
  214. */ 
  215. public function get_data_keys() { 
  216. return array_keys( $this->data ); 
  217.  
  218. /** 
  219. * Returns all "extra" data keys for an object (for sub objects like product types). 
  220. * 
  221. * @since 3.0.0 
  222. * @return array 
  223. */ 
  224. public function get_extra_data_keys() { 
  225. return array_keys( $this->extra_data ); 
  226.  
  227. /** 
  228. * Filter null meta values from array. 
  229. * 
  230. * @since 3.0.0 
  231. * @return bool 
  232. */ 
  233. protected function filter_null_meta( $meta ) { 
  234. return ! is_null( $meta->value ); 
  235.  
  236. /** 
  237. * Get All Meta Data. 
  238. * 
  239. * @since 2.6.0 
  240. * @return array 
  241. */ 
  242. public function get_meta_data() { 
  243. $this->maybe_read_meta_data(); 
  244. return array_filter( $this->meta_data, array( $this, 'filter_null_meta' ) ); 
  245.  
  246. /** 
  247. * Get Meta Data by Key. 
  248. * 
  249. * @since 2.6.0 
  250. * @param string $key 
  251. * @param bool $single return first found meta with key, or all with $key 
  252. * @param string $context What the value is for. Valid values are view and edit. 
  253. * @return mixed 
  254. */ 
  255. public function get_meta( $key = '', $single = true, $context = 'view' ) { 
  256. $this->maybe_read_meta_data(); 
  257. $meta_data = $this->get_meta_data(); 
  258. $array_keys = array_keys( wp_list_pluck( $meta_data, 'key' ), $key ); 
  259. $value = $single ? '' : array(); 
  260.  
  261. if ( ! empty( $array_keys ) ) { 
  262. // We don't use the $this->meta_data property directly here because we don't want meta with a null value (i.e. meta which has been deleted via $this->delete_meta_data()) 
  263. if ( $single ) { 
  264. $value = $meta_data[ current( $array_keys ) ]->value; 
  265. } else { 
  266. $value = array_intersect_key( $meta_data, array_flip( $array_keys ) ); 
  267.  
  268. if ( 'view' === $context ) { 
  269. $value = apply_filters( $this->get_hook_prefix() . $key, $value, $this ); 
  270.  
  271. return $value; 
  272.  
  273. /** 
  274. * See if meta data exists, since get_meta always returns a '' or array(). 
  275. * 
  276. * @since 3.0.0 
  277. * @param string $key 
  278. * @return boolean 
  279. */ 
  280. public function meta_exists( $key = '' ) { 
  281. $this->maybe_read_meta_data(); 
  282. $array_keys = wp_list_pluck( $this->get_meta_data(), 'key' ); 
  283. return in_array( $key, $array_keys ); 
  284.  
  285. /** 
  286. * Set all meta data from array. 
  287. * 
  288. * @since 2.6.0 
  289. * @param array $data Key/Value pairs 
  290. */ 
  291. public function set_meta_data( $data ) { 
  292. if ( ! empty( $data ) && is_array( $data ) ) { 
  293. $this->maybe_read_meta_data(); 
  294. foreach ( $data as $meta ) { 
  295. $meta = (array) $meta; 
  296. if ( isset( $meta['key'], $meta['value'], $meta['id'] ) ) { 
  297. $this->meta_data[] = (object) array( 
  298. 'id' => $meta['id'],  
  299. 'key' => $meta['key'],  
  300. 'value' => $meta['value'],  
  301. ); 
  302.  
  303. /** 
  304. * Add meta data. 
  305. * 
  306. * @since 2.6.0 
  307. * @param string $key Meta key 
  308. * @param string $value Meta value 
  309. * @param bool $unique Should this be a unique key? 
  310. */ 
  311. public function add_meta_data( $key, $value, $unique = false ) { 
  312. $this->maybe_read_meta_data(); 
  313. if ( $unique ) { 
  314. $this->delete_meta_data( $key ); 
  315. $this->meta_data[] = (object) array( 
  316. 'key' => $key,  
  317. 'value' => $value,  
  318. ); 
  319.  
  320. /** 
  321. * Update meta data by key or ID, if provided. 
  322. * 
  323. * @since 2.6.0 
  324. * @param string $key 
  325. * @param string $value 
  326. * @param int $meta_id 
  327. */ 
  328. public function update_meta_data( $key, $value, $meta_id = '' ) { 
  329. $this->maybe_read_meta_data(); 
  330. if ( $array_key = $meta_id ? array_keys( wp_list_pluck( $this->meta_data, 'id' ), $meta_id ) : '' ) { 
  331. $this->meta_data[ current( $array_key ) ] = (object) array( 
  332. 'id' => $meta_id,  
  333. 'key' => $key,  
  334. 'value' => $value,  
  335. ); 
  336. } else { 
  337. $this->add_meta_data( $key, $value, true ); 
  338.  
  339. /** 
  340. * Delete meta data. 
  341. * 
  342. * @since 2.6.0 
  343. * @param array $key Meta key 
  344. */ 
  345. public function delete_meta_data( $key ) { 
  346. $this->maybe_read_meta_data(); 
  347. if ( $array_keys = array_keys( wp_list_pluck( $this->meta_data, 'key' ), $key ) ) { 
  348. foreach ( $array_keys as $array_key ) { 
  349. $this->meta_data[ $array_key ]->value = null; 
  350.  
  351. /** 
  352. * Delete meta data. 
  353. * 
  354. * @since 2.6.0 
  355. * @param int $mid Meta ID 
  356. */ 
  357. public function delete_meta_data_by_mid( $mid ) { 
  358. $this->maybe_read_meta_data(); 
  359. if ( $array_keys = array_keys( wp_list_pluck( $this->meta_data, 'id' ), $mid ) ) { 
  360. foreach ( $array_keys as $array_key ) { 
  361. $this->meta_data[ $array_key ]->value = null; 
  362.  
  363. /** 
  364. * Read meta data if null. 
  365. * 
  366. * @since 3.0.0 
  367. */ 
  368. protected function maybe_read_meta_data() { 
  369. if ( is_null( $this->meta_data ) ) { 
  370. $this->read_meta_data(); 
  371.  
  372. /** 
  373. * Read Meta Data from the database. Ignore any internal properties. 
  374. * Uses it's own caches because get_metadata does not provide meta_ids. 
  375. * 
  376. * @since 2.6.0 
  377. * @param bool $force_read True to force a new DB read (and update cache). 
  378. */ 
  379. public function read_meta_data( $force_read = false ) { 
  380. $this->meta_data = array(); 
  381. $cache_loaded = false; 
  382.  
  383. if ( ! $this->get_id() ) { 
  384. return; 
  385.  
  386. if ( ! $this->data_store ) { 
  387. return; 
  388.  
  389. if ( ! empty( $this->cache_group ) ) { 
  390. $cache_key = WC_Cache_Helper::get_cache_prefix( $this->cache_group ) . 'object_meta_' . $this->get_id(); 
  391.  
  392. if ( ! $force_read ) { 
  393. if ( ! empty( $this->cache_group ) ) { 
  394. $cached_meta = wp_cache_get( $cache_key, $this->cache_group ); 
  395. $cache_loaded = ! empty( $cached_meta ); 
  396.  
  397. $raw_meta_data = $cache_loaded ? $cached_meta : $this->data_store->read_meta( $this ); 
  398. if ( $raw_meta_data ) { 
  399. foreach ( $raw_meta_data as $meta ) { 
  400. $this->meta_data[] = (object) array( 
  401. 'id' => (int) $meta->meta_id,  
  402. 'key' => $meta->meta_key,  
  403. 'value' => maybe_unserialize( $meta->meta_value ),  
  404. ); 
  405.  
  406. if ( ! $cache_loaded && ! empty( $this->cache_group ) ) { 
  407. wp_cache_set( $cache_key, $raw_meta_data, $this->cache_group ); 
  408.  
  409. /** 
  410. * Update Meta Data in the database. 
  411. * 
  412. * @since 2.6.0 
  413. */ 
  414. public function save_meta_data() { 
  415. if ( ! $this->data_store || is_null( $this->meta_data ) ) { 
  416. return; 
  417. foreach ( $this->meta_data as $array_key => $meta ) { 
  418. if ( is_null( $meta->value ) ) { 
  419. if ( ! empty( $meta->id ) ) { 
  420. $this->data_store->delete_meta( $this, $meta ); 
  421. unset( $this->meta_data[ $array_key ] ); 
  422. } elseif ( empty( $meta->id ) ) { 
  423. $new_meta_id = $this->data_store->add_meta( $this, $meta ); 
  424. $this->meta_data[ $array_key ]->id = $new_meta_id; 
  425. } else { 
  426. $this->data_store->update_meta( $this, $meta ); 
  427.  
  428. if ( ! empty( $this->cache_group ) ) { 
  429. WC_Cache_Helper::incr_cache_prefix( $this->cache_group ); 
  430.  
  431. /** 
  432. * Set ID. 
  433. * 
  434. * @since 3.0.0 
  435. * @param int $id 
  436. */ 
  437. public function set_id( $id ) { 
  438. $this->id = absint( $id ); 
  439.  
  440. /** 
  441. * Set all props to default values. 
  442. * 
  443. * @since 3.0.0 
  444. */ 
  445. public function set_defaults() { 
  446. $this->data = $this->default_data; 
  447. $this->changes = array(); 
  448. $this->set_object_read( false ); 
  449.  
  450. /** 
  451. * Set object read property. 
  452. * 
  453. * @since 3.0.0 
  454. * @param boolean $read 
  455. */ 
  456. public function set_object_read( $read = true ) { 
  457. $this->object_read = (bool) $read; 
  458.  
  459. /** 
  460. * Get object read property. 
  461. * 
  462. * @since 3.0.0 
  463. * @return boolean 
  464. */ 
  465. public function get_object_read() { 
  466. return (bool) $this->object_read; 
  467.  
  468. /** 
  469. * Set a collection of props in one go, collect any errors, and return the result. 
  470. * Only sets using public methods. 
  471. * 
  472. * @since 3.0.0 
  473. * @param array $props Key value pairs to set. Key is the prop and should map to a setter function name. 
  474. * @return WP_Error|bool 
  475. */ 
  476. public function set_props( $props, $context = 'set' ) { 
  477. $errors = new WP_Error(); 
  478.  
  479. foreach ( $props as $prop => $value ) { 
  480. try { 
  481. if ( 'meta_data' === $prop ) { 
  482. continue; 
  483. $setter = "set_$prop"; 
  484. if ( ! is_null( $value ) && is_callable( array( $this, $setter ) ) ) { 
  485. $reflection = new ReflectionMethod( $this, $setter ); 
  486.  
  487. if ( $reflection->isPublic() ) { 
  488. $this->{$setter}( $value ); 
  489. } catch ( WC_Data_Exception $e ) { 
  490. $errors->add( $e->getErrorCode(), $e->getMessage() ); 
  491.  
  492. return sizeof( $errors->get_error_codes() ) ? $errors : true; 
  493.  
  494. /** 
  495. * Sets a prop for a setter method. 
  496. * 
  497. * This stores changes in a special array so we can track what needs saving 
  498. * the the DB later. 
  499. * 
  500. * @since 3.0.0 
  501. * @param string $prop Name of prop to set. 
  502. * @param mixed $value Value of the prop. 
  503. */ 
  504. protected function set_prop( $prop, $value ) { 
  505. if ( array_key_exists( $prop, $this->data ) ) { 
  506. if ( true === $this->object_read ) { 
  507. if ( $value !== $this->data[ $prop ] || array_key_exists( $prop, $this->changes ) ) { 
  508. $this->changes[ $prop ] = $value; 
  509. } else { 
  510. $this->data[ $prop ] = $value; 
  511.  
  512. /** 
  513. * Return data changes only. 
  514. * 
  515. * @since 3.0.0 
  516. * @return array 
  517. */ 
  518. public function get_changes() { 
  519. return $this->changes; 
  520.  
  521. /** 
  522. * Merge changes with data and clear. 
  523. * 
  524. * @since 3.0.0 
  525. */ 
  526. public function apply_changes() { 
  527. $this->data = array_replace_recursive( $this->data, $this->changes ); 
  528. $this->changes = array(); 
  529.  
  530. /** 
  531. * Prefix for action and filter hooks on data. 
  532. * 
  533. * @since 3.0.0 
  534. * @return string 
  535. */ 
  536. protected function get_hook_prefix() { 
  537. return 'woocommerce_' . $this->object_type . '_get_'; 
  538.  
  539. /** 
  540. * Gets a prop for a getter method. 
  541. * 
  542. * Gets the value from either current pending changes, or the data itself. 
  543. * Context controls what happens to the value before it's returned. 
  544. * 
  545. * @since 3.0.0 
  546. * @param string $prop Name of prop to get. 
  547. * @param string $context What the value is for. Valid values are view and edit. 
  548. * @return mixed 
  549. */ 
  550. protected function get_prop( $prop, $context = 'view' ) { 
  551. $value = null; 
  552.  
  553. if ( array_key_exists( $prop, $this->data ) ) { 
  554. $value = array_key_exists( $prop, $this->changes ) ? $this->changes[ $prop ] : $this->data[ $prop ]; 
  555.  
  556. if ( 'view' === $context ) { 
  557. $value = apply_filters( $this->get_hook_prefix() . $prop, $value, $this ); 
  558.  
  559. return $value; 
  560.  
  561. /** 
  562. * Sets a date prop whilst handling formatting and datatime objects. 
  563. * 
  564. * @since 3.0.0 
  565. * @param string $prop Name of prop to set. 
  566. * @param string|integer $value Value of the prop. 
  567. */ 
  568. protected function set_date_prop( $prop, $value ) { 
  569. try { 
  570. if ( empty( $value ) ) { 
  571. $this->set_prop( $prop, null ); 
  572. return; 
  573.  
  574. if ( is_a( $value, 'WC_DateTime' ) ) { 
  575. $datetime = $value; 
  576. } elseif ( is_numeric( $value ) ) { 
  577. // Timestamps are handled as UTC timestamps in all cases. 
  578. $datetime = new WC_DateTime( "@{$value}", new DateTimeZone( 'UTC' ) ); 
  579. } else { 
  580. // Strings are defined in local WP timezone. Convert to UTC. 
  581. if ( 1 === preg_match( '/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(Z|((-|\+)\d{2}:\d{2}))$/', $value, $date_bits ) ) { 
  582. $offset = ! empty( $date_bits[7] ) ? iso8601_timezone_to_offset( $date_bits[7] ) : wc_timezone_offset(); 
  583. $timestamp = gmmktime( $date_bits[4], $date_bits[5], $date_bits[6], $date_bits[2], $date_bits[3], $date_bits[1] ) - $offset; 
  584. } else { 
  585. $timestamp = wc_string_to_timestamp( get_gmt_from_date( gmdate( 'Y-m-d H:i:s', wc_string_to_timestamp( $value ) ) ) ); 
  586. $datetime = new WC_DateTime( "@{$timestamp}", new DateTimeZone( 'UTC' ) ); 
  587.  
  588. // Set local timezone or offset. 
  589. if ( get_option( 'timezone_string' ) ) { 
  590. $datetime->setTimezone( new DateTimeZone( wc_timezone_string() ) ); 
  591. } else { 
  592. $datetime->set_utc_offset( wc_timezone_offset() ); 
  593.  
  594. $this->set_prop( $prop, $datetime ); 
  595. } catch ( Exception $e ) {} 
  596.  
  597. /** 
  598. * When invalid data is found, throw an exception unless reading from the DB. 
  599. * 
  600. * @throws WC_Data_Exception 
  601. * @since 3.0.0 
  602. * @param string $code Error code. 
  603. * @param string $message Error message. 
  604. * @param int $http_status_code HTTP status code. 
  605. * @param array $data Extra error data. 
  606. */ 
  607. protected function error( $code, $message, $http_status_code = 400, $data = array() ) { 
  608. throw new WC_Data_Exception( $code, $message, $http_status_code, $data ); 
.