Completed
Push — master ( 533fff...cf886a )
by Bjorn
16s queued 13s
created

Model::getArrayableItems()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 7
rs 10
1
<?php
2
3
namespace Swis\JsonApi\Client;
4
5
use ArrayAccess;
6
use Illuminate\Contracts\Support\Arrayable;
7
use Illuminate\Contracts\Support\Jsonable;
8
use JsonSerializable;
9
use Swis\JsonApi\Client\Exceptions\MassAssignmentException;
10
11
abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializable
12
{
13
    /**
14
     * The model's attributes.
15
     *
16
     * @var array
17
     */
18
    protected $attributes = [];
19
20
    /**
21
     * The attributes that should be hidden for arrays.
22
     *
23
     * @var array
24
     */
25
    protected $hidden = [];
26
27
    /**
28
     * The attributes that should be visible in arrays.
29
     *
30
     * @var array
31
     */
32
    protected $visible = [];
33
34
    /**
35
     * The accessors to append to the model's array form.
36
     *
37
     * @var array
38
     */
39
    protected $appends = [];
40
41
    /**
42
     * The attributes that are mass assignable.
43
     *
44
     * @var array
45
     */
46
    protected $fillable = [];
47
48
    /**
49
     * The attributes that aren't mass assignable.
50
     *
51
     * @var array
52
     */
53
    protected $guarded = [];
54
55
    /**
56
     * The attributes that should be casted to native types.
57
     *
58
     * @var array
59
     */
60
    protected $casts = [];
61
62
    /**
63
     * Indicates whether attributes are snake cased on arrays.
64
     *
65
     * @var bool
66
     */
67
    public static $snakeAttributes = true;
68
69
    /**
70
     * Indicates if all mass assignment is enabled.
71
     *
72
     * @var bool
73
     */
74
    protected static $unguarded = false;
75
76
    /**
77
     * The cache of the mutated attributes for each class.
78
     *
79
     * @var array
80
     */
81
    protected static $mutatorCache = [];
82
83
    /**
84
     * Create a new Eloquent model instance.
85
     *
86
     * @param array $attributes
87
     *
88
     * @return void
89
     */
90
    public function __construct(array $attributes = [])
91
    {
92
        $this->fill($attributes);
93
    }
94
95
    /**
96
     * Fill the model with an array of attributes.
97
     *
98
     * @param array $attributes
99
     *
100
     * @throws \Swis\JsonApi\Client\Exceptions\MassAssignmentException
101
     *
102
     * @return $this
103
     */
104
    public function fill(array $attributes)
105
    {
106
        $totallyGuarded = $this->totallyGuarded();
107
108
        foreach ($this->fillableFromArray($attributes) as $key => $value) {
109
            // The developers may choose to place some attributes in the "fillable"
110
            // array, which means only those attributes may be set through mass
111
            // assignment to the model, and all others will just be ignored.
112
            if ($this->isFillable($key)) {
113
                $this->setAttribute($key, $value);
114
            } elseif ($totallyGuarded) {
115
                throw new MassAssignmentException($key);
116
            }
117
        }
118
119
        return $this;
120
    }
121
122
    /**
123
     * Fill the model with an array of attributes. Force mass assignment.
124
     *
125
     * @param array $attributes
126
     *
127
     * @return $this
128
     */
129
    public function forceFill(array $attributes)
130
    {
131
        // Since some versions of PHP have a bug that prevents it from properly
132
        // binding the late static context in a closure, we will first store
133
        // the model in a variable, which we will then use in the closure.
134
        $model = $this;
135
136
        return static::unguarded(function () use ($model, $attributes) {
137
            return $model->fill($attributes);
138
        });
139
    }
140
141
    /**
142
     * Get the fillable attributes of a given array.
143
     *
144
     * @param array $attributes
145
     *
146
     * @return array
147
     */
148
    protected function fillableFromArray(array $attributes)
149
    {
150
        if (count($this->fillable) > 0 && !static::$unguarded) {
151
            return array_intersect_key($attributes, array_flip($this->fillable));
152
        }
153
154
        return $attributes;
155
    }
156
157
    /**
158
     * Create a new instance of the given model.
159
     *
160
     * @param array $attributes
161
     *
162
     * @return Model
163
     */
164
    public function newInstance(array $attributes = [])
165
    {
166
        return new static((array) $attributes);
167
    }
168
169
    /**
170
     * Create a collection of models from plain arrays.
171
     *
172
     * @param array $items
173
     *
174
     * @return array
175
     */
176
    public static function hydrate(array $items)
177
    {
178
        $instance = new static();
179
180
        $items = array_map(function ($item) use ($instance) {
181
            return $instance->newInstance($item);
182
        }, $items);
183
184
        return $items;
185
    }
186
187
    /**
188
     * Get the hidden attributes for the model.
189
     *
190
     * @return array
191
     */
192
    public function getHidden()
193
    {
194
        return $this->hidden;
195
    }
196
197
    /**
198
     * Set the hidden attributes for the model.
199
     *
200
     * @param array $hidden
201
     *
202
     * @return $this
203
     */
204
    public function setHidden(array $hidden)
205
    {
206
        $this->hidden = $hidden;
207
208
        return $this;
209
    }
210
211
    /**
212
     * Add hidden attributes for the model.
213
     *
214
     * @param array|string|null $attributes
215
     *
216
     * @return void
217
     */
218
    public function addHidden($attributes = null)
219
    {
220
        $attributes = is_array($attributes) ? $attributes : func_get_args();
221
222
        $this->hidden = array_merge($this->hidden, $attributes);
223
    }
224
225
    /**
226
     * Make the given, typically hidden, attributes visible.
227
     *
228
     * @param array|string $attributes
229
     *
230
     * @return $this
231
     */
232
    public function withHidden($attributes)
233
    {
234
        $this->hidden = array_diff($this->hidden, (array) $attributes);
235
236
        return $this;
237
    }
238
239
    /**
240
     * Get the visible attributes for the model.
241
     *
242
     * @return array
243
     */
244
    public function getVisible()
245
    {
246
        return $this->visible;
247
    }
248
249
    /**
250
     * Set the visible attributes for the model.
251
     *
252
     * @param array $visible
253
     *
254
     * @return $this
255
     */
256
    public function setVisible(array $visible)
257
    {
258
        $this->visible = $visible;
259
260
        return $this;
261
    }
262
263
    /**
264
     * Add visible attributes for the model.
265
     *
266
     * @param array|string|null $attributes
267
     *
268
     * @return void
269
     */
270
    public function addVisible($attributes = null)
271
    {
272
        $attributes = is_array($attributes) ? $attributes : func_get_args();
273
274
        $this->visible = array_merge($this->visible, $attributes);
275
    }
276
277
    /**
278
     * Set the accessors to append to model arrays.
279
     *
280
     * @param array $appends
281
     *
282
     * @return $this
283
     */
284
    public function setAppends(array $appends)
285
    {
286
        $this->appends = $appends;
287
288
        return $this;
289
    }
290
291
    /**
292
     * Get the fillable attributes for the model.
293
     *
294
     * @return array
295
     */
296
    public function getFillable()
297
    {
298
        return $this->fillable;
299
    }
300
301
    /**
302
     * Set the fillable attributes for the model.
303
     *
304
     * @param array $fillable
305
     *
306
     * @return $this
307
     */
308
    public function fillable(array $fillable)
309
    {
310
        $this->fillable = $fillable;
311
312
        return $this;
313
    }
314
315
    /**
316
     * Get the guarded attributes for the model.
317
     *
318
     * @return array
319
     */
320
    public function getGuarded()
321
    {
322
        return $this->guarded;
323
    }
324
325
    /**
326
     * Set the guarded attributes for the model.
327
     *
328
     * @param array $guarded
329
     *
330
     * @return $this
331
     */
332
    public function guard(array $guarded)
333
    {
334
        $this->guarded = $guarded;
335
336
        return $this;
337
    }
338
339
    /**
340
     * Disable all mass assignable restrictions.
341
     *
342
     * @param bool $state
343
     *
344
     * @return void
345
     */
346
    public static function unguard(bool $state = true)
347
    {
348
        static::$unguarded = $state;
349
    }
350
351
    /**
352
     * Enable the mass assignment restrictions.
353
     *
354
     * @return void
355
     */
356
    public static function reguard()
357
    {
358
        static::$unguarded = false;
359
    }
360
361
    /**
362
     * Determine if current state is "unguarded".
363
     *
364
     * @return bool
365
     */
366
    public static function isUnguarded()
367
    {
368
        return static::$unguarded;
369
    }
370
371
    /**
372
     * Run the given callable while being unguarded.
373
     *
374
     * @param callable $callback
375
     *
376
     * @return mixed
377
     */
378
    public static function unguarded(callable $callback)
379
    {
380
        if (static::$unguarded) {
381
            return $callback();
382
        }
383
384
        static::unguard();
385
386
        $result = $callback();
387
388
        static::reguard();
389
390
        return $result;
391
    }
392
393
    /**
394
     * Determine if the given attribute may be mass assigned.
395
     *
396
     * @param string $key
397
     *
398
     * @return bool
399
     */
400
    public function isFillable(string $key)
401
    {
402
        if (static::$unguarded) {
403
            return true;
404
        }
405
406
        // If the key is in the "fillable" array, we can of course assume that it's
407
        // a fillable attribute. Otherwise, we will check the guarded array when
408
        // we need to determine if the attribute is black-listed on the model.
409
        if (in_array($key, $this->fillable)) {
410
            return true;
411
        }
412
413
        if ($this->isGuarded($key)) {
414
            return false;
415
        }
416
417
        return empty($this->fillable);
418
    }
419
420
    /**
421
     * Determine if the given key is guarded.
422
     *
423
     * @param string $key
424
     *
425
     * @return bool
426
     */
427
    public function isGuarded(string $key)
428
    {
429
        return in_array($key, $this->guarded) || $this->guarded == ['*'];
430
    }
431
432
    /**
433
     * Determine if the model is totally guarded.
434
     *
435
     * @return bool
436
     */
437
    public function totallyGuarded()
438
    {
439
        return count($this->fillable) == 0 && $this->guarded == ['*'];
440
    }
441
442
    /**
443
     * Convert the model instance to JSON.
444
     *
445
     * @param int $options
446
     *
447
     * @return string
448
     */
449
    public function toJson($options = 0)
450
    {
451
        return json_encode($this->jsonSerialize(), $options);
452
    }
453
454
    /**
455
     * Convert the object into something JSON serializable.
456
     *
457
     * @return array
458
     */
459
    public function jsonSerialize()
460
    {
461
        return $this->toArray();
462
    }
463
464
    /**
465
     * Convert the model instance to an array.
466
     *
467
     * @return array
468
     */
469
    public function toArray()
470
    {
471
        return $this->attributesToArray();
472
    }
473
474
    /**
475
     * Convert the model's attributes to an array.
476
     *
477
     * @return array
478
     */
479
    public function attributesToArray()
480
    {
481
        $attributes = $this->getArrayableAttributes();
482
483
        $mutatedAttributes = $this->getMutatedAttributes();
484
485
        // We want to spin through all the mutated attributes for this model and call
486
        // the mutator for the attribute. We cache off every mutated attributes so
487
        // we don't have to constantly check on attributes that actually change.
488
        foreach ($mutatedAttributes as $key) {
489
            if (!array_key_exists($key, $attributes)) {
490
                continue;
491
            }
492
493
            $attributes[$key] = $this->mutateAttributeForArray(
494
        $key, $attributes[$key]
495
      );
496
        }
497
498
        // Next we will handle any casts that have been setup for this model and cast
499
        // the values to their appropriate type. If the attribute has a mutator we
500
        // will not perform the cast on those attributes to avoid any confusion.
501
        foreach ($this->casts as $key => $value) {
502
            if (!array_key_exists($key, $attributes) ||
503
        in_array($key, $mutatedAttributes)) {
504
                continue;
505
            }
506
507
            $attributes[$key] = $this->castAttribute(
508
        $key, $attributes[$key]
509
      );
510
        }
511
512
        // Here we will grab all of the appended, calculated attributes to this model
513
        // as these attributes are not really in the attributes array, but are run
514
        // when we need to array or JSON the model for convenience to the coder.
515
        foreach ($this->getArrayableAppends() as $key) {
516
            $attributes[$key] = $this->mutateAttributeForArray($key, null);
517
        }
518
519
        return $attributes;
520
    }
521
522
    /**
523
     * Get an attribute array of all arrayable attributes.
524
     *
525
     * @return array
526
     */
527
    protected function getArrayableAttributes()
528
    {
529
        return $this->getArrayableItems($this->attributes);
530
    }
531
532
    /**
533
     * Get all of the appendable values that are arrayable.
534
     *
535
     * @return array
536
     */
537
    protected function getArrayableAppends()
538
    {
539
        if (!count($this->appends)) {
540
            return [];
541
        }
542
543
        return $this->getArrayableItems(
544
      array_combine($this->appends, $this->appends)
0 ignored issues
show
Bug introduced by
It seems like array_combine($this->appends, $this->appends) can also be of type false; however, parameter $values of Swis\JsonApi\Client\Model::getArrayableItems() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

544
      /** @scrutinizer ignore-type */ array_combine($this->appends, $this->appends)
Loading history...
545
    );
546
    }
547
548
    /**
549
     * Get an attribute array of all arrayable values.
550
     *
551
     * @param array $values
552
     *
553
     * @return array
554
     */
555
    protected function getArrayableItems(array $values)
556
    {
557
        if (count($this->getVisible()) > 0) {
558
            return array_intersect_key($values, array_flip($this->getVisible()));
559
        }
560
561
        return array_diff_key($values, array_flip($this->getHidden()));
562
    }
563
564
    /**
565
     * Get an attribute from the model.
566
     *
567
     * @param string $key
568
     *
569
     * @return mixed
570
     */
571
    public function getAttribute(string $key)
572
    {
573
        return $this->getAttributeValue($key);
574
    }
575
576
    /**
577
     * Get a plain attribute (not a relationship).
578
     *
579
     * @param string $key
580
     *
581
     * @return mixed
582
     */
583
    protected function getAttributeValue(string $key)
584
    {
585
        $value = $this->getAttributeFromArray($key);
586
587
        // If the attribute has a get mutator, we will call that then return what
588
        // it returns as the value, which is useful for transforming values on
589
        // retrieval from the model to a form that is more useful for usage.
590
        if ($this->hasGetMutator($key)) {
591
            return $this->mutateAttribute($key, $value);
592
        }
593
594
        // If the attribute exists within the cast array, we will convert it to
595
        // an appropriate native PHP type dependant upon the associated value
596
        // given with the key in the pair. Dayle made this comment line up.
597
        if ($this->hasCast($key)) {
598
            $value = $this->castAttribute($key, $value);
599
        }
600
601
        return $value;
602
    }
603
604
    /**
605
     * Get an attribute from the $attributes array.
606
     *
607
     * @param string $key
608
     *
609
     * @return mixed
610
     */
611
    protected function getAttributeFromArray(string $key)
612
    {
613
        if (array_key_exists($key, $this->attributes)) {
614
            return $this->attributes[$key];
615
        }
616
    }
617
618
    /**
619
     * Determine if a get mutator exists for an attribute.
620
     *
621
     * @param string $key
622
     *
623
     * @return bool
624
     */
625
    public function hasGetMutator(string $key)
626
    {
627
        return method_exists($this, 'get'.Util::stringStudly($key).'Attribute');
628
    }
629
630
    /**
631
     * Get the value of an attribute using its mutator.
632
     *
633
     * @param string $key
634
     * @param mixed  $value
635
     *
636
     * @return mixed
637
     */
638
    protected function mutateAttribute(string $key, $value)
639
    {
640
        return $this->{'get'.Util::stringStudly($key).'Attribute'}($value);
641
    }
642
643
    /**
644
     * Get the value of an attribute using its mutator for array conversion.
645
     *
646
     * @param string $key
647
     * @param mixed  $value
648
     *
649
     * @return mixed
650
     */
651
    protected function mutateAttributeForArray(string $key, $value)
652
    {
653
        $value = $this->mutateAttribute($key, $value);
654
655
        return $value instanceof Arrayable ? $value->toArray() : $value;
656
    }
657
658
    /**
659
     * Determine whether an attribute should be casted to a native type.
660
     *
661
     * @param string $key
662
     *
663
     * @return bool
664
     */
665
    protected function hasCast(string $key)
666
    {
667
        return array_key_exists($key, $this->casts);
668
    }
669
670
    /**
671
     * Determine whether a value is JSON castable for inbound manipulation.
672
     *
673
     * @param string $key
674
     *
675
     * @return bool
676
     */
677
    protected function isJsonCastable(string $key)
678
    {
679
        $castables = ['array', 'json', 'object', 'collection'];
680
681
        return $this->hasCast($key) && in_array($this->getCastType($key), $castables, true);
682
    }
683
684
    /**
685
     * Get the type of cast for a model attribute.
686
     *
687
     * @param string $key
688
     *
689
     * @return string
690
     */
691
    protected function getCastType(string $key)
692
    {
693
        return trim(strtolower($this->casts[$key]));
694
    }
695
696
    /**
697
     * Cast an attribute to a native PHP type.
698
     *
699
     * @param string $key
700
     * @param mixed  $value
701
     *
702
     * @return mixed
703
     */
704
    protected function castAttribute(string $key, $value)
705
    {
706
        if (is_null($value)) {
707
            return $value;
708
        }
709
710
        switch ($this->getCastType($key)) {
711
      case 'int':
712
      case 'integer':
713
        return (int) $value;
714
      case 'real':
715
      case 'float':
716
      case 'double':
717
        return (float) $value;
718
      case 'string':
719
        return (string) $value;
720
      case 'bool':
721
      case 'boolean':
722
        return (bool) $value;
723
      case 'object':
724
        return $this->fromJson($value, true);
725
      case 'array':
726
      case 'json':
727
        return $this->fromJson($value);
728
      case 'collection':
729
        return Collection::wrap($this->fromJson($value));
730
      default:
731
        return $value;
732
    }
733
    }
734
735
    /**
736
     * Set a given attribute on the model.
737
     *
738
     * @param string $key
739
     * @param mixed  $value
740
     *
741
     * @return $this
742
     */
743
    public function setAttribute($key, $value)
744
    {
745
        // First we will check for the presence of a mutator for the set operation
746
        // which simply lets the developers tweak the attribute as it is set on
747
        // the model, such as "json_encoding" an listing of data for storage.
748
        if ($this->hasSetMutator($key)) {
749
            $method = 'set'.Util::stringStudly($key).'Attribute';
750
751
            return $this->{$method}($value);
752
        }
753
754
        if ($this->isJsonCastable($key) && !is_null($value)) {
755
            $value = $this->asJson($value);
756
        }
757
758
        $this->attributes[$key] = $value;
759
760
        return $this;
761
    }
762
763
    /**
764
     * Determine if a set mutator exists for an attribute.
765
     *
766
     * @param string $key
767
     *
768
     * @return bool
769
     */
770
    public function hasSetMutator(string $key)
771
    {
772
        return method_exists($this, 'set'.Util::stringStudly($key).'Attribute');
773
    }
774
775
    /**
776
     * Encode the given value as JSON.
777
     *
778
     * @param mixed $value
779
     *
780
     * @return string
781
     */
782
    protected function asJson($value)
783
    {
784
        return json_encode($value);
785
    }
786
787
    /**
788
     * Decode the given JSON back into an array or object.
789
     *
790
     * @param string $value
791
     * @param bool   $asObject
792
     *
793
     * @return mixed
794
     */
795
    public function fromJson(string $value, $asObject = false)
796
    {
797
        return json_decode($value, !$asObject);
798
    }
799
800
    /**
801
     * Clone the model into a new, non-existing instance.
802
     *
803
     * @param array|null $except
804
     *
805
     * @return Model
806
     */
807
    public function replicate(array $except = null)
808
    {
809
        $except = $except ?: [];
810
811
        $attributes = array_diff_key($this->attributes, array_flip($except));
812
813
        return (new static())->fill($attributes);
814
    }
815
816
    /**
817
     * Get all of the current attributes on the model.
818
     *
819
     * @return array
820
     */
821
    public function getAttributes()
822
    {
823
        return $this->attributes;
824
    }
825
826
    /**
827
     * Get the mutated attributes for a given instance.
828
     *
829
     * @return array
830
     */
831
    public function getMutatedAttributes()
832
    {
833
        $class = get_class($this);
834
835
        if (!isset(static::$mutatorCache[$class])) {
836
            static::cacheMutatedAttributes($class);
837
        }
838
839
        return static::$mutatorCache[$class];
840
    }
841
842
    /**
843
     * Extract and cache all the mutated attributes of a class.
844
     *
845
     * @param string $class
846
     *
847
     * @return void
848
     */
849
    public static function cacheMutatedAttributes(string $class)
850
    {
851
        $mutatedAttributes = [];
852
853
        // Here we will extract all of the mutated attributes so that we can quickly
854
        // spin through them after we export models to their array form, which we
855
        // need to be fast. This'll let us know the attributes that can mutate.
856
        if (preg_match_all('/(?<=^|;)get([^;]+?)Attribute(;|$)/', implode(';', get_class_methods($class)), $matches)) {
857
            foreach ($matches[1] as $match) {
858
                if (static::$snakeAttributes) {
859
                    $match = Util::stringSnake($match);
860
                }
861
862
                $mutatedAttributes[] = lcfirst($match);
863
            }
864
        }
865
866
        static::$mutatorCache[$class] = $mutatedAttributes;
867
    }
868
869
    /**
870
     * Dynamically retrieve attributes on the model.
871
     *
872
     * @param string $key
873
     *
874
     * @return mixed
875
     */
876
    public function __get(string $key)
877
    {
878
        return $this->getAttribute($key);
879
    }
880
881
    /**
882
     * Dynamically set attributes on the model.
883
     *
884
     * @param string $key
885
     * @param mixed  $value
886
     *
887
     * @return void
888
     */
889
    public function __set(string $key, $value)
890
    {
891
        $this->setAttribute($key, $value);
892
    }
893
894
    /**
895
     * Determine if the given attribute exists.
896
     *
897
     * @param mixed $offset
898
     *
899
     * @return bool
900
     */
901
    public function offsetExists($offset)
902
    {
903
        return isset($this->$offset);
904
    }
905
906
    /**
907
     * Get the value for a given offset.
908
     *
909
     * @param mixed $offset
910
     *
911
     * @return mixed
912
     */
913
    public function offsetGet($offset)
914
    {
915
        return $this->$offset;
916
    }
917
918
    /**
919
     * Set the value for a given offset.
920
     *
921
     * @param mixed $offset
922
     * @param mixed $value
923
     *
924
     * @return void
925
     */
926
    public function offsetSet($offset, $value)
927
    {
928
        $this->$offset = $value;
929
    }
930
931
    /**
932
     * Unset the value for a given offset.
933
     *
934
     * @param mixed $offset
935
     *
936
     * @return void
937
     */
938
    public function offsetUnset($offset)
939
    {
940
        unset($this->$offset);
941
    }
942
943
    /**
944
     * Determine if an attribute exists on the model.
945
     *
946
     * @param string $key
947
     *
948
     * @return bool
949
     */
950
    public function __isset(string $key)
951
    {
952
        return (isset($this->attributes[$key]) || isset($this->relations[$key])) ||
0 ignored issues
show
Bug Best Practice introduced by
The property relations does not exist on Swis\JsonApi\Client\Model. Since you implemented __get, consider adding a @property annotation.
Loading history...
953
      ($this->hasGetMutator($key) && !is_null($this->getAttributeValue($key)));
954
    }
955
956
    /**
957
     * Unset an attribute on the model.
958
     *
959
     * @param string $key
960
     *
961
     * @return void
962
     */
963
    public function __unset(string $key)
964
    {
965
        unset($this->attributes[$key]);
966
    }
967
968
    /**
969
     * Handle dynamic static method calls into the method.
970
     *
971
     * @param string $method
972
     * @param array  $parameters
973
     *
974
     * @return mixed
975
     */
976
    public static function __callStatic(string $method, array $parameters)
977
    {
978
        $instance = new static();
979
980
        return call_user_func_array([$instance, $method], $parameters);
981
    }
982
983
    /**
984
     * Convert the model to its string representation.
985
     *
986
     * @return string
987
     */
988
    public function __toString()
989
    {
990
        return $this->toJson();
991
    }
992
}
993