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