PluginProbe
SureCart – Ecommerce Made Easy For Selling Physical Products, Digital Downloads, Subscriptions, Donations, & Payments / 2.1.3
SureCart – Ecommerce Made Easy For Selling Physical Products, Digital Downloads, Subscriptions, Donations, & Payments v2.1.3
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,266 lines 24.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
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 * @param 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'] = 100;
689
690 $items = $this->makeRequest();
691
692 if ( $this->isError( $items ) ) {
693 return $items;
694 }
695
696 $models = [];
697 foreach ( $items->data as $data ) {
698 $models[] = new static( $data );
699 }
700
701 return $models;
702 }
703
704 /**
705 * Get the first item in the query
706 */
707 public function first() {
708 $this->query['limit'] = 1;
709
710 $items = $this->makeRequest();
711
712 if ( $this->isError( $items ) ) {
713 return $items;
714 }
715
716 return ! empty( $items->data[0] ) ? new static( $items->data[0] ) : null;
717 }
718
719 /**
720 * Find a specific model with and id
721 *
722 * @param string $id Id of the model.
723 *
724 * @return $this
725 */
726 protected function find( $id = '' ) {
727 if ( $this->fireModelEvent( 'finding' ) === false ) {
728 return false;
729 }
730
731 $attributes = $this->makeRequest( [ 'id' => $id ] );
732
733 if ( $this->isError( $attributes ) ) {
734 return $attributes;
735 }
736
737 $this->fireModelEvent( 'found' );
738 $this->syncOriginal();
739 $this->fill( $attributes );
740
741 return $this;
742 }
743
744 /**
745 * Is the response an Error?
746 *
747 * @param Array|\WP_Error|\WP_REST_Response $response Response from request.
748 *
749 * @return boolean
750 */
751 public function isError( $response ) {
752 return is_wp_error( $response ) || ( $response instanceof \WP_REST_Response && ! in_array( $response->get_status(), [ 200, 201 ], true ) );
753 }
754
755 /**
756 * Get fresh instance from DB.
757 *
758 * @return this
759 */
760 protected function fresh() {
761 if ( ! $this->attributes['id'] ) {
762 return $this;
763 }
764
765 return $this->makeRequest(
766 [
767 'id' => $this->attributes['id'],
768 ]
769 );
770 }
771
772 /**
773 * Get fresh instance from DB.
774 *
775 * @return this
776 */
777 protected function refresh() {
778 if ( ! $this->attributes['id'] ) {
779 return $this;
780 }
781
782 $attributes = $this->fresh();
783
784 if ( $this->isError( $attributes ) ) {
785 return $attributes;
786 }
787
788 $this->syncOriginal();
789 $this->fill( $attributes );
790
791 return $this;
792 }
793
794 /**
795 * Save model
796 *
797 * @return $this|false
798 */
799 protected function save() {
800 if ( $this->fireModelEvent( 'saving' ) === false ) {
801 return false;
802 }
803
804 // update or create.
805 if ( $this->id ) {
806 $saved = $this->isDirty() ? $this->update() : true;
807 } else {
808 $saved = $this->create();
809 }
810
811 if ( $this->isError( $saved ) ) {
812 return $saved;
813 }
814
815 $this->fireModelEvent( 'saved' );
816
817 $this->syncOriginal();
818
819 return $saved;
820 }
821
822 /**
823 * Create a new model
824 *
825 * @param array $attributes Attributes to create.
826 *
827 * @return $this|false
828 */
829 protected function create( $attributes = [] ) {
830 if ( $this->fireModelEvent( 'creating' ) === false ) {
831 return false;
832 }
833
834 if ( $attributes ) {
835 $this->syncOriginal();
836 $this->fill( $attributes );
837 }
838
839 // add created by WordPress param
840 $user_id = get_current_user_id();
841 if ( $user_id ) {
842 $this->addToMetaData( 'wp_created_by', $user_id );
843 }
844
845 $created = $this->makeRequest(
846 [
847 'method' => 'POST',
848 'body' => [
849 $this->object_name => $this->getAttributes(),
850 ],
851 ]
852 );
853
854 // bail if error.
855 if ( $this->isError( $created ) ) {
856 return $created;
857 }
858
859 // reset.
860 $this->resetAttributes();
861
862 // fill.
863 $this->fill( $created ?? [] );
864
865 // fire event.
866 $this->fireModelEvent( 'created' );
867
868 // clear account cache.
869 if ( $this->cachable || $this->clears_account_cache ) {
870 \SureCart::account()->clearCache();
871 }
872
873 return $this;
874 }
875
876 /**
877 * Update the model.
878 *
879 * @param array $attributes Attributes to update.
880 * @return $this|false
881 */
882 protected function update( $attributes = [] ) {
883 if ( $this->fireModelEvent( 'updating' ) === false ) {
884 return false;
885 }
886
887 if ( $attributes ) {
888 $this->syncOriginal();
889 $this->fill( $attributes );
890 }
891
892 $attributes = $this->attributes;
893 unset( $attributes['id'] );
894
895 $updated = $this->makeRequest(
896 [
897 'id' => $this->id,
898 'method' => 'PATCH',
899 'body' => [
900 $this->object_name => $attributes,
901 ],
902 ]
903 );
904
905 if ( $this->isError( $updated ) ) {
906 return $updated;
907 }
908
909 $this->resetAttributes();
910
911 $this->fill( $updated );
912
913 $this->fireModelEvent( 'updated' );
914
915 // clear account cache.
916 if ( $this->cachable || $this->clears_account_cache ) {
917 \SureCart::account()->clearCache();
918 }
919
920 return $this;
921 }
922
923 /**
924 * Delete the model.
925 *
926 * @return $this|false
927 */
928 protected function delete( $id = 0 ) {
929 $id = $id ? $id : $this->id;
930
931 if ( $this->fireModelEvent( 'deleting' ) === false ) {
932 return false;
933 }
934
935 $deleted = $this->makeRequest(
936 [
937 'id' => $id,
938 'method' => 'DELETE',
939 ]
940 );
941
942 if ( $this->isError( $deleted ) ) {
943 return $deleted;
944 }
945
946 $this->fireModelEvent( 'deleted' );
947
948 // clear account cache.
949 if ( $this->cachable || $this->clears_account_cache ) {
950 \SureCart::account()->clearCache();
951 }
952
953 return $deleted;
954 }
955
956 /**
957 * Set a model relation.
958 *
959 * @prop string $attribute Attribute name.
960 * @prop string $value Value to set.
961 * @prop string $model Model name.
962 * @return void
963 */
964 public function setRelation( $attribute, $value, $model ) {
965 if ( $value ) {
966 $this->attributes[ $attribute ] = is_string( $value ) ? $value : new $model( $value );
967 }
968 }
969
970 /**
971 * Get a relation id.
972 *
973 * @prop string $attribute Attribute name.
974 * @prop string $value Value to set.
975 * @prop string $model Model name.
976 * @return string|null;
977 */
978 public function getRelationId( $attribute ) {
979 $value = $this->attributes[ $attribute ] ?? null;
980 return ! empty( $value['id'] ) ? $value['id'] : $value;
981 }
982
983 /**
984 * Set a model collection.
985 *
986 * @prop string $attribute Attribute name.
987 * @prop string $value Value to set.
988 * @prop string $model Model name.
989 * @return void
990 */
991 public function setCollection( $attribute, $collection, $model ) {
992 $models = [];
993 if ( ! empty( $collection->data ) && is_array( $collection->data ) ) {
994 foreach ( $collection->data as $attributes ) {
995 $models[] = is_a( $attributes, $model ) ? $attributes : new $model( $attributes );
996 }
997 $collection->data = $models;
998 }
999 $this->attributes[ $attribute ] = $collection;
1000 }
1001
1002 /**
1003 * Get the model attributes
1004 *
1005 * @return array
1006 */
1007 public function getAttributes() {
1008 return json_decode( wp_json_encode( $this->attributes ), true );
1009 }
1010
1011 /**
1012 * Get a specific attribute
1013 *
1014 * @param string $key Attribute name.
1015 *
1016 * @return mixed
1017 */
1018 public function getAttribute( $key ) {
1019 $attribute = null;
1020
1021 if ( $this->hasAttribute( $key ) ) {
1022 $attribute = $this->attributes[ $key ];
1023 }
1024
1025 $getter = $this->getMutator( $key, 'get' );
1026
1027 if ( $getter ) {
1028 return $this->{$getter}( $attribute );
1029 } elseif ( ! is_null( $attribute ) ) {
1030 return $attribute;
1031 }
1032 }
1033
1034 /**
1035 * Serialize to json.
1036 *
1037 * @return Array
1038 */
1039 #[\ReturnTypeWillChange]
1040 public function jsonSerialize() {
1041 return $this->toArray();
1042 }
1043
1044 /**
1045 * Calls accessors during toArray.
1046 *
1047 * @return Array
1048 */
1049 public function toArray() {
1050 $attributes = $this->getAttributes();
1051
1052 // Check if any accessor is available and call it.
1053 foreach ( get_class_methods( $this ) as $method ) {
1054 if ( method_exists( get_class(), $method ) ) {
1055 continue;
1056 }
1057
1058 if ( 'get' === substr( $method, 0, 3 ) && 'Attribute' === substr( $method, -9 ) ) {
1059 $key = str_replace( [ 'get', 'Attribute' ], '', $method );
1060 if ( $key ) {
1061 $pieces = preg_split( '/(?=[A-Z])/', $key );
1062 $pieces = array_map( 'strtolower', array_filter( $pieces ) );
1063 $key = implode( '_', $pieces );
1064 $value = array_key_exists( $key, $this->attributes ) ? $this->attributes[ $key ] : null;
1065 $attributes[ $key ] = $this->{$method}( $value );
1066 }
1067 }
1068 }
1069
1070 // Check if any attribute is a model and call toArray.
1071 array_walk_recursive(
1072 $attributes,
1073 function ( &$value, $key ) {
1074 if ( $value instanceof Model ) {
1075 $value = $value->toArray();
1076 }
1077 }
1078 );
1079
1080 return array_merge( $attributes, $this->relations );
1081 }
1082
1083 /**
1084 * Is the model dirty (has unsaved items)
1085 *
1086 * @param array $attributes Optionally pass attributes to check.
1087 *
1088 * @return boolean
1089 */
1090 public function isDirty( $attributes = null ) {
1091 $dirty = $this->getDirty();
1092
1093 if ( is_null( $attributes ) ) {
1094 return count( $dirty ) > 0;
1095 }
1096
1097 if ( ! is_array( $attributes ) ) {
1098 $attributes = func_get_args();
1099 }
1100
1101 foreach ( $attributes as $attribute ) {
1102 if ( array_key_exists( $attribute, $dirty ) ) {
1103 return true;
1104 }
1105 }
1106
1107 return false;
1108 }
1109
1110 /**
1111 * Get dirty (unsaved) items
1112 *
1113 * @return array
1114 */
1115 public function getDirty() {
1116 $dirty = [];
1117
1118 foreach ( $this->attributes as $key => $value ) {
1119 if ( ! array_key_exists( $key, $this->original ) ) {
1120 $dirty[ $key ] = $value;
1121 } elseif ( $value !== $this->original[ $key ] &&
1122 ! $this->originalIsNumericallyEquivalent( $key ) ) {
1123 $dirty[ $key ] = $value;
1124 }
1125 }
1126
1127 return $dirty;
1128 }
1129
1130 /**
1131 * Determine if the new and old values for a given key are numerically equivalent.
1132 *
1133 * @param string $key Attribute name.
1134 * @return bool
1135 */
1136 protected function originalIsNumericallyEquivalent( $key ) {
1137 $current = $this->attributes[ $key ];
1138 $original = $this->original[ $key ];
1139 return is_numeric( $current ) && is_numeric( $original ) && strcmp( (string) $current, (string) $original ) === 0;
1140 }
1141
1142 /**
1143 * Get the original item
1144 *
1145 * @param string $key Name of the item.
1146 *
1147 * @return mixed
1148 */
1149 public function getOriginal( $key = null ) {
1150 if ( ! is_null( $key ) ) {
1151 return isset( $this->original[ $key ] ) ? $this->original[ $key ] : null;
1152 }
1153 return $this->original;
1154 }
1155
1156 /**
1157 * Get the attribute
1158 *
1159 * @param string $key Attribute name.
1160 *
1161 * @return mixed
1162 */
1163 public function __get( $key ) {
1164 return $this->getAttribute( $key );
1165 }
1166
1167 /**
1168 * Set the attribute
1169 *
1170 * @param string $key Attribute name.
1171 * @param mixed $value Value of attribute.
1172 *
1173 * @return void
1174 */
1175 public function __set( $key, $value ) {
1176 $this->setAttribute( $key, $value );
1177 }
1178
1179 /**
1180 * Determine if the given attribute exists.
1181 *
1182 * @param mixed $offset Name.
1183 * @return bool
1184 */
1185 #[\ReturnTypeWillChange]
1186 public function offsetExists( $offset ) {
1187 return ! is_null( $this->getAttribute( $offset ) );
1188 }
1189
1190 /**
1191 * Get the value for a given offset.
1192 *
1193 * @param mixed $offset Name.
1194 * @return mixed
1195 */
1196 #[\ReturnTypeWillChange]
1197 public function offsetGet( $offset ) {
1198 return $this->getAttribute( $offset );
1199 }
1200
1201 /**
1202 * Set the value for a given offset.
1203 *
1204 * @param mixed $offset Name.
1205 * @param mixed $value Value.
1206 * @return void
1207 */
1208 #[\ReturnTypeWillChange]
1209 public function offsetSet( $offset, $value ) {
1210 $this->setAttribute( $offset, $value );
1211 }
1212
1213 /**
1214 * Unset the value for a given offset.
1215 *
1216 * @param mixed $offset Name.
1217 * @return void
1218 */
1219 #[\ReturnTypeWillChange]
1220 public function offsetUnset( $offset ) {
1221 unset( $this->attributes[ $offset ], $this->relations[ $offset ] );
1222 }
1223
1224 /**
1225 * Determine if an attribute or relation exists on the model.
1226 *
1227 * @param string $key Name.
1228 * @return bool
1229 */
1230 public function __isset( $key ) {
1231 return $this->offsetExists( $key );
1232 }
1233
1234 /**
1235 * Unset an attribute on the model.
1236 *
1237 * @param string $key Name.
1238 * @return void
1239 */
1240 public function __unset( $key ) {
1241 $this->offsetUnset( $key );
1242 }
1243
1244 /**
1245 * Forward call to method
1246 *
1247 * @param string $method Method to call.
1248 * @param mixed $params Method params.
1249 */
1250 public function __call( $method, $params ) {
1251 return call_user_func_array( [ $this, $method ], $params );
1252 }
1253
1254 /**
1255 * Static Facade Accessor
1256 *
1257 * @param string $method Method to call.
1258 * @param mixed $params Method params.
1259 *
1260 * @return mixed
1261 */
1262 public static function __callStatic( $method, $params ) {
1263 return call_user_func_array( [ new static(), $method ], $params );
1264 }
1265 }
1266