Passed
Pull Request — master (#87)
by Bjorn
10:22
created

Model::__toString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

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