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