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