blob: 20fcec0215cdb235205c11dbd2f32d43b516af91 [file] [log] [blame]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001<?php
2
3namespace LdapRecord\Models\Concerns;
4
5use Carbon\Carbon;
6use DateTimeInterface;
7use Exception;
8use LdapRecord\LdapRecordException;
9use LdapRecord\Models\Attributes\MbString;
10use LdapRecord\Models\Attributes\Timestamp;
11use LdapRecord\Models\DetectsResetIntegers;
12use LdapRecord\Support\Arr;
13
14trait HasAttributes
15{
16 use DetectsResetIntegers;
17
18 /**
19 * The models original attributes.
20 *
21 * @var array
22 */
23 protected $original = [];
24
25 /**
26 * The models attributes.
27 *
28 * @var array
29 */
30 protected $attributes = [];
31
32 /**
33 * The attributes that should be mutated to dates.
34 *
35 * @var array
36 */
37 protected $dates = [];
38
39 /**
40 * The attributes that should be cast to their native types.
41 *
42 * @var array
43 */
44 protected $casts = [];
45
46 /**
47 * The accessors to append to the model's array form.
48 *
49 * @var array
50 */
51 protected $appends = [];
52
53 /**
54 * The format that dates must be output to for serialization.
55 *
56 * @var string
57 */
58 protected $dateFormat;
59
60 /**
61 * The default attributes that should be mutated to dates.
62 *
63 * @var array
64 */
65 protected $defaultDates = [
66 'createtimestamp' => 'ldap',
67 'modifytimestamp' => 'ldap',
68 ];
69
70 /**
71 * The cache of the mutated attributes for each class.
72 *
73 * @var array
74 */
75 protected static $mutatorCache = [];
76
77 /**
78 * Convert the model's attributes to an array.
79 *
80 * @return array
81 */
82 public function attributesToArray()
83 {
84 // Here we will replace our LDAP formatted dates with
85 // properly formatted ones, so dates do not need to
86 // be converted manually after being returned.
87 $attributes = $this->addDateAttributesToArray(
88 $attributes = $this->getArrayableAttributes()
89 );
90
91 $attributes = $this->addMutatedAttributesToArray(
92 $attributes,
93 $this->getMutatedAttributes()
94 );
95
96 // Before we go ahead and encode each value, we'll attempt
97 // converting any necessary attribute values to ensure
98 // they can be encoded, such as GUIDs and SIDs.
99 $attributes = $this->convertAttributesForJson($attributes);
100
101 // Here we will grab all of the appended, calculated attributes to this model
102 // as these attributes are not really in the attributes array, but are run
103 // when we need to array or JSON the model for convenience to the coder.
104 foreach ($this->getArrayableAppends() as $key) {
105 $attributes[$key] = $this->mutateAttributeForArray($key, null);
106 }
107
108 // Now we will go through each attribute to make sure it is
109 // properly encoded. If attributes aren't in UTF-8, we will
110 // encounter JSON encoding errors upon model serialization.
111 return $this->encodeAttributes($attributes);
112 }
113
114 /**
115 * Add the date attributes to the attributes array.
116 *
117 * @param array $attributes
118 *
119 * @return array
120 */
121 protected function addDateAttributesToArray(array $attributes)
122 {
123 foreach ($this->getDates() as $attribute => $type) {
124 if (! isset($attributes[$attribute])) {
125 continue;
126 }
127
128 $date = $this->asDateTime($attributes[$attribute], $type);
129
130 $attributes[$attribute] = $date instanceof Carbon
131 ? Arr::wrap($this->serializeDate($date))
132 : $attributes[$attribute];
133 }
134
135 return $attributes;
136 }
137
138 /**
139 * Prepare a date for array / JSON serialization.
140 *
141 * @param DateTimeInterface $date
142 *
143 * @return string
144 */
145 protected function serializeDate(DateTimeInterface $date)
146 {
147 return $date->format($this->getDateFormat());
148 }
149
150 /**
151 * Recursively UTF-8 encode the given attributes.
152 *
153 * @return array
154 */
155 public function encodeAttributes($attributes)
156 {
157 array_walk_recursive($attributes, function (&$value) {
158 $value = $this->encodeValue($value);
159 });
160
161 return $attributes;
162 }
163
164 /**
165 * Encode the given value for proper serialization.
166 *
167 * @param string $value
168 *
169 * @return string
170 */
171 protected function encodeValue($value)
172 {
173 // If we are able to detect the encoding, we will
174 // encode only the attributes that need to be,
175 // so that we do not double encode values.
176 return MbString::isLoaded() && MbString::isUtf8($value) ? $value : utf8_encode($value);
177 }
178
179 /**
180 * Add the mutated attributes to the attributes array.
181 *
182 * @param array $attributes
183 * @param array $mutatedAttributes
184 *
185 * @return array
186 */
187 protected function addMutatedAttributesToArray(array $attributes, array $mutatedAttributes)
188 {
189 foreach ($mutatedAttributes as $key) {
190 // We want to spin through all the mutated attributes for this model and call
191 // the mutator for the attribute. We cache off every mutated attributes so
192 // we don't have to constantly check on attributes that actually change.
193 if (! Arr::exists($attributes, $key)) {
194 continue;
195 }
196
197 // Next, we will call the mutator for this attribute so that we can get these
198 // mutated attribute's actual values. After we finish mutating each of the
199 // attributes we will return this final array of the mutated attributes.
200 $attributes[$key] = $this->mutateAttributeForArray(
201 $key,
202 $attributes[$key]
203 );
204 }
205
206 return $attributes;
207 }
208
209 /**
210 * Set the model's original attributes with the model's current attributes.
211 *
212 * @return $this
213 */
214 public function syncOriginal()
215 {
216 $this->original = $this->attributes;
217
218 return $this;
219 }
220
221 /**
222 * Fills the entry with the supplied attributes.
223 *
224 * @param array $attributes
225 *
226 * @return $this
227 */
228 public function fill(array $attributes = [])
229 {
230 foreach ($attributes as $key => $value) {
231 $this->setAttribute($key, $value);
232 }
233
234 return $this;
235 }
236
237 /**
238 * Returns the models attribute by its key.
239 *
240 * @param int|string $key
241 *
242 * @return mixed
243 */
244 public function getAttribute($key)
245 {
246 if (! $key) {
247 return;
248 }
249
250 return $this->getAttributeValue($key);
251 }
252
253 /**
254 * Get an attributes value.
255 *
256 * @param string $key
257 *
258 * @return mixed
259 */
260 public function getAttributeValue($key)
261 {
262 $key = $this->normalizeAttributeKey($key);
263 $value = $this->getAttributeFromArray($key);
264
265 if ($this->hasGetMutator($key)) {
266 return $this->getMutatedAttributeValue($key, $value);
267 }
268
269 if ($this->isDateAttribute($key) && ! is_null($value)) {
270 return $this->asDateTime(Arr::first($value), $this->getDates()[$key]);
271 }
272
273 if ($this->isCastedAttribute($key) && ! is_null($value)) {
274 return $this->castAttribute($key, $value);
275 }
276
277 return $value;
278 }
279
280 /**
281 * Determine if the given attribute is a date.
282 *
283 * @param string $key
284 *
285 * @return bool
286 */
287 public function isDateAttribute($key)
288 {
289 return array_key_exists($key, $this->getDates());
290 }
291
292 /**
293 * Get the attributes that should be mutated to dates.
294 *
295 * @return array
296 */
297 public function getDates()
298 {
299 // Since array string keys can be unique depending
300 // on casing differences, we need to normalize the
301 // array key case so they are merged properly.
302 return array_merge(
303 array_change_key_case($this->defaultDates, CASE_LOWER),
304 array_change_key_case($this->dates, CASE_LOWER)
305 );
306 }
307
308 /**
309 * Convert the given date value to an LDAP compatible value.
310 *
311 * @param string $type
312 * @param mixed $value
313 *
314 * @throws LdapRecordException
315 *
316 * @return float|string
317 */
318 public function fromDateTime($type, $value)
319 {
320 return (new Timestamp($type))->fromDateTime($value);
321 }
322
323 /**
324 * Convert the given LDAP date value to a Carbon instance.
325 *
326 * @param mixed $value
327 * @param string $type
328 *
329 * @throws LdapRecordException
330 *
331 * @return Carbon|false
332 */
333 public function asDateTime($value, $type)
334 {
335 return (new Timestamp($type))->toDateTime($value);
336 }
337
338 /**
339 * Determine whether an attribute should be cast to a native type.
340 *
341 * @param string $key
342 * @param array|string|null $types
343 *
344 * @return bool
345 */
346 public function hasCast($key, $types = null)
347 {
348 if (array_key_exists($key, $this->getCasts())) {
349 return $types ? in_array($this->getCastType($key), (array) $types, true) : true;
350 }
351
352 return false;
353 }
354
355 /**
356 * Get the attributes that should be cast to their native types.
357 *
358 * @return array
359 */
360 protected function getCasts()
361 {
362 return array_change_key_case($this->casts, CASE_LOWER);
363 }
364
365 /**
366 * Determine whether a value is JSON castable for inbound manipulation.
367 *
368 * @param string $key
369 *
370 * @return bool
371 */
372 protected function isJsonCastable($key)
373 {
374 return $this->hasCast($key, ['array', 'json', 'object', 'collection']);
375 }
376
377 /**
378 * Get the type of cast for a model attribute.
379 *
380 * @param string $key
381 *
382 * @return string
383 */
384 protected function getCastType($key)
385 {
386 if ($this->isDecimalCast($this->getCasts()[$key])) {
387 return 'decimal';
388 }
389
390 if ($this->isDateTimeCast($this->getCasts()[$key])) {
391 return 'datetime';
392 }
393
394 return trim(strtolower($this->getCasts()[$key]));
395 }
396
397 /**
398 * Determine if the cast is a decimal.
399 *
400 * @param string $cast
401 *
402 * @return bool
403 */
404 protected function isDecimalCast($cast)
405 {
406 return strncmp($cast, 'decimal:', 8) === 0;
407 }
408
409 /**
410 * Determine if the cast is a datetime.
411 *
412 * @param string $cast
413 *
414 * @return bool
415 */
416 protected function isDateTimeCast($cast)
417 {
418 return strncmp($cast, 'datetime:', 8) === 0;
419 }
420
421 /**
422 * Determine if the given attribute must be casted.
423 *
424 * @param string $key
425 *
426 * @return bool
427 */
428 protected function isCastedAttribute($key)
429 {
430 return array_key_exists($key, array_change_key_case($this->casts, CASE_LOWER));
431 }
432
433 /**
434 * Cast an attribute to a native PHP type.
435 *
436 * @param string $key
437 * @param array|null $value
438 *
439 * @return mixed
440 */
441 protected function castAttribute($key, $value)
442 {
443 $value = $this->castRequiresArrayValue($key) ? $value : Arr::first($value);
444
445 if (is_null($value)) {
446 return $value;
447 }
448
449 switch ($this->getCastType($key)) {
450 case 'int':
451 case 'integer':
452 return (int) $value;
453 case 'real':
454 case 'float':
455 case 'double':
456 return $this->fromFloat($value);
457 case 'decimal':
458 return $this->asDecimal($value, explode(':', $this->getCasts()[$key], 2)[1]);
459 case 'string':
460 return (string) $value;
461 case 'bool':
462 case 'boolean':
463 return $this->asBoolean($value);
464 case 'object':
465 return $this->fromJson($value, $asObject = true);
466 case 'array':
467 case 'json':
468 return $this->fromJson($value);
469 case 'collection':
470 return $this->newCollection($value);
471 case 'datetime':
472 return $this->asDateTime($value, explode(':', $this->getCasts()[$key], 2)[1]);
473 default:
474 return $value;
475 }
476 }
477
478 /**
479 * Determine if the cast type requires the first attribute value.
480 *
481 * @return bool
482 */
483 protected function castRequiresArrayValue($key)
484 {
485 return in_array($this->getCastType($key), ['collection']);
486 }
487
488 /**
489 * Cast the given attribute to JSON.
490 *
491 * @param string $key
492 * @param mixed $value
493 *
494 * @return string
495 */
496 protected function castAttributeAsJson($key, $value)
497 {
498 $value = $this->asJson($value);
499
500 if ($value === false) {
501 $class = get_class($this);
502 $message = json_last_error_msg();
503
504 throw new Exception("Unable to encode attribute [{$key}] for model [{$class}] to JSON: {$message}.");
505 }
506
507 return $value;
508 }
509
510 /**
511 * Convert the model to its JSON representation.
512 *
513 * @return string
514 */
515 public function toJson()
516 {
517 return json_encode($this);
518 }
519
520 /**
521 * Encode the given value as JSON.
522 *
523 * @param mixed $value
524 *
525 * @return string
526 */
527 protected function asJson($value)
528 {
529 return json_encode($value);
530 }
531
532 /**
533 * Decode the given JSON back into an array or object.
534 *
535 * @param string $value
536 * @param bool $asObject
537 *
538 * @return mixed
539 */
540 public function fromJson($value, $asObject = false)
541 {
542 return json_decode($value, ! $asObject);
543 }
544
545 /**
546 * Decode the given float.
547 *
548 * @param mixed $value
549 *
550 * @return mixed
551 */
552 public function fromFloat($value)
553 {
554 switch ((string) $value) {
555 case 'Infinity':
556 return INF;
557 case '-Infinity':
558 return -INF;
559 case 'NaN':
560 return NAN;
561 default:
562 return (float) $value;
563 }
564 }
565
566 /**
567 * Cast the value to a boolean.
568 *
569 * @param mixed $value
570 *
571 * @return bool
572 */
573 protected function asBoolean($value)
574 {
575 $map = ['true' => true, 'false' => false];
576
577 return $map[strtolower($value)] ?? (bool) $value;
578 }
579
580 /**
581 * Cast a decimal value as a string.
582 *
583 * @param float $value
584 * @param int $decimals
585 *
586 * @return string
587 */
588 protected function asDecimal($value, $decimals)
589 {
590 return number_format($value, $decimals, '.', '');
591 }
592
593 /**
594 * Get an attribute array of all arrayable attributes.
595 *
596 * @return array
597 */
598 protected function getArrayableAttributes()
599 {
600 return $this->getArrayableItems($this->attributes);
601 }
602
603 /**
604 * Get an attribute array of all arrayable values.
605 *
606 * @param array $values
607 *
608 * @return array
609 */
610 protected function getArrayableItems(array $values)
611 {
612 if (count($visible = $this->getVisible()) > 0) {
613 $values = array_intersect_key($values, array_flip($visible));
614 }
615
616 if (count($hidden = $this->getHidden()) > 0) {
617 $values = array_diff_key($values, array_flip($hidden));
618 }
619
620 return $values;
621 }
622
623 /**
624 * Get all of the appendable values that are arrayable.
625 *
626 * @return array
627 */
628 protected function getArrayableAppends()
629 {
630 if (empty($this->appends)) {
631 return [];
632 }
633
634 return $this->getArrayableItems(
635 array_combine($this->appends, $this->appends)
636 );
637 }
638
639 /**
640 * Get the format for date serialization.
641 *
642 * @return string
643 */
644 public function getDateFormat()
645 {
646 return $this->dateFormat ?: DateTimeInterface::ISO8601;
647 }
648
649 /**
650 * Set the date format used by the model for serialization.
651 *
652 * @param string $format
653 *
654 * @return $this
655 */
656 public function setDateFormat($format)
657 {
658 $this->dateFormat = $format;
659
660 return $this;
661 }
662
663 /**
664 * Get an attribute from the $attributes array.
665 *
666 * @param string $key
667 *
668 * @return mixed
669 */
670 protected function getAttributeFromArray($key)
671 {
672 return $this->getNormalizedAttributes()[$key] ?? null;
673 }
674
675 /**
676 * Get the attributes with their keys normalized.
677 *
678 * @return array
679 */
680 protected function getNormalizedAttributes()
681 {
682 return array_change_key_case($this->attributes, CASE_LOWER);
683 }
684
685 /**
686 * Returns the first attribute by the specified key.
687 *
688 * @param string $key
689 *
690 * @return mixed
691 */
692 public function getFirstAttribute($key)
693 {
694 return Arr::first(
695 Arr::wrap($this->getAttribute($key))
696 );
697 }
698
699 /**
700 * Returns all of the models attributes.
701 *
702 * @return array
703 */
704 public function getAttributes()
705 {
706 return $this->attributes;
707 }
708
709 /**
710 * Set an attribute value by the specified key and sub-key.
711 *
712 * @param mixed $key
713 * @param mixed $value
714 *
715 * @return $this
716 */
717 public function setAttribute($key, $value)
718 {
719 $key = $this->normalizeAttributeKey($key);
720
721 if ($this->hasSetMutator($key)) {
722 return $this->setMutatedAttributeValue($key, $value);
723 } elseif (
724 $value &&
725 $this->isDateAttribute($key) &&
726 ! $this->valueIsResetInteger($value)
727 ) {
728 $value = $this->fromDateTime($this->getDates()[$key], $value);
729 }
730
731 if ($this->isJsonCastable($key) && ! is_null($value)) {
732 $value = $this->castAttributeAsJson($key, $value);
733 }
734
735 $this->attributes[$key] = Arr::wrap($value);
736
737 return $this;
738 }
739
740 /**
741 * Set the models first attribute value.
742 *
743 * @param string $key
744 * @param mixed $value
745 *
746 * @return $this
747 */
748 public function setFirstAttribute($key, $value)
749 {
750 return $this->setAttribute($key, Arr::wrap($value));
751 }
752
753 /**
754 * Add a unique value to the given attribute.
755 *
756 * @param string $key
757 * @param mixed $value
758 *
759 * @return $this
760 */
761 public function addAttributeValue($key, $value)
762 {
763 return $this->setAttribute($key, array_unique(
764 array_merge(
765 Arr::wrap($this->getAttribute($key)),
766 Arr::wrap($value)
767 )
768 ));
769 }
770
771 /**
772 * Determine if a get mutator exists for an attribute.
773 *
774 * @param string $key
775 *
776 * @return bool
777 */
778 public function hasGetMutator($key)
779 {
780 return method_exists($this, 'get'.$this->getMutatorMethodName($key).'Attribute');
781 }
782
783 /**
784 * Determine if a set mutator exists for an attribute.
785 *
786 * @param string $key
787 *
788 * @return bool
789 */
790 public function hasSetMutator($key)
791 {
792 return method_exists($this, 'set'.$this->getMutatorMethodName($key).'Attribute');
793 }
794
795 /**
796 * Set the value of an attribute using its mutator.
797 *
798 * @param string $key
799 * @param mixed $value
800 *
801 * @return mixed
802 */
803 protected function setMutatedAttributeValue($key, $value)
804 {
805 return $this->{'set'.$this->getMutatorMethodName($key).'Attribute'}($value);
806 }
807
808 /**
809 * Get the value of an attribute using its mutator.
810 *
811 * @param string $key
812 * @param mixed $value
813 *
814 * @return mixed
815 */
816 protected function getMutatedAttributeValue($key, $value)
817 {
818 return $this->{'get'.$this->getMutatorMethodName($key).'Attribute'}($value);
819 }
820
821 /**
822 * Get the mutator attribute method name.
823 *
824 * Hyphenated attributes will use pascal cased methods.
825 *
826 * @param string $key
827 *
828 * @return mixed
829 */
830 protected function getMutatorMethodName($key)
831 {
832 $key = ucwords(str_replace('-', ' ', $key));
833
834 return str_replace(' ', '', $key);
835 }
836
837 /**
838 * Get the value of an attribute using its mutator for array conversion.
839 *
840 * @param string $key
841 * @param mixed $value
842 *
843 * @return array
844 */
845 protected function mutateAttributeForArray($key, $value)
846 {
847 return Arr::wrap(
848 $this->getMutatedAttributeValue($key, $value)
849 );
850 }
851
852 /**
853 * Set the attributes property.
854 *
855 * Used when constructing an existing LDAP record.
856 *
857 * @param array $attributes
858 *
859 * @return $this
860 */
861 public function setRawAttributes(array $attributes = [])
862 {
863 // We will filter out those annoying 'count' keys
864 // returned with LDAP results and lowercase all
865 // root array keys to prevent any casing issues.
866 $raw = array_change_key_case($this->filterRawAttributes($attributes), CASE_LOWER);
867
868 // Before setting the models attributes, we will filter
869 // out the attributes that contain an integer key. LDAP
870 // search results will contain integer keys that have
871 // attribute names as values. We don't need these.
872 $this->attributes = array_filter($raw, function ($key) {
873 return ! is_int($key);
874 }, ARRAY_FILTER_USE_KEY);
875
876 // LDAP search results will contain the distinguished
877 // name inside of the `dn` key. We will retrieve this,
878 // and then set it on the model for accessibility.
879 if (Arr::exists($attributes, 'dn')) {
880 $this->dn = Arr::accessible($attributes['dn'])
881 ? Arr::first($attributes['dn'])
882 : $attributes['dn'];
883 }
884
885 $this->syncOriginal();
886
887 // Here we will set the exists attribute to true,
888 // since raw attributes are only set in the case
889 // of attributes being loaded by query results.
890 $this->exists = true;
891
892 return $this;
893 }
894
895 /**
896 * Filters the count key recursively from raw LDAP attributes.
897 *
898 * @param array $attributes
899 * @param array $keys
900 *
901 * @return array
902 */
903 public function filterRawAttributes(array $attributes = [], array $keys = ['count', 'dn'])
904 {
905 foreach ($keys as $key) {
906 unset($attributes[$key]);
907 }
908
909 foreach ($attributes as $key => $value) {
910 $attributes[$key] = is_array($value)
911 ? $this->filterRawAttributes($value, $keys)
912 : $value;
913 }
914
915 return $attributes;
916 }
917
918 /**
919 * Determine if the model has the given attribute.
920 *
921 * @param int|string $key
922 *
923 * @return bool
924 */
925 public function hasAttribute($key)
926 {
927 return [] !== ($this->attributes[$this->normalizeAttributeKey($key)] ?? []);
928 }
929
930 /**
931 * Returns the number of attributes.
932 *
933 * @return int
934 */
935 public function countAttributes()
936 {
937 return count($this->getAttributes());
938 }
939
940 /**
941 * Returns the models original attributes.
942 *
943 * @return array
944 */
945 public function getOriginal()
946 {
947 return $this->original;
948 }
949
950 /**
951 * Get the attributes that have been changed since last sync.
952 *
953 * @return array
954 */
955 public function getDirty()
956 {
957 $dirty = [];
958
959 foreach ($this->attributes as $key => $value) {
960 if ($this->isDirty($key)) {
961 // We need to reset the array using array_values due to
962 // LDAP requiring consecutive indices (0, 1, 2 etc.).
963 // We would receive an exception otherwise.
964 $dirty[$key] = array_values($value);
965 }
966 }
967
968 return $dirty;
969 }
970
971 /**
972 * Determine if the given attribute is dirty.
973 *
974 * @param string $key
975 *
976 * @return bool
977 */
978 public function isDirty($key)
979 {
980 return ! $this->originalIsEquivalent($key);
981 }
982
983 /**
984 * Get the accessors being appended to the models array form.
985 *
986 * @return array
987 */
988 public function getAppends()
989 {
990 return $this->appends;
991 }
992
993 /**
994 * Set the accessors to append to model arrays.
995 *
996 * @param array $appends
997 *
998 * @return $this
999 */
1000 public function setAppends(array $appends)
1001 {
1002 $this->appends = $appends;
1003
1004 return $this;
1005 }
1006
1007 /**
1008 * Return whether the accessor attribute has been appended.
1009 *
1010 * @param string $attribute
1011 *
1012 * @return bool
1013 */
1014 public function hasAppended($attribute)
1015 {
1016 return in_array($attribute, $this->appends);
1017 }
1018
1019 /**
1020 * Returns a normalized attribute key.
1021 *
1022 * @param string $key
1023 *
1024 * @return string
1025 */
1026 public function normalizeAttributeKey($key)
1027 {
1028 // Since LDAP supports hyphens in attribute names,
1029 // we'll convert attributes being retrieved by
1030 // underscores into hyphens for convenience.
1031 return strtolower(
1032 str_replace('_', '-', $key)
1033 );
1034 }
1035
1036 /**
1037 * Determine if the new and old values for a given key are equivalent.
1038 *
1039 * @param string $key
1040 *
1041 * @return bool
1042 */
1043 protected function originalIsEquivalent($key)
1044 {
1045 if (! array_key_exists($key, $this->original)) {
1046 return false;
1047 }
1048
1049 $current = $this->attributes[$key];
1050 $original = $this->original[$key];
1051
1052 if ($current === $original) {
1053 return true;
1054 }
1055
1056 return is_numeric($current) &&
1057 is_numeric($original) &&
1058 strcmp((string) $current, (string) $original) === 0;
1059 }
1060
1061 /**
1062 * Get the mutated attributes for a given instance.
1063 *
1064 * @return array
1065 */
1066 public function getMutatedAttributes()
1067 {
1068 $class = static::class;
1069
1070 if (! isset(static::$mutatorCache[$class])) {
1071 static::cacheMutatedAttributes($class);
1072 }
1073
1074 return static::$mutatorCache[$class];
1075 }
1076
1077 /**
1078 * Extract and cache all the mutated attributes of a class.
1079 *
1080 * @param string $class
1081 *
1082 * @return void
1083 */
1084 public static function cacheMutatedAttributes($class)
1085 {
1086 static::$mutatorCache[$class] = collect(static::getMutatorMethods($class))->reject(function ($match) {
1087 return $match === 'First';
1088 })->map(function ($match) {
1089 return lcfirst($match);
1090 })->all();
1091 }
1092
1093 /**
1094 * Get all of the attribute mutator methods.
1095 *
1096 * @param mixed $class
1097 *
1098 * @return array
1099 */
1100 protected static function getMutatorMethods($class)
1101 {
1102 preg_match_all('/(?<=^|;)get([^;]+?)Attribute(;|$)/', implode(';', get_class_methods($class)), $matches);
1103
1104 return $matches[1];
1105 }
1106}