blob: b5f33357965254964588e9b83e3794ff0543ffe1 [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
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100241 * @param mixed $default
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200242 *
243 * @return mixed
244 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100245 public function getAttribute($key, $default = null)
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200246 {
247 if (! $key) {
248 return;
249 }
250
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100251 return $this->getAttributeValue($key, $default);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200252 }
253
254 /**
255 * Get an attributes value.
256 *
257 * @param string $key
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100258 * @param mixed $default
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200259 *
260 * @return mixed
261 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100262 public function getAttributeValue($key, $default = null)
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200263 {
264 $key = $this->normalizeAttributeKey($key);
265 $value = $this->getAttributeFromArray($key);
266
267 if ($this->hasGetMutator($key)) {
268 return $this->getMutatedAttributeValue($key, $value);
269 }
270
271 if ($this->isDateAttribute($key) && ! is_null($value)) {
272 return $this->asDateTime(Arr::first($value), $this->getDates()[$key]);
273 }
274
275 if ($this->isCastedAttribute($key) && ! is_null($value)) {
276 return $this->castAttribute($key, $value);
277 }
278
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100279 return is_null($value) ? $default : $value;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200280 }
281
282 /**
283 * Determine if the given attribute is a date.
284 *
285 * @param string $key
286 *
287 * @return bool
288 */
289 public function isDateAttribute($key)
290 {
291 return array_key_exists($key, $this->getDates());
292 }
293
294 /**
295 * Get the attributes that should be mutated to dates.
296 *
297 * @return array
298 */
299 public function getDates()
300 {
301 // Since array string keys can be unique depending
302 // on casing differences, we need to normalize the
303 // array key case so they are merged properly.
304 return array_merge(
305 array_change_key_case($this->defaultDates, CASE_LOWER),
306 array_change_key_case($this->dates, CASE_LOWER)
307 );
308 }
309
310 /**
311 * Convert the given date value to an LDAP compatible value.
312 *
313 * @param string $type
314 * @param mixed $value
315 *
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200316 * @return float|string
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100317 *
318 * @throws LdapRecordException
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200319 */
320 public function fromDateTime($type, $value)
321 {
322 return (new Timestamp($type))->fromDateTime($value);
323 }
324
325 /**
326 * Convert the given LDAP date value to a Carbon instance.
327 *
328 * @param mixed $value
329 * @param string $type
330 *
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200331 * @return Carbon|false
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100332 *
333 * @throws LdapRecordException
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200334 */
335 public function asDateTime($value, $type)
336 {
337 return (new Timestamp($type))->toDateTime($value);
338 }
339
340 /**
341 * Determine whether an attribute should be cast to a native type.
342 *
343 * @param string $key
344 * @param array|string|null $types
345 *
346 * @return bool
347 */
348 public function hasCast($key, $types = null)
349 {
350 if (array_key_exists($key, $this->getCasts())) {
351 return $types ? in_array($this->getCastType($key), (array) $types, true) : true;
352 }
353
354 return false;
355 }
356
357 /**
358 * Get the attributes that should be cast to their native types.
359 *
360 * @return array
361 */
362 protected function getCasts()
363 {
364 return array_change_key_case($this->casts, CASE_LOWER);
365 }
366
367 /**
368 * Determine whether a value is JSON castable for inbound manipulation.
369 *
370 * @param string $key
371 *
372 * @return bool
373 */
374 protected function isJsonCastable($key)
375 {
376 return $this->hasCast($key, ['array', 'json', 'object', 'collection']);
377 }
378
379 /**
380 * Get the type of cast for a model attribute.
381 *
382 * @param string $key
383 *
384 * @return string
385 */
386 protected function getCastType($key)
387 {
388 if ($this->isDecimalCast($this->getCasts()[$key])) {
389 return 'decimal';
390 }
391
392 if ($this->isDateTimeCast($this->getCasts()[$key])) {
393 return 'datetime';
394 }
395
396 return trim(strtolower($this->getCasts()[$key]));
397 }
398
399 /**
400 * Determine if the cast is a decimal.
401 *
402 * @param string $cast
403 *
404 * @return bool
405 */
406 protected function isDecimalCast($cast)
407 {
408 return strncmp($cast, 'decimal:', 8) === 0;
409 }
410
411 /**
412 * Determine if the cast is a datetime.
413 *
414 * @param string $cast
415 *
416 * @return bool
417 */
418 protected function isDateTimeCast($cast)
419 {
420 return strncmp($cast, 'datetime:', 8) === 0;
421 }
422
423 /**
424 * Determine if the given attribute must be casted.
425 *
426 * @param string $key
427 *
428 * @return bool
429 */
430 protected function isCastedAttribute($key)
431 {
432 return array_key_exists($key, array_change_key_case($this->casts, CASE_LOWER));
433 }
434
435 /**
436 * Cast an attribute to a native PHP type.
437 *
438 * @param string $key
439 * @param array|null $value
440 *
441 * @return mixed
442 */
443 protected function castAttribute($key, $value)
444 {
445 $value = $this->castRequiresArrayValue($key) ? $value : Arr::first($value);
446
447 if (is_null($value)) {
448 return $value;
449 }
450
451 switch ($this->getCastType($key)) {
452 case 'int':
453 case 'integer':
454 return (int) $value;
455 case 'real':
456 case 'float':
457 case 'double':
458 return $this->fromFloat($value);
459 case 'decimal':
460 return $this->asDecimal($value, explode(':', $this->getCasts()[$key], 2)[1]);
461 case 'string':
462 return (string) $value;
463 case 'bool':
464 case 'boolean':
465 return $this->asBoolean($value);
466 case 'object':
467 return $this->fromJson($value, $asObject = true);
468 case 'array':
469 case 'json':
470 return $this->fromJson($value);
471 case 'collection':
472 return $this->newCollection($value);
473 case 'datetime':
474 return $this->asDateTime($value, explode(':', $this->getCasts()[$key], 2)[1]);
475 default:
476 return $value;
477 }
478 }
479
480 /**
481 * Determine if the cast type requires the first attribute value.
482 *
483 * @return bool
484 */
485 protected function castRequiresArrayValue($key)
486 {
487 return in_array($this->getCastType($key), ['collection']);
488 }
489
490 /**
491 * Cast the given attribute to JSON.
492 *
493 * @param string $key
494 * @param mixed $value
495 *
496 * @return string
497 */
498 protected function castAttributeAsJson($key, $value)
499 {
500 $value = $this->asJson($value);
501
502 if ($value === false) {
503 $class = get_class($this);
504 $message = json_last_error_msg();
505
506 throw new Exception("Unable to encode attribute [{$key}] for model [{$class}] to JSON: {$message}.");
507 }
508
509 return $value;
510 }
511
512 /**
513 * Convert the model to its JSON representation.
514 *
515 * @return string
516 */
517 public function toJson()
518 {
519 return json_encode($this);
520 }
521
522 /**
523 * Encode the given value as JSON.
524 *
525 * @param mixed $value
526 *
527 * @return string
528 */
529 protected function asJson($value)
530 {
531 return json_encode($value);
532 }
533
534 /**
535 * Decode the given JSON back into an array or object.
536 *
537 * @param string $value
538 * @param bool $asObject
539 *
540 * @return mixed
541 */
542 public function fromJson($value, $asObject = false)
543 {
544 return json_decode($value, ! $asObject);
545 }
546
547 /**
548 * Decode the given float.
549 *
550 * @param mixed $value
551 *
552 * @return mixed
553 */
554 public function fromFloat($value)
555 {
556 switch ((string) $value) {
557 case 'Infinity':
558 return INF;
559 case '-Infinity':
560 return -INF;
561 case 'NaN':
562 return NAN;
563 default:
564 return (float) $value;
565 }
566 }
567
568 /**
569 * Cast the value to a boolean.
570 *
571 * @param mixed $value
572 *
573 * @return bool
574 */
575 protected function asBoolean($value)
576 {
577 $map = ['true' => true, 'false' => false];
578
579 return $map[strtolower($value)] ?? (bool) $value;
580 }
581
582 /**
583 * Cast a decimal value as a string.
584 *
585 * @param float $value
586 * @param int $decimals
587 *
588 * @return string
589 */
590 protected function asDecimal($value, $decimals)
591 {
592 return number_format($value, $decimals, '.', '');
593 }
594
595 /**
596 * Get an attribute array of all arrayable attributes.
597 *
598 * @return array
599 */
600 protected function getArrayableAttributes()
601 {
602 return $this->getArrayableItems($this->attributes);
603 }
604
605 /**
606 * Get an attribute array of all arrayable values.
607 *
608 * @param array $values
609 *
610 * @return array
611 */
612 protected function getArrayableItems(array $values)
613 {
614 if (count($visible = $this->getVisible()) > 0) {
615 $values = array_intersect_key($values, array_flip($visible));
616 }
617
618 if (count($hidden = $this->getHidden()) > 0) {
619 $values = array_diff_key($values, array_flip($hidden));
620 }
621
622 return $values;
623 }
624
625 /**
626 * Get all of the appendable values that are arrayable.
627 *
628 * @return array
629 */
630 protected function getArrayableAppends()
631 {
632 if (empty($this->appends)) {
633 return [];
634 }
635
636 return $this->getArrayableItems(
637 array_combine($this->appends, $this->appends)
638 );
639 }
640
641 /**
642 * Get the format for date serialization.
643 *
644 * @return string
645 */
646 public function getDateFormat()
647 {
648 return $this->dateFormat ?: DateTimeInterface::ISO8601;
649 }
650
651 /**
652 * Set the date format used by the model for serialization.
653 *
654 * @param string $format
655 *
656 * @return $this
657 */
658 public function setDateFormat($format)
659 {
660 $this->dateFormat = $format;
661
662 return $this;
663 }
664
665 /**
666 * Get an attribute from the $attributes array.
667 *
668 * @param string $key
669 *
670 * @return mixed
671 */
672 protected function getAttributeFromArray($key)
673 {
674 return $this->getNormalizedAttributes()[$key] ?? null;
675 }
676
677 /**
678 * Get the attributes with their keys normalized.
679 *
680 * @return array
681 */
682 protected function getNormalizedAttributes()
683 {
684 return array_change_key_case($this->attributes, CASE_LOWER);
685 }
686
687 /**
688 * Returns the first attribute by the specified key.
689 *
690 * @param string $key
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100691 * @param mixed $default
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200692 *
693 * @return mixed
694 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100695 public function getFirstAttribute($key, $default = null)
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200696 {
697 return Arr::first(
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100698 Arr::wrap($this->getAttribute($key, $default)),
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200699 );
700 }
701
702 /**
703 * Returns all of the models attributes.
704 *
705 * @return array
706 */
707 public function getAttributes()
708 {
709 return $this->attributes;
710 }
711
712 /**
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100713 * Set an attribute value by the specified key.
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200714 *
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100715 * @param string $key
716 * @param mixed $value
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200717 *
718 * @return $this
719 */
720 public function setAttribute($key, $value)
721 {
722 $key = $this->normalizeAttributeKey($key);
723
724 if ($this->hasSetMutator($key)) {
725 return $this->setMutatedAttributeValue($key, $value);
726 } elseif (
727 $value &&
728 $this->isDateAttribute($key) &&
729 ! $this->valueIsResetInteger($value)
730 ) {
731 $value = $this->fromDateTime($this->getDates()[$key], $value);
732 }
733
734 if ($this->isJsonCastable($key) && ! is_null($value)) {
735 $value = $this->castAttributeAsJson($key, $value);
736 }
737
738 $this->attributes[$key] = Arr::wrap($value);
739
740 return $this;
741 }
742
743 /**
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100744 * Set an attribute on the model. No checking is done.
745 *
746 * @param string $key
747 * @param mixed $value
748 *
749 * @return $this
750 */
751 public function setRawAttribute($key, $value)
752 {
753 $key = $this->normalizeAttributeKey($key);
754
755 $this->attributes[$key] = Arr::wrap($value);
756
757 return $this;
758 }
759
760 /**
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200761 * Set the models first attribute value.
762 *
763 * @param string $key
764 * @param mixed $value
765 *
766 * @return $this
767 */
768 public function setFirstAttribute($key, $value)
769 {
770 return $this->setAttribute($key, Arr::wrap($value));
771 }
772
773 /**
774 * Add a unique value to the given attribute.
775 *
776 * @param string $key
777 * @param mixed $value
778 *
779 * @return $this
780 */
781 public function addAttributeValue($key, $value)
782 {
783 return $this->setAttribute($key, array_unique(
784 array_merge(
785 Arr::wrap($this->getAttribute($key)),
786 Arr::wrap($value)
787 )
788 ));
789 }
790
791 /**
792 * Determine if a get mutator exists for an attribute.
793 *
794 * @param string $key
795 *
796 * @return bool
797 */
798 public function hasGetMutator($key)
799 {
800 return method_exists($this, 'get'.$this->getMutatorMethodName($key).'Attribute');
801 }
802
803 /**
804 * Determine if a set mutator exists for an attribute.
805 *
806 * @param string $key
807 *
808 * @return bool
809 */
810 public function hasSetMutator($key)
811 {
812 return method_exists($this, 'set'.$this->getMutatorMethodName($key).'Attribute');
813 }
814
815 /**
816 * Set the value of an attribute using its mutator.
817 *
818 * @param string $key
819 * @param mixed $value
820 *
821 * @return mixed
822 */
823 protected function setMutatedAttributeValue($key, $value)
824 {
825 return $this->{'set'.$this->getMutatorMethodName($key).'Attribute'}($value);
826 }
827
828 /**
829 * Get the value of an attribute using its mutator.
830 *
831 * @param string $key
832 * @param mixed $value
833 *
834 * @return mixed
835 */
836 protected function getMutatedAttributeValue($key, $value)
837 {
838 return $this->{'get'.$this->getMutatorMethodName($key).'Attribute'}($value);
839 }
840
841 /**
842 * Get the mutator attribute method name.
843 *
844 * Hyphenated attributes will use pascal cased methods.
845 *
846 * @param string $key
847 *
848 * @return mixed
849 */
850 protected function getMutatorMethodName($key)
851 {
852 $key = ucwords(str_replace('-', ' ', $key));
853
854 return str_replace(' ', '', $key);
855 }
856
857 /**
858 * Get the value of an attribute using its mutator for array conversion.
859 *
860 * @param string $key
861 * @param mixed $value
862 *
863 * @return array
864 */
865 protected function mutateAttributeForArray($key, $value)
866 {
867 return Arr::wrap(
868 $this->getMutatedAttributeValue($key, $value)
869 );
870 }
871
872 /**
873 * Set the attributes property.
874 *
875 * Used when constructing an existing LDAP record.
876 *
877 * @param array $attributes
878 *
879 * @return $this
880 */
881 public function setRawAttributes(array $attributes = [])
882 {
883 // We will filter out those annoying 'count' keys
884 // returned with LDAP results and lowercase all
885 // root array keys to prevent any casing issues.
886 $raw = array_change_key_case($this->filterRawAttributes($attributes), CASE_LOWER);
887
888 // Before setting the models attributes, we will filter
889 // out the attributes that contain an integer key. LDAP
890 // search results will contain integer keys that have
891 // attribute names as values. We don't need these.
892 $this->attributes = array_filter($raw, function ($key) {
893 return ! is_int($key);
894 }, ARRAY_FILTER_USE_KEY);
895
896 // LDAP search results will contain the distinguished
897 // name inside of the `dn` key. We will retrieve this,
898 // and then set it on the model for accessibility.
899 if (Arr::exists($attributes, 'dn')) {
900 $this->dn = Arr::accessible($attributes['dn'])
901 ? Arr::first($attributes['dn'])
902 : $attributes['dn'];
903 }
904
905 $this->syncOriginal();
906
907 // Here we will set the exists attribute to true,
908 // since raw attributes are only set in the case
909 // of attributes being loaded by query results.
910 $this->exists = true;
911
912 return $this;
913 }
914
915 /**
916 * Filters the count key recursively from raw LDAP attributes.
917 *
918 * @param array $attributes
919 * @param array $keys
920 *
921 * @return array
922 */
923 public function filterRawAttributes(array $attributes = [], array $keys = ['count', 'dn'])
924 {
925 foreach ($keys as $key) {
926 unset($attributes[$key]);
927 }
928
929 foreach ($attributes as $key => $value) {
930 $attributes[$key] = is_array($value)
931 ? $this->filterRawAttributes($value, $keys)
932 : $value;
933 }
934
935 return $attributes;
936 }
937
938 /**
939 * Determine if the model has the given attribute.
940 *
941 * @param int|string $key
942 *
943 * @return bool
944 */
945 public function hasAttribute($key)
946 {
947 return [] !== ($this->attributes[$this->normalizeAttributeKey($key)] ?? []);
948 }
949
950 /**
951 * Returns the number of attributes.
952 *
953 * @return int
954 */
955 public function countAttributes()
956 {
957 return count($this->getAttributes());
958 }
959
960 /**
961 * Returns the models original attributes.
962 *
963 * @return array
964 */
965 public function getOriginal()
966 {
967 return $this->original;
968 }
969
970 /**
971 * Get the attributes that have been changed since last sync.
972 *
973 * @return array
974 */
975 public function getDirty()
976 {
977 $dirty = [];
978
979 foreach ($this->attributes as $key => $value) {
980 if ($this->isDirty($key)) {
981 // We need to reset the array using array_values due to
982 // LDAP requiring consecutive indices (0, 1, 2 etc.).
983 // We would receive an exception otherwise.
984 $dirty[$key] = array_values($value);
985 }
986 }
987
988 return $dirty;
989 }
990
991 /**
992 * Determine if the given attribute is dirty.
993 *
994 * @param string $key
995 *
996 * @return bool
997 */
998 public function isDirty($key)
999 {
1000 return ! $this->originalIsEquivalent($key);
1001 }
1002
1003 /**
1004 * Get the accessors being appended to the models array form.
1005 *
1006 * @return array
1007 */
1008 public function getAppends()
1009 {
1010 return $this->appends;
1011 }
1012
1013 /**
1014 * Set the accessors to append to model arrays.
1015 *
1016 * @param array $appends
1017 *
1018 * @return $this
1019 */
1020 public function setAppends(array $appends)
1021 {
1022 $this->appends = $appends;
1023
1024 return $this;
1025 }
1026
1027 /**
1028 * Return whether the accessor attribute has been appended.
1029 *
1030 * @param string $attribute
1031 *
1032 * @return bool
1033 */
1034 public function hasAppended($attribute)
1035 {
1036 return in_array($attribute, $this->appends);
1037 }
1038
1039 /**
1040 * Returns a normalized attribute key.
1041 *
1042 * @param string $key
1043 *
1044 * @return string
1045 */
1046 public function normalizeAttributeKey($key)
1047 {
1048 // Since LDAP supports hyphens in attribute names,
1049 // we'll convert attributes being retrieved by
1050 // underscores into hyphens for convenience.
1051 return strtolower(
1052 str_replace('_', '-', $key)
1053 );
1054 }
1055
1056 /**
1057 * Determine if the new and old values for a given key are equivalent.
1058 *
1059 * @param string $key
1060 *
1061 * @return bool
1062 */
1063 protected function originalIsEquivalent($key)
1064 {
1065 if (! array_key_exists($key, $this->original)) {
1066 return false;
1067 }
1068
1069 $current = $this->attributes[$key];
1070 $original = $this->original[$key];
1071
1072 if ($current === $original) {
1073 return true;
1074 }
1075
1076 return is_numeric($current) &&
1077 is_numeric($original) &&
1078 strcmp((string) $current, (string) $original) === 0;
1079 }
1080
1081 /**
1082 * Get the mutated attributes for a given instance.
1083 *
1084 * @return array
1085 */
1086 public function getMutatedAttributes()
1087 {
1088 $class = static::class;
1089
1090 if (! isset(static::$mutatorCache[$class])) {
1091 static::cacheMutatedAttributes($class);
1092 }
1093
1094 return static::$mutatorCache[$class];
1095 }
1096
1097 /**
1098 * Extract and cache all the mutated attributes of a class.
1099 *
1100 * @param string $class
1101 *
1102 * @return void
1103 */
1104 public static function cacheMutatedAttributes($class)
1105 {
1106 static::$mutatorCache[$class] = collect(static::getMutatorMethods($class))->reject(function ($match) {
1107 return $match === 'First';
1108 })->map(function ($match) {
1109 return lcfirst($match);
1110 })->all();
1111 }
1112
1113 /**
1114 * Get all of the attribute mutator methods.
1115 *
1116 * @param mixed $class
1117 *
1118 * @return array
1119 */
1120 protected static function getMutatorMethods($class)
1121 {
1122 preg_match_all('/(?<=^|;)get([^;]+?)Attribute(;|$)/', implode(';', get_class_methods($class)), $matches);
1123
1124 return $matches[1];
1125 }
1126}