PluginProbe
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More / 2.2.0
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More v2.2.0
2.3.0 2.2.0 2.1.1 2.1.0 2.0.0 1.10.0 1.9.1 1.9.0 1.2.1 1.2.2 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 All 59 releases
storeengine / includes / classes / abstract-entity.php

abstract-entity.php in StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More 2.2.0, at includes/classes/abstract-entity.php

1,893 lines 50.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace StoreEngine\Classes;
4
5 use Exception;
6 use stdClass;
7 use StoreEngine\Classes\Exceptions\StoreEngineException;
8 use StoreEngine\Classes\Exceptions\StoreEngineNotFoundException;
9 use StoreEngine\Utils\Caching;
10 use StoreEngine\Utils\Formatting;
11 use StoreEngine\Utils\Helper;
12 use Throwable;
13 use WP_Error;
14
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit;
17 }
18
19 #[\AllowDynamicProperties]
20 abstract class AbstractEntity {
21 /**
22 * @var \wpdb
23 */
24 protected $wpdb;
25
26 /**
27 * Table name where the entity data stored.
28 *
29 * @var string
30 */
31 protected string $table;
32
33 /**
34 * Cache group for the entity.
35 * Default to the table name (without db prefix)
36 * used with wp_cache_set_last_changed
37 *
38 * @see wp_cache_set_last_changed
39 * @var ?string
40 */
41 protected ?string $cache_group = null;
42
43 /**
44 * Primary key column name for the entity.
45 *
46 * @var string
47 */
48 protected string $primary_key = 'id';
49
50 /**
51 * Meta key to entity property keys.
52 *
53 * @var array
54 */
55 protected array $meta_key_to_props = [];
56
57 /**
58 * Meta type. This should match up with
59 * the types available at https://developer.wordpress.org/reference/functions/add_metadata/.
60 * WP defines 'post', 'user', 'comment', and 'term'.
61 *
62 * @var string
63 */
64 protected string $meta_type = '';
65
66 /**
67 * ID for this object.
68 *
69 * @var int
70 */
71 protected int $id = 0;
72
73 /**
74 * Core data for this object. Name value pairs (name + default value).
75 *
76 * @var array
77 */
78 protected array $data = [];
79
80 protected array $readable_fields = [];
81
82 protected array $data_format = [];
83
84 /**
85 * Core data changes for this object.
86 *
87 * @var array
88 */
89 protected array $changes = [];
90
91 /**
92 * Extra data for this object. Name value pairs (name + default value).
93 * Used as a standard way for sub classes (like product types) to add
94 * additional information to an inherited class.
95 *
96 * @var array
97 */
98 protected array $extra_data = [];
99
100 /**
101 * If we have already saved our extra data, don't do automatic / default handling.
102 *
103 * @var bool
104 */
105 protected bool $extra_data_saved = false;
106
107 protected bool $read_extra_data_separately = true;
108
109 protected array $updated_props = [];
110
111 protected array $extra_data_format = [];
112
113 /**
114 * Set to _data on construct so we can track and reset data if needed.
115 *
116 * @var array
117 */
118 protected array $default_data = [];
119
120 protected array $default_data_format = [];
121
122 protected ?array $meta_data = null;
123
124 /**
125 * This only needs set if you are using a custom metadata type (for example payment tokens.
126 * This should be the name of the field your table uses for associating meta with objects.
127 * For example, in payment_tokenmeta, this would be payment_token_id.
128 *
129 * @var string
130 */
131 protected string $object_id_field_for_meta = '';
132
133 /**
134 * Data stored in meta keys, but not considered "meta" for an object.
135 *
136 * @var array
137 */
138 protected array $internal_meta_keys = [];
139
140 /**
141 * Meta data which should exist in the DB, even if empty.
142 *
143 * @var array
144 */
145 protected $must_exist_meta_keys = [];
146
147 /**
148 * This is false until the object is read from the DB.
149 *
150 * @var bool
151 */
152 protected bool $object_read = false;
153
154 /**
155 * This is the name of this object type.
156 *
157 * @var string
158 */
159 protected string $object_type = 'data';
160
161 /**
162 * Flag for allowing to trash without delete.
163 *
164 * @var bool
165 */
166 protected bool $allow_trash = false;
167
168 /**
169 * Default constructor.
170 *
171 * @param int|object|array $read ID to load from the DB (optional) or already queried data.
172 *
173 * @throws StoreEngineException
174 */
175 public function __construct( $read = 0 ) {
176 global $wpdb;
177 $this->wpdb = $wpdb;
178
179 $table = str_replace( $wpdb->prefix, '', $this->table );
180 if ( ! $this->cache_group ) {
181 $this->cache_group = $table;
182 }
183
184 $this->table = $wpdb->prefix . $table;
185 $this->data = array_merge( $this->data, $this->extra_data );
186 $this->data_format = array_merge( $this->data_format, $this->extra_data_format );
187 $this->default_data = $this->data;
188 $this->default_data_format = $this->data_format;
189
190 if ( is_numeric( $read ) && $read > 0 ) {
191 $this->set_id( $read );
192 } elseif ( $read instanceof self ) {
193 $this->set_id( $read->get_id() );
194 } elseif ( is_object( $read ) && ( ! empty( $read->ID ) || ! empty( $read->{$this->primary_key} ) ) ) {
195 if ( ! empty( $read->ID ) ) {
196 $this->set_id( $read->ID );
197 } else {
198 $this->set_id( $read->{$this->primary_key} );
199 }
200 } elseif ( is_array( $read ) && ( ! empty( $read['ID'] ) || ! empty( $read[ $this->primary_key ] ) ) ) {
201 if ( ! empty( $read['ID'] ) ) {
202 $this->set_id( $read['ID'] );
203 } else {
204 $this->set_id( $read[ $this->primary_key ] );
205 }
206 } else {
207 $this->set_object_read( true );
208 }
209
210 if ( $this->get_id() > 0 ) {
211 $this->read();
212 }
213 }
214
215 /**
216 * Only store the object ID to avoid serializing the data object instance.
217 *
218 * @return array
219 */
220 public function __sleep() {
221 return [ 'id' ];
222 }
223
224 /**
225 * Re-run the constructor with the object ID.
226 *
227 * If the object no longer exists, remove the ID.
228 */
229 public function __wakeup() {
230 try {
231 $this->__construct( absint( $this->id ) );
232 } catch ( Exception $e ) {
233 $this->set_id( 0 );
234 $this->set_object_read( true );
235 }
236 }
237
238 /**
239 * When the object is cloned, make sure meta is duplicated correctly.
240 */
241 public function __clone() {
242 $this->id = 0;
243 $this->maybe_read_meta_data();
244 if ( ! empty( $this->meta_data ) ) {
245 foreach ( $this->meta_data as $array_key => $meta ) {
246 $this->meta_data[ $array_key ] = clone $meta;
247 if ( ! empty( $meta->id ) ) {
248 $this->meta_data[ $array_key ]->id = null;
249 }
250 }
251 }
252 }
253
254 /**
255 * Returns the unique ID for this object.
256 *
257 * @return int
258 */
259 public function get_id(): int {
260 return $this->id;
261 }
262
263 public function __get( string $key ) {
264 if ( in_array( $key, [ 'id', 'ID', $this->primary_key ], true ) ) {
265 // Compatibility for AbstractCollection.
266 return $this->get_id();
267 }
268
269 $getter = "get_$key";
270
271 if ( is_callable( [ $this, $getter ] ) ) {
272 return $this->{$getter}();
273 }
274
275 return null;
276 }
277
278 /**
279 * Set ID.
280 *
281 * @param int|string $id ID.
282 */
283 public function set_id( $id ) {
284 $this->id = absint( $id );
285 }
286
287 /**
288 * Save should create or update based on object existence.
289 *
290 * @return int|WP_Error
291 */
292 public function save() {
293 try {
294 $create = ! $this->get_id();
295
296 /**
297 * Trigger action before saving to the DB. Allows you to adjust object props before save.
298 *
299 * @param AbstractEntity $this The object being saved.
300 * @param bool $create If the object is being creating/updating.
301 */
302 do_action( 'storeengine/' . $this->object_type . '/before/object_save', $this, $create );
303
304 if ( ! $this->get_id() ) {
305 $this->create();
306 } else {
307 $this->update();
308 }
309
310 /**
311 * Trigger action after saving to the DB.
312 *
313 * @param AbstractEntity $this The object being saved.
314 * @param bool $create If the object is being created/updated.
315 */
316 do_action( 'storeengine/' . $this->object_type . '/after/object_save', $this, $create );
317
318 return $this->get_id();
319 } catch ( StoreEngineException $e ) {
320 return $e->toWpError();
321 }
322 }
323
324 /**
325 * Create DB Record.
326 *
327 * @throws StoreEngineException
328 */
329 public function create() {
330 [ 'data' => $data, 'format' => $format ] = $this->prepare_for_db();
331
332 if ( $this->wpdb->insert( $this->table, $data, $format ) ) {
333 $this->set_id( $this->wpdb->insert_id );
334 $this->save_meta_data();
335 $this->save_item_data();
336 $this->update_object_meta();
337 $this->save_extra_data();
338 $this->apply_changes();
339 $this->clear_cache();
340 }
341
342 if ( $this->wpdb->last_error ) {
343 throw new StoreEngineException( wp_kses_post( $this->wpdb->last_error ), 'db-error-insert-record' );
344 }
345 }
346
347 /**
348 * Get and store terms from a taxonomy.
349 *
350 * @param string $taxonomy Taxonomy name e.g. product_cat.
351 *
352 * @return array of terms
353 */
354 protected function get_terms( string $taxonomy ): array {
355 $terms = get_the_terms( $this->get_id(), $taxonomy );
356 if ( false === $terms || is_wp_error( $terms ) ) {
357 return [];
358 }
359
360 return $terms;
361 }
362
363 /**
364 * Get and store terms ids from a taxonomy.
365 *
366 * @param string $taxonomy Taxonomy name e.g. product_cat.
367 *
368 * @return int[] of terms
369 */
370 protected function get_term_ids( string $taxonomy ): array {
371 $terms = get_the_terms( $this->get_id(), $taxonomy );
372 if ( false === $terms || is_wp_error( $terms ) ) {
373 return [];
374 }
375
376 return wp_list_pluck( $terms, 'term_id' );
377 }
378
379 /**
380 * Table structure is slightly different between meta types, this function will return what we need to know.
381 *
382 * @return array{table:string,object_id_field:string,meta_id_field:string}|bool Array elements: table, object_id_field, meta_id_field
383 */
384 protected function get_db_info() {
385 global $wpdb;
386
387 if ( ! $this->meta_type ) {
388 return false;
389 }
390
391 $meta_id_field = 'meta_id'; // for some reason users calls this umeta_id so we need to track this as well.
392
393 if ( isset( $wpdb->{$this->meta_type . 'meta'} ) ) {
394 $table = $wpdb->{$this->meta_type . 'meta'};
395 } else {
396 $table = $wpdb->prefix;
397 // If we are dealing with a type of metadata that is not a core type, the table should be prefixed.
398 if ( ! in_array( $this->meta_type, [ 'post', 'user', 'comment', 'term' ], true ) ) {
399 $table .= 'storeengine_';
400 }
401
402 $table .= $this->meta_type . '_meta';
403 }
404
405 $object_id_field = $this->meta_type . '_id';
406
407 // Figure out our field names.
408 if ( 'user' === $this->meta_type ) {
409 $meta_id_field = 'umeta_id';
410 $table = $wpdb->usermeta;
411 }
412 if ( 'order' === $this->meta_type ) {
413 $object_id_field = 'order_id';
414 $table = $wpdb->prefix . 'storeengine_orders_meta';
415 }
416
417 if ( ! empty( $this->object_id_field_for_meta ) ) {
418 $object_id_field = $this->object_id_field_for_meta;
419 }
420
421 return [
422 'table' => $table,
423 'object_id_field' => $object_id_field,
424 'meta_id_field' => $meta_id_field,
425 ];
426 }
427
428 /**
429 * Internal meta keys we don't want exposed as part of meta_data. This is in
430 * addition to all data props with _ prefix.
431 *
432 * @param string $key Prefix to be added to meta keys.
433 *
434 * @return string
435 */
436 protected function prefix_key( string $key ): string {
437 return '_' === substr( $key, 0, 1 ) ? $key : '_' . $key;
438 }
439
440 /**
441 * Returns an array of meta for an object.
442 *
443 * @return array
444 */
445 public function read_meta(): ?array {
446 $db_info = $this->get_db_info();
447
448 if ( ! $db_info ) {
449 return null;
450 }
451
452 // phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
453 $raw_meta_data = $this->wpdb->get_results(
454 $this->wpdb->prepare(
455 "SELECT {$db_info['meta_id_field']} as meta_id, meta_key, meta_value
456 FROM {$db_info['table']}
457 WHERE {$db_info['object_id_field']} = %d
458 ORDER BY {$db_info['meta_id_field']}",
459 $this->get_id()
460 )
461 );
462
463 // phpcs:enable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
464
465 return $this->filter_raw_meta_data( $raw_meta_data );
466 }
467
468 /**
469 * Helper method to filter internal meta keys from all meta data rows for the object.
470 *
471 * @param ?array $raw_meta_data Array of std object of meta data to be filtered.
472 *
473 * @return ?array
474 */
475 public function filter_raw_meta_data( ?array $raw_meta_data ): ?array {
476 if ( ! $raw_meta_data ) {
477 return null;
478 }
479
480 $this->internal_meta_keys = array_unique(
481 array_merge(
482 array_map(
483 [
484 $this,
485 'prefix_key',
486 ],
487 $this->get_data_keys()
488 ),
489 $this->internal_meta_keys
490 )
491 );
492 $meta_data = array_filter( $raw_meta_data, [ $this, 'exclude_internal_meta_keys' ] );
493
494 return apply_filters( "storeengine/{$this->meta_type}_read_meta", $meta_data, $this );
495 }
496
497 /**
498 * Callback to remove unwanted meta data.
499 *
500 * @param object $meta Meta object to check if it should be excluded or not.
501 *
502 * @return bool
503 */
504 protected function exclude_internal_meta_keys( object $meta ): bool {
505 return ! in_array( $meta->meta_key, $this->internal_meta_keys, true ) && 0 !== stripos( $meta->meta_key, 'wp_' );
506 }
507
508
509 /**
510 * Callback to remove unwanted meta data.
511 *
512 * @param object $meta Meta object to check if it should be excluded or not.
513 *
514 * @return bool
515 */
516 protected function include_extra_meta_keys( object $meta ): bool {
517 return array_key_exists( $meta->meta_key, $this->meta_key_to_props ) && 0 !== stripos( $meta->meta_key, 'wp_' );
518 }
519
520 /**
521 * Gets a list of props and meta keys that need updated based on change state
522 * or if they are present in the database or not.
523 *
524 * @param array $meta_key_to_props A mapping of meta keys => prop names.
525 * @param string $meta_type The internal WP meta type (post, user, etc).
526 *
527 * @return array A mapping of meta keys => prop names, filtered by ones that should be updated.
528 */
529 protected function get_props_to_update( array $meta_key_to_props ): array {
530 $props_to_update = [];
531 $changed_props = $this->get_changes();
532
533 // Props should be updated if they are a part of the $changed array or don't exist yet.
534 foreach ( $meta_key_to_props as $meta_key => $prop ) {
535 if ( array_key_exists( $prop, $changed_props ) || ! metadata_exists( $this->meta_type, $this->get_id(), $meta_key ) ) {
536 $props_to_update[ $meta_key ] = $prop;
537 }
538 }
539
540 return $props_to_update;
541 }
542
543 /**
544 * Update meta data in, or delete it from, the database.
545 *
546 * Avoids storing meta when it's either an empty string or empty array.
547 * Other empty values such as numeric 0 and null should still be stored.
548 * Data-stores can force meta to exist using `must_exist_meta_keys`.
549 *
550 * Note: WordPress `get_metadata` function returns an empty string when meta data does not exist.
551 *
552 * @param string $meta_key Meta key to update.
553 * @param mixed $meta_value Value to save.
554 *
555 * @return bool True if updated/deleted.
556 */
557 protected function update_or_delete_post_meta( string $meta_key, $meta_value ): bool {
558 if ( in_array( $meta_value, [ [], '' ], true ) && ! in_array( $meta_key, $this->must_exist_meta_keys, true ) ) {
559 $updated = delete_metadata( $this->meta_type, $this->get_id(), $meta_key );
560 } else {
561 $updated = update_metadata( $this->meta_type, $this->get_id(), $meta_key, $meta_value );
562 }
563
564 return (bool) $updated;
565 }
566
567 /**
568 * Read DB Record.
569 *
570 * @param bool $refresh If true read fresh data from DB.
571 * Else try read from cached data.
572 * Default is false.
573 *
574 * @throws StoreEngineException
575 */
576 public function read( bool $refresh = false ) {
577 $this->set_defaults();
578
579 if ( ! $this->get_id() ) {
580 throw new StoreEngineException(
581 sprintf(
582 /* translators: %s: Data object type. */
583 esc_html__( 'ID is not set for %s.', 'storeengine' ),
584 esc_html( $this->object_type )
585 ),
586 'read-error-no-id',
587 null,
588 400
589 );
590 }
591
592 // Get from cache if available.
593 $data = wp_cache_get( $this->get_id(), $this->cache_group );
594
595 if ( false === $data || true === $refresh ) {
596 $this->clear_cache( false );
597 /**
598 * @TODO Move caching inside read_data around the WPDB calls.
599 * As complex object like order returns metadata props from
600 * read_data, and metadata caching handled by WP core.
601 * This will resolve the issue with AbstractCollection::_prime_item_caches method.
602 * @see AbstractCollection::_prime_item_caches
603 */
604 $data = $this->read_data();
605 wp_cache_set( $this->get_id(), $data, $this->cache_group );
606 }
607
608 $this->set_props( $data );
609
610 $this->maybe_read_meta_data();
611 $this->maybe_read_extra_data();
612
613 $this->set_object_read( true );
614
615 /**
616 * Fires when an object is read into memory.
617 *
618 * @param int $id The Object ID.
619 * @param self $this Object instance.
620 */
621 do_action( "storeengine/$this->object_type/read", $this->get_id(), $this );
622 }
623
624 /**
625 * Read DB Record.
626 *
627 * @return array
628 * @throws StoreEngineException
629 */
630 protected function read_data(): array {
631 $columns = $this->readable_fields ? implode( ',', $this->readable_fields ) : '*';
632
633 // phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
634 $data = $this->wpdb->get_row( $this->wpdb->prepare( "SELECT $columns FROM $this->table WHERE $this->primary_key = %d LIMIT 1;", $this->get_id() ), ARRAY_A );
635 // phpcs:enable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
636
637 if ( ! $data ) {
638 if ( $this->wpdb->last_error ) {
639 /* translators: %s: Error message. */
640 throw new StoreEngineException( sprintf( esc_html__( 'Error reading data from database. Error: %s', 'storeengine' ), esc_html( $this->wpdb->last_error ) ), 'db-read-error', [ 'id' => $this->get_id() ], 500 ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
641 }
642
643 /* translators: %s: Data object type. */
644 throw new StoreEngineNotFoundException( sprintf( esc_html__( 'Data (%s) not found.', 'storeengine' ), esc_html( $this->object_type ) ), [ 'id' => $this->get_id() ] ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
645 }
646
647 return $data;
648 }
649
650 /**
651 * Update DB Record.
652 *
653 * @throws StoreEngineException
654 */
655 public function update() {
656 if ( ! $this->get_id() ) {
657 return;
658 }
659
660 $this->save_meta_data();
661 $this->save_item_data();
662 $this->update_object_meta();
663 $this->save_extra_data();
664
665 [ 'data' => $data, 'format' => $format ] = $this->prepare_for_db( 'update' );
666
667 $this->wpdb->update( $this->table, $data, [ $this->primary_key => $this->get_id() ], $format, [ '%d' ] );
668
669 if ( $this->wpdb->last_error ) {
670 throw new StoreEngineException( wp_kses_post( $this->wpdb->last_error ), 'db-error-update-record' );
671 }
672
673 $this->apply_changes();
674 $this->clear_cache();
675
676 /**
677 * Fires immediately after an object updated into database.
678 *
679 * @param int $id The Object ID.
680 * @param self $this Object instance.
681 */
682 do_action( "storeengine/$this->object_type/updated", $this->get_id(), $this );
683 }
684
685 protected function save_item_data() {
686 foreach ( $this->get_props_to_update( $this->meta_key_to_props ) as $meta_key => $prop ) {
687 update_metadata( $this->meta_type, $this->get_id(), $meta_key, $this->{"get_$prop"}( 'edit' ) );
688 }
689 }
690
691 /**
692 * @param bool $force_delete
693 *
694 * @return bool
695 * @throws StoreEngineException
696 */
697 public function delete( bool $force_delete = false ): bool {
698 if ( ! $this->get_id() ) {
699 return false;
700 }
701
702 if ( ! $force_delete && $this->is_trashable() ) {
703 return $this->trash();
704 }
705
706 // Keep a reference before delete and set to zero.
707 $id = $this->get_id();
708 if ( $this->wpdb->delete( $this->table, [ $this->primary_key => $this->id ], [ '%d' ] ) ) {
709 $this->delete_object_metas();
710 $this->clear_cache();
711 $this->set_id( 0 );
712
713 if ( $this->wpdb->last_error ) {
714 throw new StoreEngineException( wp_kses_post( $this->wpdb->last_error ), 'db-error-delete-record' );
715 }
716
717 /**
718 * Fires immediately after an object gets deleted from database.
719 *
720 * @param int $id The Object ID.
721 * @param self $this Object instance.
722 * @param bool $force_delete The Object ID.
723 */
724 do_action( "storeengine/$this->object_type/deleted", $id, $this, $force_delete );
725
726 return true;
727 } else {
728 return false;
729 }
730 }
731
732 public function trash(): bool {
733 if ( $this->is_trashable() ) {
734 $this->add_meta_data( '_trash_status', $this->get_status( 'edit' ) );
735 $this->set_status( 'trash' );
736 $this->add_meta_data( '_trash_time', time() );
737 $this->save();
738
739 return 'trash' === $this->get_status( 'edit' );
740 }
741
742 return false;
743 }
744
745 public function untrash(): bool {
746 if ( $this->is_trashable() ) {
747 $previous_status = $this->get_meta( '_trash_status' );
748 $this->set_status( $previous_status ? $previous_status : 'draft' );
749 $this->delete_meta_data( '_trash_status' );
750 $this->delete_meta_data( '_trash_time' );
751 $this->save();
752
753 do_action( "storeengine/$this->object_type/untrashed", $this, $previous_status );
754
755 return 'trash' !== $this->get_status( 'edit' );
756 }
757
758 return false;
759 }
760
761 public function is_trashable(): bool {
762 return $this->allow_trash && is_callable( [ $this, 'get_status' ] ) && is_callable( [ $this, 'set_status' ] );
763 }
764
765 protected function get_object_meta_ids(): array {
766 $db_info = $this->get_db_info();
767
768 if ( ! $db_info ) {
769 return [];
770 }
771
772 // phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
773 $object_meta_ids = $this->wpdb->get_col(
774 $this->wpdb->prepare(
775 "SELECT {$db_info['meta_id_field']} as meta_id
776 FROM {$db_info['table']}
777 WHERE {$db_info['object_id_field']} = %d;",
778 $this->get_id()
779 )
780 );
781
782 // phpcs:enable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
783
784 return array_map( 'absint', $object_meta_ids );
785 }
786
787 protected function delete_object_metas() {
788 foreach ( $this->get_object_meta_ids() as $mid ) {
789 delete_metadata_by_mid( $this->meta_type, $mid );
790 }
791 }
792
793 protected function maybe_read_meta_data() {
794 if ( is_null( $this->meta_data ) ) {
795 $this->read_meta_data();
796 }
797 }
798
799 public function read_meta_data( $force_read = false ) {
800 // Maybe not necessary to set here.
801 $this->meta_data = [];
802
803 if ( ! $this->get_id() ) {
804 return;
805 }
806
807 $cache_loaded = false;
808 $cached_meta = [];
809
810 // Prefix by group allows invalidation by group until https://core.trac.wordpress.org/ticket/4476 is implemented.
811 if ( ! $force_read ) {
812 if ( ! empty( $this->cache_group ) ) {
813 $cached_meta = wp_cache_get( $this->get_meta_cache_key(), $this->cache_group );
814 $cache_loaded = is_array( $cached_meta );
815 }
816 }
817
818 // We filter the raw meta data again when loading from cache, in case we cached in an earlier version where filter conditions were different.
819 $raw_meta_data = $cache_loaded ? $this->filter_raw_meta_data( $cached_meta ) : $this->read_meta();
820
821 if ( is_array( $raw_meta_data ) ) {
822 $this->init_meta_data( $raw_meta_data );
823 if ( ! $cache_loaded && ! empty( $this->cache_group ) ) {
824 wp_cache_set( $this->get_meta_cache_key(), $raw_meta_data, $this->cache_group );
825 }
826 }
827 }
828
829 /**
830 * 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.
831 *
832 * @return string
833 */
834 public function get_meta_cache_key() {
835 if ( ! $this->get_id() ) {
836 _doing_it_wrong( 'get_meta_cache_key', 'ID needs to be set before fetching a cache key.', '1.0.0' );
837
838 return false;
839 }
840
841 return $this->generate_meta_cache_key();
842 }
843
844 /**
845 * Generate cache key from id and group.
846 *
847 * @return string Meta cache key.
848 */
849 public function generate_meta_cache_key(): string {
850 return Caching::get_cache_prefix( $this->cache_group ) . Caching::get_cache_prefix( 'object_' . $this->get_id() ) . 'object_meta_' . $this->get_id();
851 }
852
853 protected function maybe_read_extra_data() {
854 if ( $this->read_extra_data_separately ) {
855 $this->read_extra_data();
856 }
857 }
858
859 protected function read_extra_data() {
860 foreach ( $this->get_extra_data_keys() as $key ) {
861 $function = 'set_' . $key;
862 if ( is_callable( [ $this, $function ] ) ) {
863 $this->{$function}( $this->get_metadata( $key ) );
864 }
865 }
866 }
867
868 /**
869 * Saves extra token data as meta.
870 *
871 * @param bool $force By default, only changed props are updated. When this param is true all props are updated.
872 *
873 * @return array List of updated props.
874 */
875 protected function save_extra_data( bool $force = false ): array {
876 if ( $this->extra_data_saved ) {
877 return [];
878 }
879
880 $updated_props = [];
881 $extra_data_keys = $this->get_extra_data_keys();
882 $meta_key_to_props = ! empty( $extra_data_keys ) ? array_combine( $extra_data_keys, $extra_data_keys ) : [];
883 $props_to_update = $force ? $meta_key_to_props : $this->get_props_to_update( $meta_key_to_props );
884
885 foreach ( $extra_data_keys as $key ) {
886 if ( ! array_key_exists( $key, $props_to_update ) ) {
887 continue;
888 }
889
890 $function = 'get_' . $key;
891
892 if ( is_callable( [ $this, $function ] ) ) {
893 $value = $this->{$function}( 'edit' );
894
895 if ( $value && is_a( $value, StoreengineDatetime::class ) ) {
896 $value = $this->prepare_date_for_db( $value );
897 }
898
899 if ( update_metadata( $this->meta_type, $this->get_id(), $key, $value ) ) {
900 $updated_props[] = $key;
901 }
902 }
903 }
904
905 return $updated_props;
906 }
907
908 /**
909 * Helper function to initialize metadata entries from filtered raw meta data.
910 *
911 * @param array $filtered_meta_data Filtered metadata fetched from DB.
912 */
913 public function init_meta_data( array $filtered_meta_data = [] ) {
914 $this->meta_data = [];
915 foreach ( $filtered_meta_data as $meta ) {
916 $this->meta_data[] = new MetaData( [
917 'id' => (int) $meta->meta_id,
918 'key' => $meta->meta_key,
919 'value' => maybe_unserialize( $meta->meta_value ),
920 ] );
921 }
922 }
923
924 public function save_meta_data() {
925 if ( is_null( $this->meta_data ) ) {
926 return;
927 }
928
929 foreach ( $this->meta_data as $array_key => $meta ) {
930 if ( ! is_object( $meta ) ) {
931 continue;
932 }
933 if ( is_null( $meta->value ) ) {
934 if ( ! empty( $meta->id ) ) {
935 $this->delete_meta( $meta );
936 /**
937 * Fires immediately after deleting metadata.
938 *
939 * @param int $meta_id ID of deleted metadata entry.
940 * @param int $object_id Object ID.
941 * @param string $meta_key Metadata key.
942 * @param mixed $meta_value Metadata value (will be empty for delete).
943 */
944 do_action( "storeengine/deleted_{$this->object_type}_meta", $meta->id, $this->get_id(), $meta->key, $meta->value );
945
946 unset( $this->meta_data[ $array_key ] );
947 }
948 } elseif ( empty( $meta->id ) ) {
949 $meta->id = $this->add_meta( $meta );
950 /**
951 * Fires immediately after adding metadata.
952 *
953 * @param int $meta_id ID of added metadata entry.
954 * @param int $object_id Object ID.
955 * @param string $meta_key Metadata key.
956 * @param mixed $meta_value Metadata value.
957 */
958 do_action( "storeengine/added_{$this->object_type}_meta", $meta->id, $this->get_id(), $meta->key, $meta->value );
959
960 $meta->apply_changes();
961 } else {
962 if ( $meta->get_changes() ) {
963 $this->update_meta( $meta );
964 /**
965 * Fires immediately after updating metadata.
966 *
967 * @param int $meta_id ID of updated metadata entry.
968 * @param int $object_id Object ID.
969 * @param string $meta_key Metadata key.
970 * @param mixed $meta_value Metadata value.
971 */
972 do_action( "storeengine/updated_{$this->object_type}_meta", $meta->id, $this->get_id(), $meta->key, $meta->value );
973
974 $meta->apply_changes();
975 }
976 }
977 }
978
979 if ( ! empty( $this->cache_group ) ) {
980 wp_cache_delete( self::get_meta_cache_key(), $this->cache_group );
981 }
982 }
983
984 /**
985 * Deletes meta based on meta ID.
986 *
987 * @param stdClass|MetaData $meta (containing at least ->id).
988 *
989 * @return bool
990 */
991 public function delete_meta( $meta ) {
992 return delete_metadata_by_mid( $this->meta_type, $meta->id );
993 }
994
995 /**
996 * Add new piece of meta.
997 *
998 * @param stdClass|MetaData $meta (containing ->key and ->value).
999 *
1000 * @return int|false meta ID
1001 */
1002 public function add_meta( $meta ) {
1003 if ( ! is_string( $meta->key ) ) {
1004 return false;
1005 }
1006
1007 $value = is_string( $meta->value ) ? wp_slash( $meta->value ) : $meta->value;
1008
1009 if ( $value && is_a( $value, StoreengineDatetime::class ) ) {
1010 $value = $this->prepare_date_for_db( $value );
1011 }
1012
1013 return add_metadata( $this->meta_type, $this->get_id(), wp_slash( $meta->key ), $value, false );
1014 }
1015
1016 /**
1017 * Update meta.
1018 *
1019 * @param stdClass|MetaData $meta (containing ->id, ->key and ->value).
1020 *
1021 * @return bool
1022 */
1023 public function update_meta( $meta ): bool {
1024 if ( ! $meta->key && ! $meta->id ) {
1025 return false;
1026 }
1027
1028 $value = is_string( $meta->value ) ? wp_slash( $meta->value ) : $meta->value;
1029
1030 if ( $value && is_a( $value, StoreengineDatetime::class ) ) {
1031 $value = $this->prepare_date_for_db( $value );
1032 }
1033
1034 return update_metadata_by_mid( $this->meta_type, $meta->id, $value, $meta->key );
1035 }
1036
1037
1038 protected function read_object_meta() {
1039 if ( ! $this->meta_type || ! $this->get_id() || empty( $this->meta_key_to_props ) ) {
1040 return;
1041 }
1042
1043 $meta_data = $this->get_metadata( '', false );
1044 $set_props = [];
1045 foreach ( $this->meta_key_to_props as $meta_key => $prop ) {
1046 $meta_value = $meta_data[ $meta_key ][0] ?? null;
1047 $set_props[ $prop ] = maybe_unserialize( $meta_value ); // get_post_meta only unserializes single values.
1048 }
1049
1050 $this->set_props( $set_props );
1051 }
1052
1053 protected function update_object_meta( $force = false ) {
1054 // Make sure to take extra data (like product url or text for external products) into account.
1055 $extra_data_keys = $this->get_extra_data_keys();
1056 $props_to_update = $force ? $this->meta_key_to_props : $this->get_props_to_update( $this->meta_key_to_props );
1057
1058 foreach ( $props_to_update as $meta_key => $prop ) {
1059 $value = $this->{"get_$prop"}( 'edit' );
1060 $value = is_string( $value ) ? wp_slash( $value ) : $value;
1061
1062 if ( $value && is_a( $value, StoreengineDatetime::class ) ) {
1063 $value = $this->prepare_date_for_db( $value );
1064 }
1065
1066 if ( $this->update_or_delete_post_meta( $meta_key, $value ) ) {
1067 $this->updated_props[] = $prop;
1068 }
1069 }
1070
1071 // Update extra data associated with the product like button text or product URL for external products.
1072 if ( ! $this->extra_data_saved ) {
1073 foreach ( $extra_data_keys as $key ) {
1074 $meta_key = '_' . $key;
1075 $function = 'get_' . $key;
1076 if ( ! array_key_exists( $meta_key, $props_to_update ) ) {
1077 continue;
1078 }
1079 if ( is_callable( array( $this, $function ) ) ) {
1080 $value = $this->{$function}( 'edit' );
1081 $value = is_string( $value ) ? wp_slash( $value ) : $value;
1082 if ( $value && is_a( $value, StoreengineDatetime::class ) ) {
1083 $value = $this->prepare_date_for_db( $value );
1084 }
1085
1086 if ( $this->update_or_delete_post_meta( $meta_key, $value ) ) {
1087 $this->updated_props[] = $key;
1088 }
1089 }
1090 }
1091
1092 $this->extra_data_saved = true;
1093 }
1094 }
1095
1096 /**
1097 * Methods is protected and final to keep this as an internal API.
1098 *
1099 * @param string $context
1100 *
1101 * @return array
1102 * @internal
1103 */
1104 protected function prepare_for_db( string $context = 'create' ): array {
1105 $data = [];
1106 $format = [];
1107
1108 $now = current_time( 'mysql', 1 );
1109
1110 if ( 'create' === $context ) {
1111 if ( array_key_exists( 'created_at', $this->data ) ) {
1112 $this->set_date_prop( 'created_at', $now );
1113 }
1114
1115 if ( array_key_exists( 'date_created', $this->data ) ) {
1116 $this->set_date_prop( 'date_created', $now );
1117 }
1118
1119 if ( array_key_exists( 'date_created_gmt', $this->data ) ) {
1120 $this->set_date_prop( 'date_created_gmt', $now );
1121 }
1122 }
1123
1124 // Always set.
1125 if ( array_key_exists( 'updated_at', $this->data ) ) {
1126 $this->set_date_prop( 'updated_at', $now );
1127 }
1128
1129 if ( array_key_exists( 'date_updated', $this->data ) ) {
1130 $this->set_date_prop( 'date_updated', $now );
1131 }
1132
1133 if ( array_key_exists( 'date_updated_gmt', $this->data ) ) {
1134 $this->set_date_prop( 'date_updated_gmt', $now );
1135 }
1136
1137 // Should just get data from change when available and check context?
1138 $raw_data = $this->get_changes();
1139
1140 if ( empty( $raw_data ) && 'create' === $context ) {
1141 $raw_data = $this->get_data(); // get defaults.
1142 }
1143
1144 $extra_data_keys = $this->get_extra_data_keys();
1145
1146 foreach ( array_keys( $this->data ) as $key ) {
1147 if ( 'update' === $context && ( str_contains( 'date_created', $key ) || str_contains( 'created_at', $key ) ) ) {
1148 continue;
1149 }
1150
1151 if ( in_array( $key, $extra_data_keys, true ) ) {
1152 continue;
1153 }
1154
1155 $getter = 'get_' . $key;
1156 if ( method_exists( $this, $getter ) ) {
1157 $value = $this->{$getter}( 'edit' );
1158 } else {
1159 $value = $raw_data[ $key ] ?? null;
1160 }
1161
1162 if ( $value && is_a( $value, StoreengineDatetime::class ) ) {
1163 $value = $this->prepare_date_for_db( $value );
1164 }
1165
1166 if ( is_array( $value ) || is_object( $value ) ) {
1167 $value = maybe_serialize( $value );
1168 }
1169
1170 if ( is_bool( $value ) ) {
1171 $value = (int) $value;
1172 }
1173
1174 $data[ $key ] = $value;
1175 $format[] = $this->predict_format( $key, $value );
1176 }
1177
1178 // Not using any filter for handling format.
1179 // If necessary update format for column in $wpdb::$field_types.
1180
1181 return [
1182 'data' => apply_filters( 'storeengine/' . $this->object_type . '/db/' . $context, $data, $this ),
1183 'format' => $format,
1184 ];
1185 }
1186
1187 /**
1188 * Predict data format for preparing database query with $wpdb->prepare()
1189 *
1190 * > This should only be called at SQL-prepare time, so the function
1191 * can detect the correct format based on the final value. Calling
1192 * it earlier may lead to incorrect format prediction.
1193 *
1194 * @param string $key
1195 * @param mixed $value
1196 *
1197 * @return string
1198 * @internal Methods is protected and final to keep this as an internal API.
1199 */
1200 final protected function predict_format( string $key, $value ): string {
1201 if ( ! empty( $this->data_format[ $key ] ) ) {
1202 return $this->data_format[ $key ];
1203 }
1204
1205 if ( is_numeric( $value ) || is_bool( $value ) ) {
1206 // @NOTE WordPress converts %f to a decimal string with 6 digits of precision
1207 // Which converts 1.05 into 1.050000 in string while preparing sql.
1208 return 'double' === gettype( $value ) || str_contains( (string) $value, '.' ) ? '%s' : '%d';
1209 } else {
1210 // @NOTE Treating array or any other data type as string as db can't store array.
1211 // As it will get converted into json or serialized string.
1212 return '%s';
1213 }
1214 }
1215
1216 public function prepare_date_for_db( $value, string $key = '' ) {
1217 if ( $value && is_a( $value, StoreengineDatetime::class ) ) {
1218 $value = $value->format( 'Y-m-d H:i:s' );
1219 }
1220
1221 return $value;
1222 }
1223
1224 /**
1225 * Set all props to default values.
1226 */
1227 public function set_defaults() {
1228 $this->data = $this->default_data;
1229 $this->data_format = $this->default_data_format;
1230 $this->changes = [];
1231 $this->set_object_read( false );
1232 }
1233
1234 /**
1235 * Set object read property.
1236 *
1237 * @param bool $read Should read?.
1238 */
1239 public function set_object_read( bool $read = true ): bool {
1240 $previous = $this->object_read;
1241 $this->object_read = $read;
1242
1243 return $previous;
1244 }
1245
1246 /**
1247 * Get object read property.
1248 *
1249 * @return bool
1250 */
1251 public function get_object_read(): bool {
1252 return $this->object_read;
1253 }
1254
1255 /**
1256 * Set a collection of props in one go, collect any errors, and return the result.
1257 * Only sets using public methods.
1258 *
1259 * @param array|stdClass $props Key value pairs to set. Key is the prop and should map to a setter function name.
1260 * @param string $context In what context to run this.
1261 *
1262 * @return bool|WP_Error
1263 */
1264 public function set_props( $props, string $context = 'set' ) {
1265 $errors = false;
1266
1267 foreach ( $props as $prop => $value ) {
1268 if ( $prop === $this->primary_key ) {
1269 $this->set_id( $value );
1270 continue;
1271 }
1272
1273 try {
1274 if ( 'created_via' !== $prop && $value && ( str_contains( $prop, 'date' ) || str_contains( $prop, 'created' ) || str_contains( $prop, 'modified' ) ) ) {
1275 if ( Formatting::is_datetime( $value ) ) {
1276 $this->set_date_prop( $prop, $value );
1277 continue;
1278 }
1279 }
1280
1281 $setter = "set_$prop";
1282
1283 if ( is_callable( [ $this, $setter ] ) ) {
1284 $this->{$setter}( $value );
1285 } else {
1286 // Allow setting props declared directly.
1287 $this->set_prop( $prop, $value );
1288 }
1289 } catch ( StoreEngineException $e ) {
1290 if ( ! $errors ) {
1291 $errors = new WP_Error();
1292 }
1293
1294 if ( ! $e->get_data( 'property' ) ) {
1295 $e->add_data( 'property', $prop );
1296 }
1297
1298 if ( ! $e->get_data( 'value' ) ) {
1299 $e->add_data( 'value', $value );
1300 }
1301
1302 $errors->merge_from( $e->toWpError() );
1303 }
1304 }
1305
1306 return $errors && $errors->has_errors() ? $errors : true;
1307 }
1308
1309 public function get_props( array $props, string $context = 'view' ): array {
1310 $output = [];
1311
1312 foreach ( $props as $prop ) {
1313 $getter = "get_$prop";
1314
1315 if ( is_callable( [ $this, $getter ] ) ) {
1316 $value = $this->{$getter}( $context );
1317 } else {
1318 // Allow getting props declared directly.
1319 $value = $this->get_prop( $prop, $context );
1320 }
1321
1322 if ( $value && is_a( $value, StoreengineDatetime::class ) ) {
1323 $value = $this->prepare_date_for_db( $value );
1324 }
1325
1326 $output[ $prop ] = $value;
1327 }
1328
1329 return $output;
1330 }
1331
1332 /**
1333 * Sets a date prop whilst handling formatting and datetime objects.
1334 *
1335 * @param string $prop Name of prop to set.
1336 * @param string|int $value Value of the prop.
1337 */
1338 protected function set_date_prop( string $prop, $value ) {
1339 try {
1340 if ( empty( $value ) || '0000-00-00 00:00:00' === $value ) {
1341 $this->set_prop( $prop, null );
1342
1343 return;
1344 }
1345
1346 $this->set_prop( $prop, Formatting::string_to_datetime( $value ) );
1347 } catch ( Exception $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
1348 }
1349 }
1350
1351 /**
1352 * Sets a prop for a setter method.
1353 *
1354 * This stores changes in a special array so we can track what needs saving
1355 * the DB later.
1356 *
1357 * @param string $prop Name of prop to set.
1358 * @param mixed $value Value of the prop.
1359 */
1360 protected function set_prop( string $prop, $value ) {
1361 if ( array_key_exists( $prop, $this->data ) ) {
1362 if ( true === $this->object_read ) {
1363 if ( $value !== $this->data[ $prop ] || array_key_exists( $prop, $this->changes ) ) {
1364 $this->changes[ $prop ] = $value;
1365 }
1366 } else {
1367 $this->data[ $prop ] = $value;
1368 }
1369 }
1370 }
1371
1372 /**
1373 * Get Object type. Overridden by child classes.
1374 *
1375 * @return string
1376 */
1377 public function get_type(): string {
1378 return $this->object_type;
1379 }
1380
1381 public function get_object_type(): string {
1382 return $this->object_type;
1383 }
1384
1385 /**
1386 * Return the entity status.
1387 *
1388 * @param string $context View or edit context.
1389 *
1390 * @return string
1391 */
1392 public function get_status( string $context = 'view' ): ?string {
1393 return $this->get_prop( 'status', $context );
1394 }
1395
1396 /**
1397 * Return data.
1398 *
1399 * @return array
1400 */
1401 public function get_data(): array {
1402 return $this->data;
1403 }
1404
1405 /**
1406 * Returns array of expected data keys for this object.
1407 *
1408 * @return array
1409 */
1410 public function get_data_keys(): array {
1411 return array_keys( $this->data );
1412 }
1413
1414 /**
1415 * Returns all "extra" data keys for an object (for sub objects like product types).
1416 *
1417 * @return array
1418 */
1419 public function get_extra_data_keys(): array {
1420 return array_keys( $this->extra_data );
1421 }
1422
1423 /**
1424 * Filter null meta values from array.
1425 *
1426 * @param mixed $meta Meta value to check.
1427 *
1428 * @return bool
1429 */
1430 protected function filter_null_meta( $meta ): bool {
1431 return ! is_null( $meta->value );
1432 }
1433
1434 /**
1435 * Get All Meta-Data.
1436 *
1437 * @return MetaData[] of objects.
1438 */
1439 public function get_meta_data(): array {
1440 $this->maybe_read_meta_data();
1441
1442 return array_values( array_filter( $this->meta_data, [ $this, 'filter_null_meta' ] ) );
1443 }
1444
1445 /**
1446 * Return list of internal meta keys.
1447 *
1448 * @return array
1449 */
1450 public function get_internal_meta_keys(): array {
1451 return $this->internal_meta_keys;
1452 }
1453
1454 /**
1455 * Check if the key is an internal one.
1456 *
1457 * @param ?string $key Key to check.
1458 *
1459 * @return bool true if it's an internal key, false otherwise
1460 */
1461 protected function is_internal_meta_key( ?string $key ): bool {
1462 $internal_meta_key = ! empty( $key ) && in_array( $key, $this->get_internal_meta_keys(), true );
1463
1464 if ( ! $internal_meta_key ) {
1465 return false;
1466 }
1467
1468 $has_setter_or_getter = is_callable( [ $this, 'set_' . ltrim( $key, '_' ) ] ) || is_callable( [
1469 $this,
1470 'get_' . ltrim( $key, '_' ),
1471 ] );
1472
1473 if ( ! $has_setter_or_getter ) {
1474 return false;
1475 }
1476
1477
1478 /* translators: %s: $key Key to check */
1479 _doing_it_wrong( __FUNCTION__, sprintf( esc_html__( 'Generic add/update/get meta methods should not be used for internal meta data, including "%s". Use getters and setters.', 'storeengine' ), esc_html( $key ) ), '1.0.0' );
1480
1481 return true;
1482 }
1483
1484 /**
1485 * Get Metadata by Key.
1486 *
1487 * @param string $key Meta Key.
1488 * @param bool $single return first found meta with key, or all with $key.
1489 * @param string $context What the value is for. Valid values are view and edit.
1490 *
1491 * @return mixed
1492 */
1493 public function get_meta( string $key = '', bool $single = true, string $context = 'view' ) {
1494 if ( $this->is_internal_meta_key( $key ) ) {
1495 $function = 'get_' . ltrim( $key, '_' );
1496
1497 if ( is_callable( array( $this, $function ) ) ) {
1498 return $this->{$function}();
1499 }
1500 }
1501
1502 $this->maybe_read_meta_data();
1503 $meta_data = $this->get_meta_data();
1504 $array_keys = array_keys( wp_list_pluck( $meta_data, 'key' ), $key, true );
1505 $value = $single ? '' : [];
1506
1507 if ( ! empty( $array_keys ) ) {
1508 // 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()).
1509 if ( $single ) {
1510 $value = $meta_data[ current( $array_keys ) ]->value;
1511 } else {
1512 $value = array_intersect_key( $meta_data, array_flip( $array_keys ) );
1513 }
1514 }
1515
1516 if ( 'view' === $context ) {
1517 /**
1518 * @ignore Ignore from Hook parser.
1519 */
1520 $value = apply_filters( $this->get_hook_prefix( $key ), $value, $this ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
1521 }
1522
1523 return $value;
1524 }
1525
1526 public function get_meta_values( $key = '', $context = 'view' ) {
1527 /** @var MetaData[] $data */
1528 $data = $this->get_meta( $key, false, 'read' );
1529 if ( empty( $data ) ) {
1530 return [];
1531 }
1532
1533 $values = array_values( wp_list_pluck( $data, 'value' ) );
1534
1535 if ( 'view' === $context ) {
1536 /**
1537 * @ignore Ignore from Hook parser.
1538 */
1539 $values = apply_filters( $this->get_hook_prefix( $key ), $values, $this ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
1540 }
1541
1542 return $values;
1543 }
1544
1545 /**
1546 * Use get_meta() method for reading metadata value with context.
1547 * This is for internal usage, intended to be used with read_data()
1548 *
1549 * @param string $key
1550 * @param bool $single
1551 *
1552 * @return string|array|false|mixed
1553 * @see read_data
1554 * @see get_meta
1555 */
1556 protected function get_metadata( string $key = '', bool $single = true ) {
1557 return get_metadata( $this->meta_type, $this->get_id(), $key, $single );
1558 }
1559
1560 /**
1561 * See if meta data exists, since get_meta always returns a '' or array().
1562 *
1563 * @param string $key Meta Key.
1564 * @param bool $strict
1565 *
1566 * @return boolean
1567 */
1568 public function meta_exists( string $key = '', bool $strict = true ): bool {
1569 $this->maybe_read_meta_data();
1570 $array_keys = wp_list_pluck( $this->get_meta_data(), 'key' );
1571
1572 return in_array( $key, $array_keys, $strict ); // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
1573 }
1574
1575 /**
1576 * Set all meta data from array.
1577 *
1578 * @param array $data Key/Value pairs.
1579 */
1580 public function set_meta_data( $data ) {
1581 if ( ! empty( $data ) && is_array( $data ) ) {
1582 $this->maybe_read_meta_data();
1583 foreach ( $data as $meta ) {
1584 $meta = (array) $meta;
1585 if ( isset( $meta['key'], $meta['value'], $meta['id'] ) ) {
1586 $this->meta_data[] = new MetaData( [
1587 'id' => $meta['id'],
1588 'key' => $meta['key'],
1589 'value' => $meta['value'],
1590 ] );
1591 }
1592 }
1593 }
1594 }
1595
1596 /**
1597 * Add meta data.
1598 *
1599 * @param string $key Meta key.
1600 * @param string|array|mixed $value Meta value.
1601 * @param bool $unique Should this be a unique key?.
1602 */
1603 public function add_meta_data( string $key, $value, bool $unique = false ) {
1604 if ( ! $key ) {
1605 return;
1606 }
1607 if ( $this->is_internal_meta_key( $key ) ) {
1608 $function = 'set_' . ltrim( $key, '_' );
1609
1610 if ( is_callable( array( $this, $function ) ) ) {
1611 $this->{$function}( $value );
1612 }
1613 }
1614
1615 $this->maybe_read_meta_data();
1616
1617 if ( $unique ) {
1618 $this->delete_meta_data( $key );
1619 }
1620
1621 $this->meta_data[] = new MetaData( [
1622 'key' => $key,
1623 'value' => $value,
1624 ] );
1625 }
1626
1627 /**
1628 * Update meta data by key or ID, if provided.
1629 *
1630 * @param string $key Meta key.
1631 * @param mixed $value Meta value.
1632 * @param int $meta_id Meta ID.
1633 */
1634 public function update_meta_data( string $key, $value, int $meta_id = 0 ) {
1635 if ( $this->is_internal_meta_key( $key ) ) {
1636 $function = 'set_' . ltrim( $key, '_' );
1637
1638 if ( is_callable( array( $this, $function ) ) ) {
1639 $this->{$function}( $value );
1640
1641 return;
1642 }
1643 }
1644
1645 $this->maybe_read_meta_data();
1646
1647 $array_key = false;
1648
1649 if ( $meta_id ) {
1650 $array_keys = array_keys( wp_list_pluck( $this->meta_data, 'id' ), $meta_id, true );
1651 $array_key = $array_keys ? current( $array_keys ) : false;
1652 } else {
1653 // Find matches by key.
1654 $matches = [];
1655 foreach ( $this->meta_data as $meta_data_array_key => $meta ) {
1656 if ( $meta->key === $key ) {
1657 $matches[] = $meta_data_array_key;
1658 }
1659 }
1660
1661 if ( ! empty( $matches ) ) {
1662 // Set matches to null so only one key gets the new value.
1663 foreach ( $matches as $meta_data_array_key ) {
1664 $this->meta_data[ $meta_data_array_key ]->value = null;
1665 }
1666 $array_key = current( $matches );
1667 }
1668 }
1669
1670 if ( false !== $array_key ) {
1671 $meta = $this->meta_data[ $array_key ];
1672 $meta->key = $key;
1673 $meta->value = $value;
1674 }
1675
1676 $this->add_meta_data( $key, $value, true );
1677 }
1678
1679 /**
1680 * Delete meta data.
1681 *
1682 * @param string $key Meta key.
1683 */
1684 public function delete_meta_data( string $key ) {
1685 $this->maybe_read_meta_data();
1686 $array_keys = array_keys( wp_list_pluck( $this->meta_data, 'key' ), $key, true );
1687
1688 if ( $array_keys ) {
1689 foreach ( $array_keys as $array_key ) {
1690 $this->meta_data[ $array_key ]->value = null;
1691 }
1692 }
1693 }
1694
1695 /**
1696 * Delete meta data with a matching value.
1697 *
1698 * @param string $key Meta key.
1699 * @param mixed $value Meta value. Entries will only be removed that match the value.
1700 */
1701 public function delete_meta_data_value( $key, $value ) {
1702 $this->maybe_read_meta_data();
1703 $array_keys = array_keys( wp_list_pluck( $this->meta_data, 'key' ), $key, true );
1704
1705 if ( $array_keys ) {
1706 foreach ( $array_keys as $array_key ) {
1707 if ( $value === $this->meta_data[ $array_key ]->value ) {
1708 $this->meta_data[ $array_key ]->value = null;
1709 }
1710 }
1711 }
1712 }
1713
1714 /**
1715 * Delete meta data.
1716 *
1717 * @param int $mid Meta ID.
1718 */
1719 public function delete_meta_data_by_mid( $mid ) {
1720 $this->maybe_read_meta_data();
1721 $array_keys = array_keys( wp_list_pluck( $this->meta_data, 'id' ), (int) $mid, true );
1722
1723 if ( $array_keys ) {
1724 foreach ( $array_keys as $array_key ) {
1725 $this->meta_data[ $array_key ]->value = null;
1726 }
1727 }
1728 }
1729
1730 /**
1731 * Return data changes only.
1732 *
1733 * @return array
1734 */
1735 public function get_changes(): array {
1736 return $this->changes;
1737 }
1738
1739 /**
1740 * Merge changes with data and clear.
1741 */
1742 public function apply_changes() {
1743 $this->data = array_replace_recursive( $this->data, $this->changes );
1744 $this->changes = [];
1745 }
1746
1747 /**
1748 * Prefix for action and filter hooks on data.
1749 *
1750 * @param string $prop
1751 *
1752 * @return string
1753 */
1754 protected function get_hook_prefix( string $prop ): string {
1755 return 'storeengine/' . $this->object_type . '/get/' . $prop;
1756 }
1757
1758 /**
1759 * Gets a prop for a getter method.
1760 *
1761 * Gets the value from either current pending changes, or the data itself.
1762 * Context controls what happens to the value before it's returned.
1763 *
1764 * @param string $prop Name of prop to get.
1765 * @param string $context What the value is for. Valid values are view and edit.
1766 *
1767 * @return mixed
1768 */
1769 protected function get_prop( string $prop, string $context = 'view' ) {
1770 $value = null;
1771
1772 if ( array_key_exists( $prop, $this->data ) ) {
1773 $value = array_key_exists( $prop, $this->changes ) ? $this->changes[ $prop ] : $this->data[ $prop ];
1774
1775 if ( 'view' === $context ) {
1776 /**
1777 * @ignore Ignore from Hook parser.
1778 */
1779 $value = apply_filters( $this->get_hook_prefix( $prop ), $value, $this ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
1780 }
1781 }
1782
1783 return $value;
1784 }
1785
1786 /**
1787 * Get datetime string or unix-timestamp
1788 *
1789 * @param string $prop
1790 * @param string $format
1791 * @param bool $gmt
1792 * @param string $context
1793 *
1794 * @return false|int|string|null
1795 */
1796 protected function get_formatted_date_prop( string $prop, string $format = 'mysql', bool $gmt = false, string $context = 'view' ) {
1797 /**
1798 * @var ?StoreengineDatetime $date
1799 */
1800 $date = $this->get_prop( $prop, $context );
1801
1802 if ( ! $date ) {
1803 return $date;
1804 }
1805
1806 if ( 'mysql' === $format ) {
1807 $format = 'Y-m-d H:i:s';
1808 }
1809
1810 $timestamp = ! $gmt ? $date->getOffsetTimestamp() : $date->getTimestamp();
1811
1812 if ( 'timestamp' === $format || 'U' === $format ) {
1813 return $timestamp;
1814 }
1815
1816 return gmdate( $format, $timestamp );
1817 }
1818
1819 /*
1820 |--------------------------------------------------------------------------
1821 | Conditionals
1822 |--------------------------------------------------------------------------
1823 |
1824 | Checks if a condition is true or false.
1825 |
1826 */
1827
1828 /**
1829 * Checks the order status against a passed in status.
1830 *
1831 * @param array|string $status Status to check.
1832 *
1833 * @return bool
1834 */
1835 public function has_status( $status ): bool {
1836 return apply_filters( "storeengine/{$this->object_type}/has_status", ( is_array( $status ) && in_array( $this->get_status(), $status, true ) ) || $this->get_status() === $status, $this, $status );
1837 }
1838
1839 /**
1840 * Type checking.
1841 *
1842 * @param string|array $type Type.
1843 *
1844 * @return boolean
1845 */
1846 public function is_type( $type ): bool {
1847 return is_array( $type ) ? in_array( $this->get_type(), $type, true ) : $type === $this->get_type();
1848 }
1849
1850 /*
1851 |--------------------------------------------------------------------------
1852 | Mics.
1853 |--------------------------------------------------------------------------
1854 */
1855
1856 /**
1857 * Clear object cache for the entity.
1858 *
1859 * @param bool $flush_collection Flush collection query cache.
1860 *
1861 * @return void
1862 */
1863 public function clear_cache( bool $flush_collection = true ) {
1864 wp_cache_delete( $this->get_id(), $this->cache_group );
1865 if ( $this->meta_type ) {
1866 wp_cache_delete( $this->get_id(), $this->meta_type . '_meta' );
1867 }
1868
1869 if ( $flush_collection ) {
1870 wp_cache_set_last_changed( $this->cache_group );
1871 wp_cache_flush_group( $this->cache_group . '-queries' );
1872 wp_cache_delete( 'post_parent:' . $this->get_id(), $this->cache_group );
1873 }
1874 }
1875
1876 /**
1877 * When invalid data is found, throw an exception unless reading from the DB.
1878 *
1879 * @param string $code Error code.
1880 * @param string $message Error message.
1881 * @param int $http_status_code HTTP status code.
1882 * @param array|null $data Extra error data.
1883 * @param ?Throwable $previous Extra error data.
1884 *
1885 * @throws StoreEngineException Data Exception.
1886 */
1887 protected function error( string $code, string $message, int $http_status_code = 400, ?array $data = null, ?Throwable $previous = null ) {
1888 throw new StoreEngineException( wp_kses_post( $message ), esc_html( $code ), $data, $http_status_code, $previous ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
1889 }
1890 }
1891
1892 // End of file abstract-entity.php.
1893