PluginProbe
SureCart – Ecommerce Made Easy For Selling Physical Products, Digital Downloads, Subscriptions, Donations, & Payments / 4.2.1
SureCart – Ecommerce Made Easy For Selling Physical Products, Digital Downloads, Subscriptions, Donations, & Payments v4.2.1
4.7.2 4.7.1 4.7.0 4.6.6 4.6.5 4.6.4 4.6.3 4.6.2 4.6.1 4.6.0 4.5.1 4.5.0 4.4.2 4.4.1 4.4.0 4.3.3 4.3.2 4.3.1 4.3.0 4.2.3 4.2.2 4.2.1 1.0.3 1.0.4 1.0.5 All 281 releases
surecart / app / src / Models / Model.php
Model.php
1,432 lines 28.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace SureCart\Models;
4
5 use ArrayAccess;
6 use JsonSerializable;
7 use SureCart\Concerns\Arrayable;
8 use SureCart\Concerns\Objectable;
9
10 /**
11 * Model class
12 */
13 abstract class Model implements ArrayAccess, JsonSerializable, Arrayable, Objectable, ModelInterface {
14 /**
15 * Keeps track of booted models
16 *
17 * @var array
18 */
19 protected static $booted = [];
20
21 /**
22 * Keeps track of model events
23 *
24 * @var array
25 */
26 protected static $events = [];
27
28 /**
29 * Stores model attributes
30 *
31 * @var array
32 */
33 protected $attributes = [];
34
35 /**
36 * Static cache for attribute lookups.
37 *
38 * @var array
39 */
40 private static $attribute_cache = [];
41
42 /**
43 * Default attributes.
44 *
45 * @var array
46 */
47 protected $defaults = [];
48
49 /**
50 * Original attributes for dirty handling
51 *
52 * @var array
53 */
54 protected $original = [];
55
56 /**
57 * Rest API endpoint
58 *
59 * @var string
60 */
61 protected $endpoint = '';
62
63 /**
64 * Object name
65 *
66 * @var string
67 */
68 protected $object_name = '';
69
70 /**
71 * Query arguments
72 *
73 * @var array
74 */
75 protected $query = [];
76
77 /**
78 * Stores model relations
79 *
80 * @var array
81 */
82 protected $relations = [];
83
84 /**
85 * Fillable model items
86 *
87 * @var array
88 */
89 protected $fillable = [ '*' ];
90
91 /**
92 * Guarded model items
93 *
94 * @var array
95 */
96 protected $guarded = [];
97
98 /**
99 * Default collection limit
100 *
101 * @var integer
102 */
103 protected $limit = 20;
104
105 /**
106 * Default collection offset.
107 *
108 * @var integer
109 */
110 protected $offset = 0;
111
112 /**
113 * Is this cachable?
114 *
115 * @var boolean
116 */
117 protected $cachable = false;
118
119 /**
120 * Is this cachable?
121 *
122 * @var boolean
123 */
124 protected $cache_key = '';
125
126 /**
127 * Is this optimized caching?
128 *
129 * @var boolean
130 */
131 protected $optimized_caching = false;
132
133 /**
134 * Cache status for the request.
135 *
136 * @var string|null;
137 */
138 protected $cache_status = null;
139
140 /**
141 * Does an update clear account cache?
142 *
143 * @var boolean
144 */
145 protected $clears_account_cache = false;
146
147 /**
148 * Whether a get_attribute filter is currently executing on this instance.
149 * While true, getAttribute() bypasses the filter to prevent recursion.
150 *
151 * @var bool
152 */
153 protected $is_filtering_attribute = false;
154
155 /**
156 * Model constructor
157 *
158 * @param array $attributes Optional attributes.
159 */
160 public function __construct( $attributes = [] ) {
161 // Allow skipping filter registration during bulk operations (e.g., sitemap generation).
162 if ( apply_filters( "surecart/$this->object_name/skip_filters", false ) ) {
163 return;
164 }
165
166 add_filter( "surecart/$this->object_name/set_meta_data", [ $this, 'filterMetaData' ], 9 );
167
168 // if we have string here, assume it's the id.
169 if ( is_string( $attributes ) ) {
170 $attributes = [ 'id' => $attributes ];
171 }
172
173 $this->bootModel();
174 $this->syncOriginal();
175 $this->fill( $attributes );
176 }
177
178 /**
179 * Get the object name
180 *
181 * @return string
182 */
183 protected function getObjectName() {
184 return $this->object_name;
185 }
186
187 /**
188 * Get the cache status for the model.
189 *
190 * @return string|null;
191 */
192 public function getCacheStatus() {
193 return $this->cache_status;
194 }
195
196 /**
197 * Get the query.
198 *
199 * @return array
200 */
201 public function getQuery() {
202 return $this->query;
203 }
204
205 /**
206 * Filter meta data setting
207 *
208 * @param object $meta_data Meta data.
209 *
210 * @return function
211 */
212 public function filterMetaData( $meta_data ) {
213 return $meta_data;
214 }
215
216 /**
217 * Sync original attributes in case of dirty needs
218 *
219 * @return Model
220 */
221 public function syncOriginal() {
222 $this->original = $this->attributes;
223 return $this;
224 }
225
226 /**
227 * Run boot method on model
228 *
229 * @return void
230 */
231 public function bootModel() {
232 $called_class = static::getCalledClassName();
233
234 if ( ! isset( static::$booted[ $called_class ] ) ) {
235 static::$booted[ $called_class ] = true;
236 static::boot();
237 }
238 }
239
240 /**
241 * Get the called class name
242 *
243 * @return string
244 */
245 public static function getCalledClassName() {
246 $class = get_called_class();
247 $class = explode( '\\', $class );
248 end( $class );
249 $last = key( $class );
250 return strtolower( $class[ $last ] );
251 }
252
253 /**
254 * Model boot method
255 *
256 * @return void
257 */
258 public static function boot() {
259 // Note: Don't remove this method.
260 }
261
262 /**
263 * Does it have the attribute
264 *
265 * @param string $key Attribute key.
266 *
267 * @return boolean
268 */
269 public function hasAttribute( $key ) {
270 return array_key_exists( $key, $this->attributes );
271 }
272
273 /**
274 * Register a model event
275 *
276 * @param string $event Event name.
277 * @param function $callback Callback function.
278 *
279 * @return void
280 */
281 public static function registerModelEvent( $event, $callback ) {
282 $called_class = static::getCalledClassName();
283 static::$events[ "model.{$called_class}.{$event}" ] = $callback;
284 }
285
286 /**
287 * Model has been retrieved
288 *
289 * @param function $callback Callback method.
290 *
291 * @return void
292 */
293 public static function retrieved( $callback ) {
294 static::registerModelEvent( __FUNCTION__, $callback );
295 }
296
297 /**
298 * Model Saving (Before Save)
299 *
300 * @param function $callback Callback method.
301 *
302 * @return void
303 */
304 public static function saving( $callback ) {
305 static::registerModelEvent( __FUNCTION__, $callback );
306 }
307
308 /**
309 * Model Savied (After Save)
310 *
311 * @param function $callback Callback method.
312 *
313 * @return void
314 */
315 public static function saved( $callback ) {
316 static::registerModelEvent( __FUNCTION__, $callback );
317 }
318
319 /**
320 * Model Creating (Before Create)
321 *
322 * @param function $callback Callback method.
323 *
324 * @return void
325 */
326 public static function creating( $callback ) {
327 static::registerModelEvent( __FUNCTION__, $callback );
328 }
329
330 /**
331 * Model Created (After Create)
332 *
333 * @param function $callback Callback method.
334 *
335 * @return void
336 */
337 public static function created( $callback ) {
338 static::registerModelEvent( __FUNCTION__, $callback );
339 }
340
341 /**
342 * Model Updating (Before Updating)
343 *
344 * @param function $callback Callback method.
345 *
346 * @return void
347 */
348 public static function updating( $callback ) {
349 static::registerModelEvent( __FUNCTION__, $callback );
350 }
351
352 /**
353 * Model Updating (After Updating)
354 *
355 * @param function $callback Callback method.
356 *
357 * @return void
358 */
359 public static function updated( $callback ) {
360 static::registerModelEvent( __FUNCTION__, $callback );
361 }
362
363 /**
364 * Model Deleting (Before Deleting)
365 *
366 * @param function $callback Callback method.
367 *
368 * @return void
369 */
370 public static function deleting( $callback ) {
371 static::registerModelEvent( __FUNCTION__, $callback );
372 }
373
374 /**
375 * Model Deleted (After Deleted)
376 *
377 * @param function $callback Callback method.
378 *
379 * @return void
380 */
381 public static function deleted( $callback ) {
382 static::registerModelEvent( __FUNCTION__, $callback );
383 }
384
385 /**
386 * Fires the model event.
387 *
388 * @param string $event Event name.
389 *
390 * @return mixed
391 */
392 public function fireModelEvent( $event ) {
393 $called_class = static::getCalledClassName();
394 $event_name = "model.{$called_class}.{$event}";
395
396 // fire global event.
397 \do_action( "surecart/models/{$called_class}/{$event}", $this );
398
399 if ( isset( static::$events[ $event_name ] ) ) {
400 return call_user_func( static::$events[ $event_name ], $this );
401 }
402 }
403
404 /**
405 * Sets attributes in model
406 *
407 * @param array $attributes Attributes to fill.
408 *
409 * @return Model
410 */
411 public function fill( $attributes ) {
412 return $this->setAttributes( $attributes );
413 }
414
415 /**
416 * Reset attributes to blank.
417 *
418 * @return $this
419 */
420 public function resetAttributes() {
421 $this->attributes = [];
422 return $this;
423 }
424
425 /**
426 * Set model attributes
427 *
428 * @param array $attributes Attributes to add.
429 * @param boolean $is_guarded Use guarded.
430 *
431 * @return Model
432 */
433 public function setAttributes( $attributes, $is_guarded = true ) {
434 if ( empty( $attributes ) ) {
435 return $this;
436 }
437
438 foreach ( $attributes as $key => $value ) {
439 // allow filtering attribute.
440 $value = apply_filters( "surecart/{$this->object_name}/set_attribute", $value, $key, $this );
441
442 // remove api attributes.
443 if ( in_array( $key, [ '_locale', 'rest_route' ], true ) ) {
444 continue;
445 }
446
447 // set attribute.
448 if ( ! $is_guarded ) {
449 $this->setAttribute( $key, $value );
450 } elseif ( $this->isFillable( $key ) ) {
451 $this->setAttribute( $key, $value );
452 }
453 }
454
455 // Do an action.
456 do_action( "surecart/{$this->object_name}/attributes_set", $this );
457
458 return $this;
459 }
460
461 /**
462 * Get the metadata attribute.
463 * This makes sure the metadata is always an object.
464 *
465 * @return object
466 */
467 public function getMetadataAttribute() {
468 return (object) ( $this->attributes['metadata'] ?? [] );
469 }
470
471 /**
472 * Get the person who created it.
473 *
474 * @return int|false
475 */
476 public function getCreatedByAttribute() {
477 if ( empty( $this->metadata->wp_created_by ) ) {
478 return false;
479 }
480
481 return (int) $this->metadata->wp_created_by;
482 }
483
484 /**
485 * Sets an attribute
486 * Optionally calls a mutator based on set{Attribute}Attribute
487 *
488 * @param string $key Attribute key.
489 * @param mixed $value Attribute value.
490 *
491 * @return mixed|void
492 */
493 public function setAttribute( $key, $value ) {
494 // we are setting the cache status.
495 if ( 'cache_status' === $key ) {
496 $this->cache_status = $value;
497 return;
498 }
499
500 $setter = $this->getMutator( $key, 'set' );
501
502 if ( $setter ) {
503 return $this->{$setter}( $value );
504 } else {
505 $this->attributes[ $key ] = apply_filters( "surecart/$this->object_name/attributes/$key", $value, $this );
506 }
507 }
508
509 /**
510 * Calls a mutator based on set{Attribute}Attribute
511 *
512 * @param string $key Attribute key.
513 * @param mixed $type 'get' or 'set'.
514 *
515 * @return string|false
516 */
517 public function getMutator( $key, $type ) {
518 $key = ucwords( str_replace( [ '-', '_' ], ' ', $key ) );
519
520 $method = $type . str_replace( ' ', '', $key ) . 'Attribute';
521
522 if ( method_exists( $this, $method ) ) {
523 return $method;
524 }
525
526 return false;
527 }
528
529 /**
530 * Set the meta data attribute.
531 *
532 * @param array $meta_data Model meta data.
533 *
534 * @return self
535 */
536 public function setMetadataAttribute( $meta_data ) {
537 $this->attributes['metadata'] = (object) apply_filters( "surecart/$this->object_name/set_meta_data", $meta_data );
538 return $this;
539 }
540
541 /**
542 * Is the attribute fillable?
543 *
544 * @param string $key Attribute name.
545 *
546 * @return boolean
547 */
548 public function isFillable( $key ) {
549 $fillable = $this->getFillable();
550
551 if ( in_array( $key, $fillable, true ) ) {
552 return true;
553 }
554
555 if ( $this->isGuarded( $key ) ) {
556 return false;
557 }
558
559 return ! empty( $fillable ) && '*' === $fillable[0];
560 }
561
562 /**
563 * Is the key guarded
564 *
565 * @param string $key Name of the attribute.
566 *
567 * @return boolean
568 */
569 public function isGuarded( $key ) {
570 $guarded = $this->getGuarded();
571 return in_array( $key, $guarded, true ) || [ '*' ] === $guarded;
572 }
573
574 /**
575 * Get fillable items
576 *
577 * @return array
578 */
579 public function getFillable() {
580 return $this->fillable;
581 }
582
583 /**
584 * Get guarded items
585 *
586 * @return array
587 */
588 public function getGuarded() {
589 return $this->guarded;
590 }
591
592 /**
593 * Build query query
594 *
595 * @param array $query Arguments.
596 *
597 * @return Model
598 */
599 protected function where( $query = [] ) {
600 $this->query = array_merge( $query, $this->query );
601 return $this;
602 }
603
604 /**
605 * Build query query
606 *
607 * @param array $query Arguments.
608 *
609 * @return Model
610 */
611 protected function with( $query = [] ) {
612 $this->query['expand'] = (array) array_merge( $query, $this->query['expand'] ?? [] );
613 return $this;
614 }
615
616 /**
617 * Set the request mode (test or live).
618 *
619 * @param 'test'|'live' $mode Request mode.
620 */
621 public function setMode( $mode ) {
622 $this->mode = $mode;
623 return $this;
624 }
625
626 /**
627 * Get the current mode.
628 */
629 public function getMode() {
630 return $this->mode;
631 }
632
633 /**
634 * Make the API request.
635 *
636 * @param array $args Array of arguments.
637 * @param string $endpoint Optional endpoint override.
638 *
639 * @return Model
640 */
641 protected function makeRequest( $args = [], $endpoint = '' ) {
642 return \SureCart::request( ...$this->prepareRequest( $args, $endpoint ) );
643 }
644
645 /**
646 * Prepare API Request Arguments.
647 *
648 * @param array $args Array of arguments.
649 * @param string $endpoint Optional endpoint override.
650 *
651 * @return array $args for API request.
652 */
653 protected function prepareRequest( $args, $endpoint = '' ) {
654 // Create the endpoint.
655 if ( ! $endpoint ) {
656 $endpoint = ! empty( $args['id'] ) ? $this->endpoint . '/' . $args['id'] : $this->endpoint;
657 }
658
659 unset( $args['id'] );
660
661 // add query vars.
662 $args['query'] = $this->query;
663
664 return [ $endpoint, $args, $this->cachable, $this->cache_key, $this->optimized_caching ];
665 }
666
667 /**
668 * Paginate results
669 *
670 * @param array $args Pagination args.
671 * @return mixed
672 */
673 protected function paginate( $args = [] ) {
674 $this->setPagination( $args );
675
676 $items = $this->makeRequest();
677
678 if ( $this->isError( $items ) ) {
679 return $items;
680 }
681
682 if ( ! empty( $items->data ) ) {
683 $models = [];
684 foreach ( $items->data as $data ) {
685 $models[] = new static( $data );
686 }
687 $items->data = $models;
688 }
689
690 return new Collection( $items );
691 }
692
693 /**
694 * Set the pagination args.
695 *
696 * @param array $args Pagination args.
697 * @return $this
698 */
699 protected function setPagination( $args ) {
700 $args = wp_parse_args(
701 $args,
702 [
703 'page' => 1,
704 'per_page' => 20,
705 ]
706 );
707
708 $this->query['limit'] = $args['per_page'];
709 $this->query['page'] = $args['page'];
710
711 return $this;
712 }
713
714 /**
715 * Fetch a list of items
716 *
717 * @return array|\WP_Error;
718 */
719 protected function get() {
720 $this->query['limit'] = $this->query['limit'] ?? 100;
721
722 $items = $this->makeRequest();
723
724 if ( $this->isError( $items ) ) {
725 return $items;
726 }
727
728 // check empty.
729 if ( empty( $items->data ) ) {
730 return [];
731 }
732
733 $models = [];
734 foreach ( $items->data as $data ) {
735 $models[] = new static( $data );
736 }
737
738 return $models;
739 }
740
741 /**
742 * Get the first item in the query
743 */
744 public function first() {
745 $this->query['limit'] = 1;
746
747 $items = $this->makeRequest();
748
749 if ( $this->isError( $items ) ) {
750 return $items;
751 }
752
753 $item = ! empty( $items->data[0] ) ? new static( $items->data[0] ) : null;
754
755 return $item;
756 }
757
758 /**
759 * Find a specific model with and id
760 *
761 * @param string $id Id of the model.
762 *
763 * @return $this
764 */
765 protected function find( $id = '' ) {
766 if ( $this->fireModelEvent( 'finding' ) === false ) {
767 return false;
768 }
769
770 $attributes = $this->makeRequest( [ 'id' => $id ] );
771
772 if ( $this->isError( $attributes ) ) {
773 return $attributes;
774 }
775
776 $this->fireModelEvent( 'found' );
777 $this->syncOriginal();
778 $this->fill( $attributes );
779
780 return $this;
781 }
782
783 /**
784 * Is the response an Error?
785 *
786 * @param array|\WP_Error|\WP_REST_Response $response Response from request.
787 *
788 * @return boolean
789 */
790 public function isError( $response ) {
791 return is_wp_error( $response ) || ( $response instanceof \WP_REST_Response && ! in_array( $response->get_status(), [ 200, 201 ], true ) );
792 }
793
794 /**
795 * Get fresh instance from DB.
796 *
797 * @return self
798 */
799 protected function fresh() {
800 if ( ! $this->attributes['id'] ) {
801 return $this;
802 }
803
804 return $this->makeRequest(
805 [
806 'id' => $this->attributes['id'],
807 ]
808 );
809 }
810
811 /**
812 * Get fresh instance from DB.
813 *
814 * @return self
815 */
816 protected function refresh() {
817 if ( ! $this->attributes['id'] ) {
818 return $this;
819 }
820
821 $attributes = $this->fresh();
822
823 if ( $this->isError( $attributes ) ) {
824 return $attributes;
825 }
826
827 $this->syncOriginal();
828 $this->fill( $attributes );
829
830 return $this;
831 }
832
833 /**
834 * Save model
835 *
836 * @return $this|false
837 */
838 protected function save() {
839 if ( $this->fireModelEvent( 'saving' ) === false ) {
840 return false;
841 }
842
843 // update or create.
844 if ( $this->id ) {
845 $saved = $this->isDirty() ? $this->update() : true;
846 } else {
847 $saved = $this->create();
848 }
849
850 if ( $this->isError( $saved ) ) {
851 return $saved;
852 }
853
854 $this->fireModelEvent( 'saved' );
855
856 $this->syncOriginal();
857
858 return $saved;
859 }
860
861 /**
862 * Create a new model
863 *
864 * @param array $attributes Attributes to create.
865 *
866 * @return $this|false
867 */
868 protected function create( $attributes = [] ) {
869 if ( $this->fireModelEvent( 'creating' ) === false ) {
870 return false;
871 }
872
873 if ( $attributes ) {
874 $this->syncOriginal();
875 $this->fill( $attributes );
876 }
877
878 // add created by WordPress param.
879 $user_id = get_current_user_id();
880 if ( $user_id && isset( $this->metadata ) ) {
881 $this->metadata->wp_created_by = $user_id;
882 }
883
884 $created = $this->makeRequest(
885 [
886 'method' => 'POST',
887 'body' => [
888 $this->object_name => $this->getAttributes(),
889 ],
890 ]
891 );
892
893 // bail if error.
894 if ( $this->isError( $created ) ) {
895 return $created;
896 }
897
898 // reset.
899 $this->resetAttributes();
900
901 // fill.
902 $this->fill( $created ?? [] );
903
904 // fire event.
905 $this->fireModelEvent( 'created' );
906
907 // clear account cache.
908 if ( $this->cachable || $this->clears_account_cache ) {
909 \SureCart::account()->clearCache();
910 }
911
912 return $this;
913 }
914
915 /**
916 * Queue a sync job for later.
917 *
918 * @return \SureCart\Background\QueueService
919 */
920 protected function queueSync() {
921 return \SureCart::queue()->async( 'surecart/sync/product', [ 'id' => $this->id ] );
922 }
923
924 /**
925 * Only return specific properties from the model.
926 *
927 * @param array $attributes Attributes to return.
928 *
929 * @return array
930 */
931 protected function only( $attributes ) {
932 $attributes = is_array( $attributes ) ? $attributes : func_get_args();
933 return array_intersect_key( $this->toArray(), array_flip( $attributes ) );
934 }
935
936 /**
937 * Return all attributes except the ones passed.
938 *
939 * @param array $attributes Attributes to exclude.
940 */
941 protected function without( $attributes ) {
942 $attributes = is_array( $attributes ) ? $attributes : func_get_args();
943 return array_diff_key( $this->toArray(), array_flip( $attributes ) );
944 }
945
946 /**
947 * Update the model.
948 *
949 * @param array $attributes Attributes to update.
950 * @return $this|\WP_Error|false
951 */
952 protected function update( $attributes = [] ) {
953 if ( $this->fireModelEvent( 'updating' ) === false ) {
954 return false;
955 }
956
957 if ( $attributes ) {
958 $this->syncOriginal();
959 $this->fill( $attributes );
960 }
961
962 $attributes = $this->attributes;
963 unset( $attributes['id'] );
964
965 $updated = $this->makeRequest(
966 [
967 'id' => $this->id,
968 'method' => 'PATCH',
969 'body' => [
970 $this->object_name => $attributes,
971 ],
972 ]
973 );
974
975 if ( $this->isError( $updated ) ) {
976 return $updated;
977 }
978
979 $this->resetAttributes();
980
981 $this->fill( $updated );
982
983 $this->fireModelEvent( 'updated' );
984
985 // clear account cache.
986 if ( $this->cachable || $this->clears_account_cache ) {
987 \SureCart::account()->clearCache();
988 }
989
990 return $this;
991 }
992
993 /**
994 * Delete the model.
995 *
996 * @param string $id The id of the model to delete.
997 * @return $this|false
998 */
999 protected function delete( $id = '' ) {
1000 $this->id = $id ? $id : $this->id;
1001
1002 if ( $this->fireModelEvent( 'deleting' ) === false ) {
1003 return false;
1004 }
1005
1006 $deleted = $this->makeRequest(
1007 [
1008 'id' => $this->id,
1009 'method' => 'DELETE',
1010 ]
1011 );
1012
1013 if ( $this->isError( $deleted ) ) {
1014 return $deleted;
1015 }
1016
1017 $this->fireModelEvent( 'deleted' );
1018
1019 // clear account cache.
1020 if ( $this->cachable || $this->clears_account_cache ) {
1021 \SureCart::account()->clearCache();
1022 }
1023
1024 return $deleted;
1025 }
1026
1027 /**
1028 * Set a model relation.
1029 *
1030 * @param string $attribute Attribute name.
1031 * @param string $value Value to set.
1032 * @param string $model Model name.
1033 * @return void
1034 */
1035 public function setRelation( $attribute, $value, $model ) {
1036 if ( $value ) {
1037 $this->attributes[ $attribute ] = is_string( $value ) ? $value : new $model( $value );
1038 }
1039 }
1040
1041 /**
1042 * Get a relation id.
1043 *
1044 * @param string $attribute Attribute name.
1045 * @return string|null;
1046 */
1047 public function getRelationId( $attribute ) {
1048 $value = $this->attributes[ $attribute ] ?? null;
1049 return ! empty( $value['id'] ) ? $value['id'] : $value;
1050 }
1051
1052 /**
1053 * Set a model collection.
1054 *
1055 * @param string $attribute Attribute name.
1056 * @param \SureCart\Models\Collection $collection Collection.
1057 * @param string $model Model name.
1058 *
1059 * @return void
1060 */
1061 public function setCollection( $attribute, $collection, $model ) {
1062 $models = [];
1063 if ( ! empty( $collection->data ) && is_array( $collection->data ) ) {
1064 foreach ( $collection->data as $attributes ) {
1065 $models[] = is_a( $attributes, $model ) ? $attributes : new $model( $attributes );
1066 }
1067 $collection->data = $models;
1068 }
1069 $this->attributes[ $attribute ] = $collection;
1070 }
1071
1072 /**
1073 * Get the model attributes
1074 *
1075 * @return array
1076 */
1077 public function getAttributes() {
1078 return json_decode( wp_json_encode( $this->attributes ), true );
1079 }
1080
1081 /**
1082 * Get a specific attribute.
1083 *
1084 * Fires the `surecart/{object_name}/get_attribute` filter, allowing
1085 * third-party code to modify the returned value.
1086 *
1087 * Note: filter callbacks should use $value (first argument) for the
1088 * current attribute. Accessing other attributes on $model (e.g.
1089 * $model->slug inside a name filter) returns unfiltered values to
1090 * prevent recursion. Avoid re-reading the same key — use $value instead.
1091 *
1092 * @param string $key Attribute name.
1093 *
1094 * @return mixed
1095 */
1096 public function getAttribute( $key ) {
1097 $attribute = null;
1098
1099 if ( $this->hasAttribute( $key ) ) {
1100 $attribute = $this->attributes[ $key ];
1101 }
1102
1103 $getter = $this->getMutator( $key, 'get' );
1104
1105 if ( $getter ) {
1106 $attribute = $this->{$getter}( $attribute );
1107 }
1108
1109 if ( ! $this->is_filtering_attribute
1110 && $this->object_name
1111 && has_filter( "surecart/{$this->object_name}/get_attribute" )
1112 ) {
1113 $this->is_filtering_attribute = true;
1114 try {
1115 $attribute = apply_filters(
1116 "surecart/{$this->object_name}/get_attribute",
1117 $attribute,
1118 $key,
1119 $this
1120 );
1121 } finally {
1122 $this->is_filtering_attribute = false;
1123 }
1124 }
1125
1126 return $attribute;
1127 }
1128
1129 /**
1130 * Get a in-memory cached attribute.
1131 *
1132 * @param string $key The attribute key.
1133 * @return mixed|null The cached value or null if not found.
1134 */
1135 protected function getCachedAttribute( $key ) {
1136 if ( ! isset( self::$attribute_cache ) ) {
1137 self::$attribute_cache = [];
1138 }
1139
1140 if ( empty( $this->id ) ) {
1141 return null;
1142 }
1143
1144 return self::$attribute_cache[ $this->id . '_' . $key ] ?? null;
1145 }
1146
1147 /**
1148 * Set a cached attribute in memory.
1149 *
1150 * @param string $key The attribute key.
1151 * @param mixed $value The value to cache.
1152 * @return void
1153 */
1154 protected function setAttributeCache( $key, $value ) {
1155 if ( ! isset( self::$attribute_cache ) ) {
1156 self::$attribute_cache = [];
1157 }
1158
1159 if ( empty( $this->id ) ) {
1160 return;
1161 }
1162
1163 self::$attribute_cache[ $this->id . '_' . $key ] = $value;
1164 }
1165
1166 /**
1167 * Serialize to json.
1168 *
1169 * @return array
1170 */
1171 #[\ReturnTypeWillChange]
1172 public function jsonSerialize() {
1173 return $this->toArray();
1174 }
1175
1176 /**
1177 * Convert to object.
1178 *
1179 * @return Object
1180 */
1181 public function toObject() {
1182 $attributes = (object) $this->attributes;
1183
1184 // Check if any accessor is available and call it.
1185 foreach ( get_class_methods( $this ) as $method ) {
1186 if ( method_exists( get_class(), $method ) ) {
1187 continue;
1188 }
1189
1190 if ( 'get' === substr( $method, 0, 3 ) && 'Attribute' === substr( $method, -9 ) ) {
1191 $key = str_replace( [ 'get', 'Attribute' ], '', $method );
1192 if ( $key ) {
1193 $pieces = preg_split( '/(?=[A-Z])/', $key );
1194 $pieces = array_map( 'strtolower', array_filter( $pieces ) );
1195 $key = implode( '_', $pieces );
1196 $attributes->$key = $this->getAttribute( $key );
1197 }
1198 }
1199 }
1200
1201 // Check if any attribute is a model and call toArray.
1202 array_walk_recursive(
1203 $attributes,
1204 function ( &$value ) {
1205 if ( is_a( $value, Objectable::class ) ) {
1206 $value = $value->toObject();
1207 }
1208 }
1209 );
1210
1211 return $attributes;
1212 }
1213
1214 /**
1215 * Calls accessors during toArray.
1216 *
1217 * @return array
1218 */
1219 public function toArray() {
1220 $attributes = $this->getAttributes();
1221
1222 // Check if any accessor is available and call it.
1223 foreach ( get_class_methods( $this ) as $method ) {
1224 if ( ! method_exists( get_class( $this ), $method ) ) {
1225 continue;
1226 }
1227
1228 if ( 'get' === substr( $method, 0, 3 ) && 'Attribute' === substr( $method, -9 ) ) {
1229 $key = str_replace( [ 'get', 'Attribute' ], '', $method );
1230 if ( $key ) {
1231 $pieces = preg_split( '/(?=[A-Z])/', $key );
1232 $pieces = array_map( 'strtolower', array_filter( $pieces ) );
1233 $key = implode( '_', $pieces );
1234 $attributes[ $key ] = $this->getAttribute( $key );
1235 }
1236 }
1237 }
1238
1239 // Check if any attribute is a model and call toArray.
1240 array_walk_recursive(
1241 $attributes,
1242 function ( &$value ) {
1243 if ( is_a( $value, Arrayable::class ) ) {
1244 $value = $value->toArray();
1245 }
1246 }
1247 );
1248
1249 return array_merge( $attributes, $this->relations );
1250 }
1251
1252 /**
1253 * Is the model dirty (has unsaved items)
1254 *
1255 * @param array $attributes Optionally pass attributes to check.
1256 *
1257 * @return boolean
1258 */
1259 public function isDirty( $attributes = null ) {
1260 $dirty = $this->getDirty();
1261
1262 if ( is_null( $attributes ) ) {
1263 return count( $dirty ) > 0;
1264 }
1265
1266 if ( ! is_array( $attributes ) ) {
1267 $attributes = func_get_args();
1268 }
1269
1270 foreach ( $attributes as $attribute ) {
1271 if ( array_key_exists( $attribute, $dirty ) ) {
1272 return true;
1273 }
1274 }
1275
1276 return false;
1277 }
1278
1279 /**
1280 * Get dirty (unsaved) items
1281 *
1282 * @return array
1283 */
1284 public function getDirty() {
1285 $dirty = [];
1286
1287 foreach ( $this->attributes as $key => $value ) {
1288 if ( ! array_key_exists( $key, $this->original ) ) {
1289 $dirty[ $key ] = $value;
1290 } elseif ( $value !== $this->original[ $key ] &&
1291 ! $this->originalIsNumericallyEquivalent( $key ) ) {
1292 $dirty[ $key ] = $value;
1293 }
1294 }
1295
1296 return $dirty;
1297 }
1298
1299 /**
1300 * Determine if the new and old values for a given key are numerically equivalent.
1301 *
1302 * @param string $key Attribute name.
1303 * @return bool
1304 */
1305 protected function originalIsNumericallyEquivalent( $key ) {
1306 $current = $this->attributes[ $key ];
1307 $original = $this->original[ $key ];
1308 return is_numeric( $current ) && is_numeric( $original ) && strcmp( (string) $current, (string) $original ) === 0;
1309 }
1310
1311 /**
1312 * Get the original item
1313 *
1314 * @param string $key Name of the item.
1315 *
1316 * @return mixed
1317 */
1318 public function getOriginal( $key = null ) {
1319 if ( ! is_null( $key ) ) {
1320 return isset( $this->original[ $key ] ) ? $this->original[ $key ] : null;
1321 }
1322 return $this->original;
1323 }
1324
1325 /**
1326 * Get the attribute
1327 *
1328 * @param string $key Attribute name.
1329 *
1330 * @return mixed
1331 */
1332 public function __get( $key ) {
1333 return $this->getAttribute( $key );
1334 }
1335
1336 /**
1337 * Set the attribute
1338 *
1339 * @param string $key Attribute name.
1340 * @param mixed $value Value of attribute.
1341 *
1342 * @return void
1343 */
1344 public function __set( $key, $value ) {
1345 $this->setAttribute( $key, $value );
1346 }
1347
1348 /**
1349 * Determine if the given attribute exists.
1350 *
1351 * @param mixed $offset Name.
1352 * @return bool
1353 */
1354 public function offsetExists( $offset ): bool {
1355 return ! is_null( $this->getAttribute( $offset ) );
1356 }
1357
1358 /**
1359 * Get the value for a given offset.
1360 *
1361 * @param mixed $offset Name.
1362 * @return mixed
1363 */
1364 #[\ReturnTypeWillChange]
1365 public function offsetGet( $offset ) {
1366 return $this->getAttribute( $offset );
1367 }
1368
1369 /**
1370 * Set the value for a given offset.
1371 *
1372 * @param mixed $offset Name.
1373 * @param mixed $value Value.
1374 * @return void
1375 */
1376 public function offsetSet( $offset, $value ): void {
1377 $this->setAttribute( $offset, $value );
1378 }
1379
1380 /**
1381 * Unset the value for a given offset.
1382 *
1383 * @param mixed $offset Name.
1384 * @return void
1385 */
1386 public function offsetUnset( $offset ): void {
1387 unset( $this->attributes[ $offset ], $this->relations[ $offset ] );
1388 }
1389
1390 /**
1391 * Determine if an attribute or relation exists on the model.
1392 *
1393 * @param string $key Name.
1394 * @return bool
1395 */
1396 public function __isset( $key ) {
1397 return $this->offsetExists( $key );
1398 }
1399
1400 /**
1401 * Unset an attribute on the model.
1402 *
1403 * @param string $key Name.
1404 * @return void
1405 */
1406 public function __unset( $key ) {
1407 $this->offsetUnset( $key );
1408 }
1409
1410 /**
1411 * Forward call to method
1412 *
1413 * @param string $method Method to call.
1414 * @param mixed $params Method params.
1415 */
1416 public function __call( $method, $params ) {
1417 return call_user_func_array( [ $this, $method ], $params );
1418 }
1419
1420 /**
1421 * Static Facade Accessor
1422 *
1423 * @param string $method Method to call.
1424 * @param mixed $params Method params.
1425 *
1426 * @return mixed
1427 */
1428 public static function __callStatic( $method, $params ) {
1429 return call_user_func_array( [ new static(), $method ], $params );
1430 }
1431 }
1432