Passed
Pull Request — master (#88)
by Jasper
03:24
created

HasAttributes::getAttributes()   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\Concerns;
4
5
use Illuminate\Contracts\Support\Arrayable;
6
use Swis\JsonApi\Client\Collection;
7
use Swis\JsonApi\Client\Util;
8
9
trait HasAttributes
10
{
11
    /**
12
     * The model's attributes.
13
     *
14
     * @var array
15
     */
16
    protected $attributes = [];
17
18
    /**
19
     * The attributes that should be cast.
20
     *
21
     * @var array
22
     */
23
    protected $casts = [];
24
25
    /**
26
     * The accessors to append to the model's array form.
27
     *
28
     * @var array
29
     */
30
    protected $appends = [];
31
32
    /**
33
     * Indicates whether attributes are snake cased on arrays.
34
     *
35
     * @var bool
36
     */
37
    public static $snakeAttributes = true;
38
39
    /**
40
     * The cache of the mutated attributes for each class.
41
     *
42
     * @var array
43
     */
44
    protected static $mutatorCache = [];
45
46
    /**
47
     * Convert the model's attributes to an array.
48
     *
49
     * @return array
50
     */
51
    public function attributesToArray()
52
    {
53
        $attributes = $this->getArrayableAttributes();
54
55
        $attributes = $this->addMutatedAttributesToArray(
56
            $attributes, $mutatedAttributes = $this->getMutatedAttributes()
57
        );
58
59
        // Next we will handle any casts that have been setup for this model and cast
60
        // the values to their appropriate type. If the attribute has a mutator we
61
        // will not perform the cast on those attributes to avoid any confusion.
62
        $attributes = $this->addCastAttributesToArray(
63
            $attributes, $mutatedAttributes
64
        );
65
66
        // Here we will grab all of the appended, calculated attributes to this model
67
        // as these attributes are not really in the attributes array, but are run
68
        // when we need to array or JSON the model for convenience to the coder.
69
        foreach ($this->getArrayableAppends() as $key) {
70
            $attributes[$key] = $this->mutateAttributeForArray($key, null);
71
        }
72
73
        return $attributes;
74
    }
75
76
    /**
77
     * Add the mutated attributes to the attributes array.
78
     *
79
     * @param array $attributes
80
     * @param array $mutatedAttributes
81
     *
82
     * @return array
83
     */
84
    protected function addMutatedAttributesToArray(array $attributes, array $mutatedAttributes)
85
    {
86
        foreach ($mutatedAttributes as $key) {
87
            // We want to spin through all the mutated attributes for this model and call
88
            // the mutator for the attribute. We cache off every mutated attributes so
89
            // we don't have to constantly check on attributes that actually change.
90
            if (!array_key_exists($key, $attributes)) {
91
                continue;
92
            }
93
94
            // Next, we will call the mutator for this attribute so that we can get these
95
            // mutated attribute's actual values. After we finish mutating each of the
96
            // attributes we will return this final array of the mutated attributes.
97
            $attributes[$key] = $this->mutateAttributeForArray(
98
                $key, $attributes[$key]
99
            );
100
        }
101
102
        return $attributes;
103
    }
104
105
    /**
106
     * Add the casted attributes to the attributes array.
107
     *
108
     * @param array $attributes
109
     * @param array $mutatedAttributes
110
     *
111
     * @return array
112
     */
113
    protected function addCastAttributesToArray(array $attributes, array $mutatedAttributes)
114
    {
115
        foreach ($this->getCasts() as $key => $value) {
116
            if (!array_key_exists($key, $attributes) ||
117
                in_array($key, $mutatedAttributes)) {
118
                continue;
119
            }
120
121
            // Next we will handle any casts that have been setup for this model and cast
122
            // the values to their appropriate type. If the attribute has a mutator we
123
            // will not perform the cast on those attributes to avoid any confusion.
124
            $attributes[$key] = $this->castAttribute(
125
                $key, $attributes[$key]
126
            );
127
128
            if ($attributes[$key] instanceof Arrayable) {
129
                $attributes[$key] = $attributes[$key]->toArray();
130
            }
131
        }
132
133
        return $attributes;
134
    }
135
136
    /**
137
     * Get an attribute array of all arrayable attributes.
138
     *
139
     * @return array
140
     */
141
    protected function getArrayableAttributes()
142
    {
143
        return $this->getArrayableItems($this->getAttributes());
144
    }
145
146
    /**
147
     * Get all of the appendable values that are arrayable.
148
     *
149
     * @return array
150
     */
151
    protected function getArrayableAppends()
152
    {
153
        if (!count($this->appends)) {
154
            return [];
155
        }
156
157
        return $this->getArrayableItems(
158
            array_combine($this->appends, $this->appends)
159
        );
160
    }
161
162
    /**
163
     * Get an attribute array of all arrayable values.
164
     *
165
     * @param array $values
166
     *
167
     * @return array
168
     */
169
    protected function getArrayableItems(array $values)
170
    {
171
        if (count($this->getVisible()) > 0) {
0 ignored issues
show
Bug introduced by
It seems like getVisible() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

171
        if (count($this->/** @scrutinizer ignore-call */ getVisible()) > 0) {
Loading history...
172
            $values = array_intersect_key($values, array_flip($this->getVisible()));
173
        }
174
175
        if (count($this->getHidden()) > 0) {
0 ignored issues
show
Bug introduced by
It seems like getHidden() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

175
        if (count($this->/** @scrutinizer ignore-call */ getHidden()) > 0) {
Loading history...
176
            $values = array_diff_key($values, array_flip($this->getHidden()));
177
        }
178
179
        return $values;
180
    }
181
182
    /**
183
     * Get an attribute from the model.
184
     *
185
     * @param string $key
186
     *
187
     * @return mixed
188
     */
189
    public function getAttribute($key)
190
    {
191
        if (!$key) {
192
            return;
193
        }
194
195
        // If the attribute exists in the attribute array or has a "get" mutator we will
196
        // get the attribute's value. Otherwise, we will proceed as if the developers
197
        // are asking for a relationship's value. This covers both types of values.
198
        if (array_key_exists($key, $this->attributes) ||
199
            array_key_exists($key, $this->casts) ||
200
            $this->hasGetMutator($key)) {
201
            return $this->getAttributeValue($key);
202
        }
203
204
        // Here we will determine if the model base class itself contains this given key
205
        // since we don't want to treat any of those methods as relationships because
206
        // they are all intended as helper methods and none of these are relations.
207
        if (method_exists(self::class, $key)) {
208
            return;
209
        }
210
211
        return $this->getRelationValue($key);
0 ignored issues
show
Bug introduced by
It seems like getRelationValue() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

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

211
        return $this->/** @scrutinizer ignore-call */ getRelationValue($key);
Loading history...
212
    }
213
214
    /**
215
     * Get a plain attribute (not a relationship).
216
     *
217
     * @param string $key
218
     *
219
     * @return mixed
220
     */
221
    public function getAttributeValue($key)
222
    {
223
        return $this->transformModelValue($key, $this->getAttributeFromArray($key));
224
    }
225
226
    /**
227
     * Get an attribute from the $attributes array.
228
     *
229
     * @param string $key
230
     *
231
     * @return mixed
232
     */
233
    protected function getAttributeFromArray(string $key)
234
    {
235
        return $this->getAttributes()[$key] ?? null;
236
    }
237
238
    /**
239
     * Determine if a get mutator exists for an attribute.
240
     *
241
     * @param string $key
242
     *
243
     * @return bool
244
     */
245
    public function hasGetMutator($key)
246
    {
247
        return method_exists($this, 'get'.Util::stringStudly($key).'Attribute');
248
    }
249
250
    /**
251
     * Get the value of an attribute using its mutator.
252
     *
253
     * @param string $key
254
     * @param mixed  $value
255
     *
256
     * @return mixed
257
     */
258
    protected function mutateAttribute($key, $value)
259
    {
260
        return $this->{'get'.Util::stringStudly($key).'Attribute'}($value);
261
    }
262
263
    /**
264
     * Get the value of an attribute using its mutator for array conversion.
265
     *
266
     * @param string $key
267
     * @param mixed  $value
268
     *
269
     * @return mixed
270
     */
271
    protected function mutateAttributeForArray($key, $value)
272
    {
273
        $value = $this->mutateAttribute($key, $value);
274
275
        return $value instanceof Arrayable ? $value->toArray() : $value;
276
    }
277
278
    /**
279
     * Merge new casts with existing casts on the model.
280
     *
281
     * @param array $casts
282
     *
283
     * @return $this
284
     */
285
    public function mergeCasts(array $casts)
286
    {
287
        $this->casts = array_merge($this->casts, $casts);
288
289
        return $this;
290
    }
291
292
    /**
293
     * Cast an attribute to a native PHP type.
294
     *
295
     * @param string $key
296
     * @param mixed  $value
297
     *
298
     * @return mixed
299
     */
300
    protected function castAttribute($key, $value)
301
    {
302
        if (is_null($value)) {
303
            return $value;
304
        }
305
306
        switch ($this->getCastType($key)) {
307
            case 'int':
308
            case 'integer':
309
                return (int) $value;
310
            case 'real':
311
            case 'float':
312
            case 'double':
313
                return $this->fromFloat($value);
314
            case 'string':
315
                return (string) $value;
316
            case 'bool':
317
            case 'boolean':
318
                return (bool) $value;
319
            case 'object':
320
                return $this->fromJson($value, true);
321
            case 'array':
322
            case 'json':
323
                return $this->fromJson($value);
324
            case 'collection':
325
                return new Collection($this->fromJson($value));
326
        }
327
328
        return $value;
329
    }
330
331
    /**
332
     * Get the type of cast for a model attribute.
333
     *
334
     * @param string $key
335
     *
336
     * @return string
337
     */
338
    protected function getCastType($key)
339
    {
340
        return trim(strtolower($this->getCasts()[$key]));
341
    }
342
343
    /**
344
     * Set a given attribute on the model.
345
     *
346
     * @param string $key
347
     * @param mixed  $value
348
     *
349
     * @return mixed
350
     */
351
    public function setAttribute($key, $value)
352
    {
353
        // First we will check for the presence of a mutator for the set operation
354
        // which simply lets the developers tweak the attribute as it is set on
355
        // the model, such as "json_encoding" an listing of data for storage.
356
        if ($this->hasSetMutator($key)) {
357
            return $this->setMutatedAttributeValue($key, $value);
358
        }
359
360
        if (!is_null($value) && $this->isJsonCastable($key)) {
361
            $value = $this->asJson($value);
362
        }
363
364
        $this->attributes[$key] = $value;
365
366
        return $this;
367
    }
368
369
    /**
370
     * Determine if a set mutator exists for an attribute.
371
     *
372
     * @param string $key
373
     *
374
     * @return bool
375
     */
376
    public function hasSetMutator($key)
377
    {
378
        return method_exists($this, 'set'.Util::stringStudly($key).'Attribute');
379
    }
380
381
    /**
382
     * Set the value of an attribute using its mutator.
383
     *
384
     * @param string $key
385
     * @param mixed  $value
386
     *
387
     * @return mixed
388
     */
389
    protected function setMutatedAttributeValue($key, $value)
390
    {
391
        return $this->{'set'.Util::stringStudly($key).'Attribute'}($value);
392
    }
393
394
    /**
395
     * Encode the given value as JSON.
396
     *
397
     * @param mixed $value
398
     *
399
     * @return string
400
     */
401
    protected function asJson($value)
402
    {
403
        return json_encode($value);
404
    }
405
406
    /**
407
     * Decode the given JSON back into an array or object.
408
     *
409
     * @param string $value
410
     * @param bool   $asObject
411
     *
412
     * @return mixed
413
     */
414
    public function fromJson(string $value, $asObject = false)
415
    {
416
        return json_decode($value, !$asObject);
417
    }
418
419
    /**
420
     * Decode the given float.
421
     *
422
     * @param mixed $value
423
     *
424
     * @return mixed
425
     */
426
    public function fromFloat($value)
427
    {
428
        switch ((string) $value) {
429
            case 'Infinity':
430
                return INF;
431
            case '-Infinity':
432
                return -INF;
433
            case 'NaN':
434
                return NAN;
435
            default:
436
                return (float) $value;
437
        }
438
    }
439
440
    /**
441
     * Determine whether an attribute should be cast to a native type.
442
     *
443
     * @param string            $key
444
     * @param array|string|null $types
445
     *
446
     * @return bool
447
     */
448
    public function hasCast($key, $types = null)
449
    {
450
        if (array_key_exists($key, $this->getCasts())) {
451
            return $types ? in_array($this->getCastType($key), (array) $types, true) : true;
452
        }
453
454
        return false;
455
    }
456
457
    /**
458
     * Get the casts array.
459
     *
460
     * @return array
461
     */
462
    public function getCasts()
463
    {
464
        return $this->casts;
465
    }
466
467
    /**
468
     * Determine whether a value is JSON castable for inbound manipulation.
469
     *
470
     * @param string $key
471
     *
472
     * @return bool
473
     */
474
    protected function isJsonCastable($key)
475
    {
476
        return $this->hasCast($key, ['array', 'json', 'object', 'collection']);
477
    }
478
479
    /**
480
     * Get all of the current attributes on the model.
481
     *
482
     * @return array
483
     */
484
    public function getAttributes()
485
    {
486
        return $this->attributes;
487
    }
488
489
    /**
490
     * Get a subset of the model's attributes.
491
     *
492
     * @param array|mixed $attributes
493
     *
494
     * @return array
495
     */
496
    public function only($attributes)
497
    {
498
        $results = [];
499
500
        foreach (is_array($attributes) ? $attributes : func_get_args() as $attribute) {
501
            $results[$attribute] = $this->getAttribute($attribute);
502
        }
503
504
        return $results;
505
    }
506
507
    /**
508
     * Transform a raw model value using mutators, casts, etc.
509
     *
510
     * @param string $key
511
     * @param mixed  $value
512
     *
513
     * @return mixed
514
     */
515
    protected function transformModelValue($key, $value)
516
    {
517
        // If the attribute has a get mutator, we will call that then return what
518
        // it returns as the value, which is useful for transforming values on
519
        // retrieval from the model to a form that is more useful for usage.
520
        if ($this->hasGetMutator($key)) {
521
            return $this->mutateAttribute($key, $value);
522
        }
523
524
        // If the attribute exists within the cast array, we will convert it to
525
        // an appropriate native PHP type dependent upon the associated value
526
        // given with the key in the pair. Dayle made this comment line up.
527
        if ($this->hasCast($key)) {
528
            return $this->castAttribute($key, $value);
529
        }
530
531
        return $value;
532
    }
533
534
    /**
535
     * Set the accessors to append to model arrays.
536
     *
537
     * @param array $appends
538
     *
539
     * @return $this
540
     */
541
    public function setAppends(array $appends)
542
    {
543
        $this->appends = $appends;
544
545
        return $this;
546
    }
547
548
    /**
549
     * Add the accessors to append to model arrays.
550
     *
551
     * @param array $attributes
552
     *
553
     * @return $this
554
     */
555
    public function mergeAppends($attributes)
556
    {
557
        $this->appends = array_merge($this->appends, $attributes);
558
559
        return $this;
560
    }
561
562
    /**
563
     * Return whether the accessor attribute has been appended.
564
     *
565
     * @param string $attribute
566
     *
567
     * @return bool
568
     */
569
    public function hasAppended($attribute)
570
    {
571
        return in_array($attribute, $this->appends);
572
    }
573
574
    /**
575
     * Get the mutated attributes for a given instance.
576
     *
577
     * @return array
578
     */
579
    public function getMutatedAttributes()
580
    {
581
        $class = static::class;
582
583
        if (!isset(static::$mutatorCache[$class])) {
584
            static::cacheMutatedAttributes($class);
585
        }
586
587
        return static::$mutatorCache[$class];
588
    }
589
590
    /**
591
     * Extract and cache all the mutated attributes of a class.
592
     *
593
     * @param string $class
594
     *
595
     * @return void
596
     */
597
    public static function cacheMutatedAttributes($class)
598
    {
599
        $mutatedAttributes = [];
600
601
        // Here we will extract all of the mutated attributes so that we can quickly
602
        // spin through them after we export models to their array form, which we
603
        // need to be fast. This'll let us know the attributes that can mutate.
604
        foreach (static::getMutatorMethods($class) as $match) {
605
            $mutatedAttributes[] = lcfirst(static::$snakeAttributes ? Util::stringSnake($match) : $match);
606
        }
607
608
        static::$mutatorCache[$class] = $mutatedAttributes;
609
    }
610
611
    /**
612
     * Get all of the attribute mutator methods.
613
     *
614
     * @param mixed $class
615
     *
616
     * @return array
617
     */
618
    protected static function getMutatorMethods($class)
619
    {
620
        preg_match_all('/(?<=^|;)get([^;]+?)Attribute(;|$)/', implode(';', get_class_methods($class)), $matches);
621
622
        return $matches[1];
623
    }
624
}
625