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