Make WordPress Core

Ticket #61373: abstract-wc-data.php.txt

File abstract-wc-data.php.txt, 24.8 KB (added by lucasli26, 14 months ago)
Line 
1<?php
2/**
3 * Abstract Data.
4 *
5 * Handles generic data interaction which is implemented by
6 * the different data store classes.
7 *
8 * @class       WC_Data
9 * @version     3.0.0
10 * @package     WooCommerce\Classes
11 */
12
13if ( ! defined( 'ABSPATH' ) ) {
14        exit;
15}
16
17/**
18 * Abstract WC Data Class
19 *
20 * Implemented by classes using the same CRUD(s) pattern.
21 *
22 * @version  2.6.0
23 * @package  WooCommerce\Abstracts
24 */
25abstract class WC_Data {
26
27        /**
28         * ID for this object.
29         *
30         * @since 3.0.0
31         * @var int
32         */
33        protected $id = 0;
34
35        /**
36         * Core data for this object. Name value pairs (name + default value).
37         *
38         * @since 3.0.0
39         * @var array
40         */
41        protected $data = array();
42
43        /**
44         * Core data changes for this object.
45         *
46         * @since 3.0.0
47         * @var array
48         */
49        protected $changes = array();
50
51        /**
52         * This is false until the object is read from the DB.
53         *
54         * @since 3.0.0
55         * @var bool
56         */
57        protected $object_read = false;
58
59        /**
60         * This is the name of this object type.
61         *
62         * @since 3.0.0
63         * @var string
64         */
65        protected $object_type = 'data';
66
67        /**
68         * Extra data for this object. Name value pairs (name + default value).
69         * Used as a standard way for sub classes (like product types) to add
70         * additional information to an inherited class.
71         *
72         * @since 3.0.0
73         * @var array
74         */
75        protected $extra_data = array();
76
77        /**
78         * Set to _data on construct so we can track and reset data if needed.
79         *
80         * @since 3.0.0
81         * @var array
82         */
83        protected $default_data = array();
84
85        /**
86         * Contains a reference to the data store for this class.
87         *
88         * @since 3.0.0
89         * @var object
90         */
91        protected $data_store;
92
93        /**
94         * Stores meta in cache for future reads.
95         * A group must be set to to enable caching.
96         *
97         * @since 3.0.0
98         * @var string
99         */
100        protected $cache_group = '';
101
102        /**
103         * Stores additional meta data.
104         *
105         * @since 3.0.0
106         * @var array
107         */
108        protected $meta_data = null;
109
110        /**
111         * List of properties that were earlier managed by data store. However, since DataStore is a not a stored entity in itself, they used to store data in metadata of the data object.
112         * With custom tables, some of these are moved from metadata to their own columns, but existing code will still try to add them to metadata. This array is used to keep track of such properties.
113         *
114         * Only reason to add a property here is that you are moving properties from DataStore instance to data object. If you are adding a new property, consider adding it to to $data array instead.
115         *
116         * @var array
117         */
118        protected $legacy_datastore_props = array();
119
120        /**
121         * Default constructor.
122         *
123         * @param int|object|array $read ID to load from the DB (optional) or already queried data.
124         */
125        public function __construct( $read = 0 ) {
126                $this->data         = array_merge( $this->data, $this->extra_data );
127                $this->default_data = $this->data;
128        }
129
130        /**
131         * Only store the object ID to avoid serializing the data object instance.
132         *
133         * @return array
134         */
135        public function __sleep() {
136                return array( 'id' );
137        }
138
139        /**
140         * Re-run the constructor with the object ID.
141         *
142         * If the object no longer exists, remove the ID.
143         */
144        public function __wakeup() {
145                try {
146                        $this->__construct( absint( $this->id ) );
147                } catch ( Exception $e ) {
148                        $this->set_id( 0 );
149                        $this->set_object_read( true );
150                }
151        }
152
153        /**
154         * When the object is cloned, make sure meta is duplicated correctly.
155         *
156         * @since 3.0.2
157         */
158        public function __clone() {
159                $this->maybe_read_meta_data();
160                if ( ! empty( $this->meta_data ) ) {
161                        foreach ( $this->meta_data as $array_key => $meta ) {
162                                $this->meta_data[ $array_key ] = clone $meta;
163                                if ( ! empty( $meta->id ) ) {
164                                        $this->meta_data[ $array_key ]->id = null;
165                                }
166                        }
167                }
168        }
169
170        /**
171         * Get the data store.
172         *
173         * @since  3.0.0
174         * @return object
175         */
176        public function get_data_store() {
177                return $this->data_store;
178        }
179
180        /**
181         * Returns the unique ID for this object.
182         *
183         * @since  2.6.0
184         * @return int
185         */
186        public function get_id() {
187                return $this->id;
188        }
189
190        /**
191         * Delete an object, set the ID to 0, and return result.
192         *
193         * @since  2.6.0
194         * @param  bool $force_delete Should the date be deleted permanently.
195         * @return bool result
196         */
197        public function delete( $force_delete = false ) {
198                /**
199                 * Filters whether an object deletion should take place. Equivalent to `pre_delete_post`.
200                 *
201                 * @param mixed   $check Whether to go ahead with deletion.
202                 * @param WC_Data $this The data object being deleted.
203                 * @param bool    $force_delete Whether to bypass the trash.
204                 *
205                 * @since 8.1.0.
206                 */
207                $check = apply_filters( "woocommerce_pre_delete_$this->object_type", null, $this, $force_delete );
208                if ( null !== $check ) {
209                        return $check;
210                }
211                if ( $this->data_store ) {
212                        $this->data_store->delete( $this, array( 'force_delete' => $force_delete ) );
213                        $this->set_id( 0 );
214                        return true;
215                }
216                return false;
217        }
218
219        /**
220         * Save should create or update based on object existence.
221         *
222         * @since  2.6.0
223         * @return int
224         */
225        public function save() {
226                if ( ! $this->data_store ) {
227                        return $this->get_id();
228                }
229
230                /**
231                 * Trigger action before saving to the DB. Allows you to adjust object props before save.
232                 *
233                 * @param WC_Data          $this The object being saved.
234                 * @param WC_Data_Store_WP $data_store THe data store persisting the data.
235                 */
236                do_action( 'woocommerce_before_' . $this->object_type . '_object_save', $this, $this->data_store );
237
238                if ( $this->get_id() ) {
239                        $this->data_store->update( $this );
240                } else {
241                        $this->data_store->create( $this );
242                }
243
244                /**
245                 * Trigger action after saving to the DB.
246                 *
247                 * @param WC_Data          $this The object being saved.
248                 * @param WC_Data_Store_WP $data_store THe data store persisting the data.
249                 */
250                do_action( 'woocommerce_after_' . $this->object_type . '_object_save', $this, $this->data_store );
251
252                return $this->get_id();
253        }
254
255        /**
256         * Change data to JSON format.
257         *
258         * @since  2.6.0
259         * @return string Data in JSON format.
260         */
261        public function __toString() {
262                return wp_json_encode( $this->get_data() );
263        }
264
265        /**
266         * Returns all data for this object.
267         *
268         * @since  2.6.0
269         * @return array
270         */
271        public function get_data() {
272                return array_merge( array( 'id' => $this->get_id() ), $this->data, array( 'meta_data' => $this->get_meta_data() ) );
273        }
274
275        /**
276         * Returns array of expected data keys for this object.
277         *
278         * @since   3.0.0
279         * @return array
280         */
281        public function get_data_keys() {
282                return array_keys( $this->data );
283        }
284
285        /**
286         * Returns all "extra" data keys for an object (for sub objects like product types).
287         *
288         * @since  3.0.0
289         * @return array
290         */
291        public function get_extra_data_keys() {
292                return array_keys( $this->extra_data );
293        }
294
295        /**
296         * Filter null meta values from array.
297         *
298         * @since  3.0.0
299         * @param mixed $meta Meta value to check.
300         * @return bool
301         */
302        protected function filter_null_meta( $meta ) {
303                return ! is_null( $meta->value );
304        }
305
306        /**
307         * Get All Meta Data.
308         *
309         * @since 2.6.0
310         * @return array of objects.
311         */
312        public function get_meta_data() {
313                $this->maybe_read_meta_data();
314                return array_values( array_filter( $this->meta_data, array( $this, 'filter_null_meta' ) ) );
315        }
316
317        /**
318         * Check if the key is an internal one.
319         *
320         * @since  3.2.0
321         * @param  string $key Key to check.
322         * @return bool   true if it's an internal key, false otherwise
323         */
324        protected function is_internal_meta_key( $key ) {
325                $internal_meta_key = ! empty( $key ) && $this->data_store && in_array( $key, $this->data_store->get_internal_meta_keys(), true );
326
327                if ( ! $internal_meta_key ) {
328                        return false;
329                }
330
331                $has_setter_or_getter = is_callable( array( $this, 'set_' . ltrim( $key, '_' ) ) ) || is_callable( array( $this, 'get_' . ltrim( $key, '_' ) ) );
332
333                if ( ! $has_setter_or_getter ) {
334                        return false;
335                }
336
337                if ( in_array( $key, $this->legacy_datastore_props, true ) ) {
338                        return true; // return without warning because we don't want to break legacy code which was calling add/get/update/delete meta.
339                }
340
341                /* translators: %s: $key Key to check */
342                wc_doing_it_wrong( __FUNCTION__, sprintf( __( 'Generic add/update/get meta methods should not be used for internal meta data, including "%s". Use getters and setters.', 'woocommerce' ), $key ), '3.2.0' );
343
344                return true;
345        }
346
347        /**
348         * Get Meta Data by Key.
349         *
350         * @since  2.6.0
351         * @param  string $key Meta Key.
352         * @param  bool   $single return first found meta with key, or all with $key.
353         * @param  string $context What the value is for. Valid values are view and edit.
354         * @return mixed
355         */
356        public function get_meta( $key = '', $single = true, $context = 'view' ) {
357                if ( $this->is_internal_meta_key( $key ) ) {
358                        $function = 'get_' . ltrim( $key, '_' );
359
360                        if ( is_callable( array( $this, $function ) ) ) {
361                                return $this->{$function}();
362                        }
363                }
364
365                $this->maybe_read_meta_data();
366                $meta_data  = $this->get_meta_data();
367                $array_keys = array_keys( wp_list_pluck( $meta_data, 'key' ), $key, true );
368                $value      = $single ? '' : array();
369
370                if ( ! empty( $array_keys ) ) {
371                        // 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()).
372                        if ( $single ) {
373                                $value = $meta_data[ current( $array_keys ) ]->value;
374                        } else {
375                                $value = array_intersect_key( $meta_data, array_flip( $array_keys ) );
376                        }
377                }
378
379                if ( 'view' === $context ) {
380                        $value = apply_filters( $this->get_hook_prefix() . $key, $value, $this );
381                }
382
383                return $value;
384        }
385
386        /**
387         * See if meta data exists, since get_meta always returns a '' or array().
388         *
389         * @since  3.0.0
390         * @param  string $key Meta Key.
391         * @return boolean
392         */
393        public function meta_exists( $key = '' ) {
394                $this->maybe_read_meta_data();
395                $array_keys = wp_list_pluck( $this->get_meta_data(), 'key' );
396                return in_array( $key, $array_keys, true );
397        }
398
399        /**
400         * Set all meta data from array.
401         *
402         * @since 2.6.0
403         * @param array $data Key/Value pairs.
404         */
405        public function set_meta_data( $data ) {
406                if ( ! empty( $data ) && is_array( $data ) ) {
407                        $this->maybe_read_meta_data();
408                        foreach ( $data as $meta ) {
409                                $meta = (array) $meta;
410                                if ( isset( $meta['key'], $meta['value'], $meta['id'] ) ) {
411                                        $this->meta_data[] = new WC_Meta_Data(
412                                                array(
413                                                        'id'    => $meta['id'],
414                                                        'key'   => $meta['key'],
415                                                        'value' => $meta['value'],
416                                                )
417                                        );
418                                }
419                        }
420                }
421        }
422
423        /**
424         * Add meta data.
425         *
426         * @since 2.6.0
427         *
428         * @param string       $key Meta key.
429         * @param string|array $value Meta value.
430         * @param bool         $unique Should this be a unique key?.
431         */
432        public function add_meta_data( $key, $value, $unique = false ) {
433                if ( $this->is_internal_meta_key( $key ) ) {
434                        $function = 'set_' . ltrim( $key, '_' );
435
436                        if ( is_callable( array( $this, $function ) ) ) {
437                                return $this->{$function}( $value );
438                        }
439                }
440
441                $this->maybe_read_meta_data();
442                if ( $unique ) {
443                        $this->delete_meta_data( $key );
444                }
445                $this->meta_data[] = new WC_Meta_Data(
446                        array(
447                                'key'   => $key,
448                                'value' => $value,
449                        )
450                );
451        }
452
453        /**
454         * Update meta data by key or ID, if provided.
455         *
456         * @since  2.6.0
457         *
458         * @param  string       $key Meta key.
459         * @param  string|array $value Meta value.
460         * @param  int          $meta_id Meta ID.
461         */
462        public function update_meta_data( $key, $value, $meta_id = 0 ) {
463                if ( $this->is_internal_meta_key( $key ) ) {
464                        $function = 'set_' . ltrim( $key, '_' );
465
466                        if ( is_callable( array( $this, $function ) ) ) {
467                                return $this->{$function}( $value );
468                        }
469                }
470
471                $this->maybe_read_meta_data();
472
473                $array_key = false;
474
475                if ( $meta_id ) {
476                        $array_keys = array_keys( wp_list_pluck( $this->meta_data, 'id' ), $meta_id, true );
477                        $array_key  = $array_keys ? current( $array_keys ) : false;
478                } else {
479                        // Find matches by key.
480                        $matches = array();
481                        foreach ( $this->meta_data as $meta_data_array_key => $meta ) {
482                                if ( $meta->key === $key ) {
483                                        $matches[] = $meta_data_array_key;
484                                }
485                        }
486
487                        if ( ! empty( $matches ) ) {
488                                // Set matches to null so only one key gets the new value.
489                                foreach ( $matches as $meta_data_array_key ) {
490                                        $this->meta_data[ $meta_data_array_key ]->value = null;
491                                }
492                                $array_key = current( $matches );
493                        }
494                }
495
496                if ( false !== $array_key ) {
497                        $meta        = $this->meta_data[ $array_key ];
498                        $meta->key   = $key;
499                        $meta->value = $value;
500                } else {
501                        $this->add_meta_data( $key, $value, true );
502                }
503        }
504
505        /**
506         * Delete meta data.
507         *
508         * @since 2.6.0
509         * @param string $key Meta key.
510         */
511        public function delete_meta_data( $key ) {
512                $this->maybe_read_meta_data();
513                $array_keys = array_keys( wp_list_pluck( $this->meta_data, 'key' ), $key, true );
514
515                if ( $array_keys ) {
516                        foreach ( $array_keys as $array_key ) {
517                                $this->meta_data[ $array_key ]->value = null;
518                        }
519                }
520        }
521
522        /**
523         * Delete meta data with a matching value.
524         *
525         * @since 7.7.0
526         * @param string $key   Meta key.
527         * @param mixed  $value Meta value. Entries will only be removed that match the value.
528         */
529        public function delete_meta_data_value( $key, $value ) {
530                $this->maybe_read_meta_data();
531                $array_keys = array_keys( wp_list_pluck( $this->meta_data, 'key' ), $key, true );
532
533                if ( $array_keys ) {
534                        foreach ( $array_keys as $array_key ) {
535                                if ( $value === $this->meta_data[ $array_key ]->value ) {
536                                        $this->meta_data[ $array_key ]->value = null;
537                                }
538                        }
539                }
540        }
541
542        /**
543         * Delete meta data.
544         *
545         * @since 2.6.0
546         * @param int $mid Meta ID.
547         */
548        public function delete_meta_data_by_mid( $mid ) {
549                $this->maybe_read_meta_data();
550                $array_keys = array_keys( wp_list_pluck( $this->meta_data, 'id' ), (int) $mid, true );
551
552                if ( $array_keys ) {
553                        foreach ( $array_keys as $array_key ) {
554                                $this->meta_data[ $array_key ]->value = null;
555                        }
556                }
557        }
558
559        /**
560         * Read meta data if null.
561         *
562         * @since 3.0.0
563         */
564        protected function maybe_read_meta_data() {
565                if ( is_null( $this->meta_data ) ) {
566                        $this->read_meta_data();
567                }
568        }
569
570        /**
571         * Helper method to compute meta cache key. Different from WP Meta cache key in that meta data cached using this key also contains meta_id column.
572         *
573         * @since 4.7.0
574         *
575         * @return string
576         */
577        public function get_meta_cache_key() {
578                if ( ! $this->get_id() ) {
579                        wc_doing_it_wrong( 'get_meta_cache_key', 'ID needs to be set before fetching a cache key.', '4.7.0' );
580                        return false;
581                }
582                return self::generate_meta_cache_key( $this->get_id(), $this->cache_group );
583        }
584
585        /**
586         * Generate cache key from id and group.
587         *
588         * @since 4.7.0
589         *
590         * @param int|string $id          Object ID.
591         * @param string     $cache_group Group name use to store cache. Whole group cache can be invalidated in one go.
592         *
593         * @return string Meta cache key.
594         */
595        public static function generate_meta_cache_key( $id, $cache_group ) {
596                return WC_Cache_Helper::get_cache_prefix( $cache_group ) . WC_Cache_Helper::get_cache_prefix( 'object_' . $id ) . 'object_meta_' . $id;
597        }
598
599        /**
600         * Prime caches for raw meta data. This includes meta_id column as well, which is not included by default in WP meta data.
601         *
602         * @since 4.7.0
603         *
604         * @param array  $raw_meta_data_collection Array of objects of { object_id => array( meta_row_1, meta_row_2, ... }.
605         * @param string $cache_group              Name of cache group.
606         */
607        public static function prime_raw_meta_data_cache( $raw_meta_data_collection, $cache_group ) {
608                foreach ( $raw_meta_data_collection as $object_id => $raw_meta_data_array ) {
609                        $cache_key = self::generate_meta_cache_key( $object_id, $cache_group );
610                        wp_cache_set( $cache_key, $raw_meta_data_array, $cache_group );
611                }
612        }
613
614        /**
615         * Read Meta Data from the database. Ignore any internal properties.
616         * Uses it's own caches because get_metadata does not provide meta_ids.
617         *
618         * @since 2.6.0
619         * @param bool $force_read True to force a new DB read (and update cache).
620         */
621        public function read_meta_data( $force_read = false ) {
622                $this->meta_data = array();
623                $cache_loaded    = false;
624
625                if ( ! $this->get_id() ) {
626                        return;
627                }
628
629                if ( ! $this->data_store ) {
630                        return;
631                }
632
633                if ( ! empty( $this->cache_group ) ) {
634                        // Prefix by group allows invalidation by group until https://core.trac.wordpress.org/ticket/4476 is implemented.
635                        $cache_key = $this->get_meta_cache_key();
636                }
637
638                if ( ! $force_read ) {
639                        if ( ! empty( $this->cache_group ) ) {
640                                $cached_meta  = wp_cache_get( $cache_key, $this->cache_group );
641                                $cache_loaded = is_array( $cached_meta );
642                        }
643                }
644
645                // We filter the raw meta data again when loading from cache, in case we cached in an earlier version where filter conditions were different.
646                $raw_meta_data = $cache_loaded ? $this->data_store->filter_raw_meta_data( $this, $cached_meta ) : $this->data_store->read_meta( $this );
647
648                if ( is_array( $raw_meta_data ) ) {
649                        $this->init_meta_data( $raw_meta_data );
650                        if ( ! $cache_loaded && ! empty( $this->cache_group ) ) {
651                                wp_cache_set( $cache_key, $raw_meta_data, $this->cache_group );
652                        }
653                }
654        }
655
656        /**
657         * Helper function to initialize metadata entries from filtered raw meta data.
658         *
659         * @param array $filtered_meta_data Filtered metadata fetched from DB.
660         */
661        public function init_meta_data( array $filtered_meta_data = array() ) {
662                $this->meta_data = array();
663                foreach ( $filtered_meta_data as $meta ) {
664                        $this->meta_data[] = new WC_Meta_Data(
665                                array(
666                                        'id'    => (int) $meta->meta_id,
667                                        'key'   => $meta->meta_key,
668                                        'value' => maybe_unserialize( $meta->meta_value ),
669                                )
670                        );
671                }
672        }
673
674        /**
675         * Update Meta Data in the database.
676         *
677         * @since 2.6.0
678         */
679        public function save_meta_data() {
680                if ( ! $this->data_store || is_null( $this->meta_data ) ) {
681                        return;
682                }
683                foreach ( $this->meta_data as $array_key => $meta ) {
684                        if ( is_null( $meta->value ) ) {
685                                if ( ! empty( $meta->id ) ) {
686                                        $this->data_store->delete_meta( $this, $meta );
687                                        /**
688                                         * Fires immediately after deleting metadata.
689                                         *
690                                         * @param int    $meta_id    ID of deleted metadata entry.
691                                         * @param int    $object_id  Object ID.
692                                         * @param string $meta_key   Metadata key.
693                                         * @param mixed  $meta_value Metadata value (will be empty for delete).
694                                         */
695                                        do_action( "deleted_{$this->object_type}_meta", $meta->id, $this->get_id(), $meta->key, $meta->value );
696
697                                        unset( $this->meta_data[ $array_key ] );
698                                }
699                        } elseif ( empty( $meta->id ) ) {
700                                $meta->id = $this->data_store->add_meta( $this, $meta );
701                                /**
702                                 * Fires immediately after adding metadata.
703                                 *
704                                 * @param int    $meta_id    ID of added metadata entry.
705                                 * @param int    $object_id  Object ID.
706                                 * @param string $meta_key   Metadata key.
707                                 * @param mixed  $meta_value Metadata value.
708                                 */
709                                do_action( "added_{$this->object_type}_meta", $meta->id, $this->get_id(), $meta->key, $meta->value );
710
711                                $meta->apply_changes();
712                        } else {
713                                if ( $meta->get_changes() ) {
714                                        $this->data_store->update_meta( $this, $meta );
715                                        /**
716                                         * Fires immediately after updating metadata.
717                                         *
718                                         * @param int    $meta_id    ID of updated metadata entry.
719                                         * @param int    $object_id  Object ID.
720                                         * @param string $meta_key   Metadata key.
721                                         * @param mixed  $meta_value Metadata value.
722                                         */
723                                        do_action( "updated_{$this->object_type}_meta", $meta->id, $this->get_id(), $meta->key, $meta->value );
724
725                                        $meta->apply_changes();
726                                }
727                        }
728                }
729                if ( ! empty( $this->cache_group ) ) {
730                        $cache_key = self::generate_meta_cache_key( $this->get_id(), $this->cache_group );
731                        wp_cache_delete( $cache_key, $this->cache_group );
732                }
733        }
734
735        /**
736         * Set ID.
737         *
738         * @since 3.0.0
739         * @param int $id ID.
740         */
741        public function set_id( $id ) {
742                $this->id = absint( $id );
743        }
744
745        /**
746         * Set all props to default values.
747         *
748         * @since 3.0.0
749         */
750        public function set_defaults() {
751                $this->data    = $this->default_data;
752                $this->changes = array();
753                $this->set_object_read( false );
754        }
755
756        /**
757         * Set object read property.
758         *
759         * @since 3.0.0
760         * @param boolean $read Should read?.
761         */
762        public function set_object_read( $read = true ) {
763                $this->object_read = (bool) $read;
764        }
765
766        /**
767         * Get object read property.
768         *
769         * @since  3.0.0
770         * @return boolean
771         */
772        public function get_object_read() {
773                return (bool) $this->object_read;
774        }
775
776        /**
777         * Set a collection of props in one go, collect any errors, and return the result.
778         * Only sets using public methods.
779         *
780         * @since  3.0.0
781         *
782         * @param array  $props Key value pairs to set. Key is the prop and should map to a setter function name.
783         * @param string $context In what context to run this.
784         *
785         * @return bool|WP_Error
786         */
787        public function set_props( $props, $context = 'set' ) {
788                $errors = false;
789
790                foreach ( $props as $prop => $value ) {
791                        try {
792                                /**
793                                 * Checks if the prop being set is allowed, and the value is not null.
794                                 */
795                                if ( is_null( $value ) || in_array( $prop, array( 'prop', 'date_prop', 'meta_data' ), true ) ) {
796                                        continue;
797                                }
798                                $setter = "set_$prop";
799
800                                if ( is_callable( array( $this, $setter ) ) ) {
801                                        $this->{$setter}( $value );
802                                }
803                        } catch ( WC_Data_Exception $e ) {
804                                if ( ! $errors ) {
805                                        $errors = new WP_Error();
806                                }
807                                $errors->add( $e->getErrorCode(), $e->getMessage(), array( 'property_name' => $prop ) );
808                        }
809                }
810
811                return $errors && count( $errors->get_error_codes() ) ? $errors : true;
812        }
813
814        /**
815         * Sets a prop for a setter method.
816         *
817         * This stores changes in a special array so we can track what needs saving
818         * the the DB later.
819         *
820         * @since 3.0.0
821         * @param string $prop Name of prop to set.
822         * @param mixed  $value Value of the prop.
823         */
824        protected function set_prop( $prop, $value ) {
825                if ( array_key_exists( $prop, $this->data ) ) {
826                        if ( true === $this->object_read ) {
827                                if ( $value !== $this->data[ $prop ] || array_key_exists( $prop, $this->changes ) ) {
828                                        $this->changes[ $prop ] = $value;
829                                }
830                        } else {
831                                $this->data[ $prop ] = $value;
832                        }
833                }
834        }
835
836        /**
837         * Return data changes only.
838         *
839         * @since 3.0.0
840         * @return array
841         */
842        public function get_changes() {
843                return $this->changes;
844        }
845
846        /**
847         * Merge changes with data and clear.
848         *
849         * @since 3.0.0
850         */
851        public function apply_changes() {
852                $this->data    = array_replace_recursive( $this->data, $this->changes ); // @codingStandardsIgnoreLine
853                $this->changes = array();
854        }
855
856        /**
857         * Prefix for action and filter hooks on data.
858         *
859         * @since  3.0.0
860         * @return string
861         */
862        protected function get_hook_prefix() {
863                return 'woocommerce_' . $this->object_type . '_get_';
864        }
865
866        /**
867         * Gets a prop for a getter method.
868         *
869         * Gets the value from either current pending changes, or the data itself.
870         * Context controls what happens to the value before it's returned.
871         *
872         * @since  3.0.0
873         * @param  string $prop Name of prop to get.
874         * @param  string $context What the value is for. Valid values are view and edit.
875         * @return mixed
876         */
877        protected function get_prop( $prop, $context = 'view' ) {
878                $value = null;
879
880                if ( array_key_exists( $prop, $this->data ) ) {
881                        $value = array_key_exists( $prop, $this->changes ) ? $this->changes[ $prop ] : $this->data[ $prop ];
882
883                        if ( 'view' === $context ) {
884                                $value = apply_filters( $this->get_hook_prefix() . $prop, $value, $this );
885                        }
886                }
887
888                return $value;
889        }
890
891        /**
892         * Sets a date prop whilst handling formatting and datetime objects.
893         *
894         * @since 3.0.0
895         * @param string         $prop Name of prop to set.
896         * @param string|integer $value Value of the prop.
897         */
898        protected function set_date_prop( $prop, $value ) {
899                try {
900                        if ( empty( $value ) || '0000-00-00 00:00:00' === $value ) {
901                                $this->set_prop( $prop, null );
902                                return;
903                        }
904
905                        if ( is_a( $value, 'WC_DateTime' ) ) {
906                                $datetime = $value;
907                        } elseif ( is_numeric( $value ) ) {
908                                // Timestamps are handled as UTC timestamps in all cases.
909                                $datetime = new WC_DateTime( "@{$value}", new DateTimeZone( 'UTC' ) );
910                        } else {
911                                // Strings are defined in local WP timezone. Convert to UTC.
912                                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 ) ) {
913                                        $offset    = ! empty( $date_bits[7] ) ? iso8601_timezone_to_offset( $date_bits[7] ) : wc_timezone_offset();
914                                        $timestamp = gmmktime( $date_bits[4], $date_bits[5], $date_bits[6], $date_bits[2], $date_bits[3], $date_bits[1] ) - $offset;
915                                } else {
916                                        $timestamp = wc_string_to_timestamp( get_gmt_from_date( gmdate( 'Y-m-d H:i:s', wc_string_to_timestamp( $value ) ) ) );
917                                }
918                                $datetime = new WC_DateTime( "@{$timestamp}", new DateTimeZone( 'UTC' ) );
919                        }
920
921                        // Set local timezone or offset.
922                        if ( get_option( 'timezone_string' ) ) {
923                                $datetime->setTimezone( new DateTimeZone( wc_timezone_string() ) );
924                        } else {
925                                $datetime->set_utc_offset( wc_timezone_offset() );
926                        }
927
928                        $this->set_prop( $prop, $datetime );
929                } catch ( Exception $e ) {} // @codingStandardsIgnoreLine.
930        }
931
932        /**
933         * When invalid data is found, throw an exception unless reading from the DB.
934         *
935         * @throws WC_Data_Exception Data Exception.
936         * @since 3.0.0
937         * @param string $code             Error code.
938         * @param string $message          Error message.
939         * @param int    $http_status_code HTTP status code.
940         * @param array  $data             Extra error data.
941         */
942        protected function error( $code, $message, $http_status_code = 400, $data = array() ) {
943                throw new WC_Data_Exception( $code, $message, $http_status_code, $data );
944        }
945}