HasAttributes   F
last analyzed

Complexity

Total Complexity 77

Size/Duplication

Total Lines 603
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 77
eloc 129
c 1
b 0
f 0
dl 0
loc 603
ccs 161
cts 161
cp 1
rs 2.24

33 Methods

Rating   Name   Duplication   Size   Complexity  
A cacheMutatedAttributes() 0 12 3
A hasGetMutator() 0 3 1
A mutateAttribute() 0 3 1
A hasCast() 0 7 3
A getAttributes() 0 3 1
A getAttributeFromArray() 0 3 1
A getCasts() 0 3 1
A getMutatorMethods() 0 5 1
A addMutatedAttributesToArray() 0 19 3
A mergeCasts() 0 5 1
A only() 0 9 3
A attributesToArray() 0 23 2
A getAttributeValue() 0 3 1
A hasSetMutator() 0 3 1
A setMutatedAttributeValue() 0 3 1
A getAttribute() 0 23 6
A getArrayableAppends() 0 8 2
A addCastAttributesToArray() 0 21 5
A getMutatedAttributes() 0 9 2
A mergeAppends() 0 5 1
A asJson() 0 3 1
A getArrayableItems() 0 11 3
A setAppends() 0 5 1
A isJsonCastable() 0 3 1
A mutateAttributeForArray() 0 5 2
A fromJson() 0 3 1
A getArrayableAttributes() 0 3 1
A setAttribute() 0 16 4
A transformModelValue() 0 17 3
A fromFloat() 0 11 4
C castAttribute() 0 29 14
A hasAppended() 0 3 1
A getCastType() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like HasAttributes often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use HasAttributes, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Swis\JsonApi\Client\Concerns;
6
7
use Illuminate\Contracts\Support\Arrayable;
8
use Swis\JsonApi\Client\Collection;
9
use Swis\JsonApi\Client\Interfaces\DataInterface;
10
use Swis\JsonApi\Client\Util;
11
12
trait HasAttributes
13
{
14
    /**
15
     * The model's attributes.
16
     *
17
     * @var array
18
     */
19
    protected $attributes = [];
20
21
    /**
22
     * The attributes that should be cast.
23
     *
24
     * @var array
25
     */
26
    protected $casts = [];
27
28
    /**
29
     * The accessors to append to the model's array form.
30
     *
31
     * @var array
32
     */
33
    protected $appends = [];
34
35
    /**
36
     * Indicates whether attributes are snake cased on arrays.
37
     *
38
     * @var bool
39
     */
40
    public static $snakeAttributes = true;
41
42
    /**
43
     * The cache of the mutated attributes for each class.
44
     *
45
     * @var array
46
     */
47
    protected static $mutatorCache = [];
48
49
    /**
50
     * Get the visible attributes for the model.
51
     *
52
     * @return array
53
     */
54
    abstract public function getVisible();
55
56
    /**
57
     * Get the visible attributes for the model.
58
     *
59
     * @return array
60
     */
61
    abstract public function getHidden();
62
63
    /**
64
     * Get the relationship data (included).
65
     */
66
    abstract public function getRelationValue(string $name): ?DataInterface;
67
68
    /**
69
     * Convert the model's attributes to an array.
70
     *
71
     * @return array
72
     */
73 188
    public function attributesToArray()
74
    {
75 188
        $attributes = $this->getArrayableAttributes();
76
77 188
        $attributes = $this->addMutatedAttributesToArray(
78 188
            $attributes, $mutatedAttributes = $this->getMutatedAttributes()
79 94
        );
80
81
        // Next we will handle any casts that have been setup for this model and cast
82
        // the values to their appropriate type. If the attribute has a mutator we
83
        // will not perform the cast on those attributes to avoid any confusion.
84 188
        $attributes = $this->addCastAttributesToArray(
85 188
            $attributes, $mutatedAttributes
86 94
        );
87
88
        // Here we will grab all of the appended, calculated attributes to this model
89
        // as these attributes are not really in the attributes array, but are run
90
        // when we need to array or JSON the model for convenience to the coder.
91 188
        foreach ($this->getArrayableAppends() as $key) {
92 4
            $attributes[$key] = $this->mutateAttributeForArray($key, null);
93
        }
94
95 188
        return $attributes;
96
    }
97
98
    /**
99
     * Add the mutated attributes to the attributes array.
100
     *
101
     *
102
     * @return array
103
     */
104 188
    protected function addMutatedAttributesToArray(array $attributes, array $mutatedAttributes)
105
    {
106 188
        foreach ($mutatedAttributes as $key) {
107
            // We want to spin through all the mutated attributes for this model and call
108
            // the mutator for the attribute. We cache off every mutated attributes so
109
            // we don't have to constantly check on attributes that actually change.
110 28
            if (! array_key_exists($key, $attributes)) {
111 28
                continue;
112
            }
113
114
            // Next, we will call the mutator for this attribute so that we can get these
115
            // mutated attribute's actual values. After we finish mutating each of the
116
            // attributes we will return this final array of the mutated attributes.
117 4
            $attributes[$key] = $this->mutateAttributeForArray(
118 4
                $key, $attributes[$key]
119 2
            );
120
        }
121
122 188
        return $attributes;
123
    }
124
125
    /**
126
     * Add the casted attributes to the attributes array.
127
     *
128
     *
129
     * @return array
130
     */
131 188
    protected function addCastAttributesToArray(array $attributes, array $mutatedAttributes)
132
    {
133 188
        foreach ($this->getCasts() as $key => $value) {
134 28
            if (! array_key_exists($key, $attributes)
135 28
                || in_array($key, $mutatedAttributes)) {
136 28
                continue;
137
            }
138
139
            // Next we will handle any casts that have been setup for this model and cast
140
            // the values to their appropriate type. If the attribute has a mutator we
141
            // will not perform the cast on those attributes to avoid any confusion.
142 8
            $attributes[$key] = $this->castAttribute(
143 8
                $key, $attributes[$key]
144 4
            );
145
146 8
            if ($attributes[$key] instanceof Arrayable) {
147 4
                $attributes[$key] = $attributes[$key]->toArray();
148
            }
149
        }
150
151 188
        return $attributes;
152
    }
153
154
    /**
155
     * Get an attribute array of all arrayable attributes.
156
     *
157
     * @return array
158
     */
159 188
    protected function getArrayableAttributes()
160
    {
161 188
        return $this->getArrayableItems($this->getAttributes());
162
    }
163
164
    /**
165
     * Get all of the appendable values that are arrayable.
166
     *
167
     * @return array
168
     */
169 188
    protected function getArrayableAppends()
170
    {
171 188
        if (! count($this->appends)) {
172 188
            return [];
173
        }
174
175 4
        return $this->getArrayableItems(
176 4
            array_combine($this->appends, $this->appends)
177 2
        );
178
    }
179
180
    /**
181
     * Get an attribute array of all arrayable values.
182
     *
183
     *
184
     * @return array
185
     */
186 188
    protected function getArrayableItems(array $values)
187
    {
188 188
        if (count($this->getVisible()) > 0) {
189 108
            $values = array_intersect_key($values, array_flip($this->getVisible()));
190
        }
191
192 188
        if (count($this->getHidden()) > 0) {
193 32
            $values = array_diff_key($values, array_flip($this->getHidden()));
194
        }
195
196 188
        return $values;
197
    }
198
199
    /**
200
     * Get an attribute from the model.
201
     *
202
     * @param  string  $key
203
     * @return mixed
204
     */
205 88
    public function getAttribute($key)
206
    {
207 88
        if (! $key) {
208 4
            return;
209
        }
210
211
        // If the attribute exists in the attribute array or has a "get" mutator we will
212
        // get the attribute's value. Otherwise, we will proceed as if the developers
213
        // are asking for a relationship's value. This covers both types of values.
214 84
        if (array_key_exists($key, $this->attributes)
215 32
            || array_key_exists($key, $this->casts)
216 84
            || $this->hasGetMutator($key)) {
217 76
            return $this->getAttributeValue($key);
218
        }
219
220
        // Here we will determine if the model base class itself contains this given key
221
        // since we don't want to treat any of those methods as relationships because
222
        // they are all intended as helper methods and none of these are relations.
223 20
        if (method_exists(self::class, $key)) {
224 4
            return;
225
        }
226
227 16
        return $this->getRelationValue($key);
228
    }
229
230
    /**
231
     * Get a plain attribute (not a relationship).
232
     *
233
     * @param  string  $key
234
     * @return mixed
235
     */
236 76
    public function getAttributeValue($key)
237
    {
238 76
        return $this->transformModelValue($key, $this->getAttributeFromArray($key));
239
    }
240
241
    /**
242
     * Get an attribute from the $attributes array.
243
     *
244
     *
245
     * @return mixed
246
     */
247 76
    protected function getAttributeFromArray(string $key)
248
    {
249 76
        return $this->getAttributes()[$key] ?? null;
250
    }
251
252
    /**
253
     * Determine if a get mutator exists for an attribute.
254
     *
255
     * @param  string  $key
256
     * @return bool
257
     */
258 84
    public function hasGetMutator($key)
259
    {
260 84
        return method_exists($this, 'get'.Util::stringStudly($key).'Attribute');
261
    }
262
263
    /**
264
     * Get the value of an attribute using its mutator.
265
     *
266
     * @param  string  $key
267
     * @param  mixed  $value
268
     * @return mixed
269
     */
270 12
    protected function mutateAttribute($key, $value)
271
    {
272 12
        return $this->{'get'.Util::stringStudly($key).'Attribute'}($value);
273
    }
274
275
    /**
276
     * Get the value of an attribute using its mutator for array conversion.
277
     *
278
     * @param  string  $key
279
     * @param  mixed  $value
280
     * @return mixed
281
     */
282 8
    protected function mutateAttributeForArray($key, $value)
283
    {
284 8
        $value = $this->mutateAttribute($key, $value);
285
286 8
        return $value instanceof Arrayable ? $value->toArray() : $value;
287
    }
288
289
    /**
290
     * Merge new casts with existing casts on the model.
291
     *
292
     *
293
     * @return $this
294
     */
295 12
    public function mergeCasts(array $casts)
296
    {
297 12
        $this->casts = array_merge($this->casts, $casts);
298
299 12
        return $this;
300
    }
301
302
    /**
303
     * Cast an attribute to a native PHP type.
304
     *
305
     * @param  string  $key
306
     * @param  mixed  $value
307
     * @return mixed
308
     */
309 16
    protected function castAttribute($key, $value)
310
    {
311 16
        if (is_null($value)) {
312 8
            return $value;
313
        }
314
315 16
        switch ($this->getCastType($key)) {
316 16
            case 'int':
317 16
            case 'integer':
318 4
                return (int) $value;
319 16
            case 'real':
320 16
            case 'float':
321 16
            case 'double':
322 4
                return $this->fromFloat($value);
323 16
            case 'string':
324 4
                return (string) $value;
325 12
            case 'bool':
326 12
            case 'boolean':
327 4
                return (bool) $value;
328 12
            case 'object':
329 4
                return $this->fromJson($value, true);
330 12
            case 'array':
331 12
            case 'json':
332 4
                return $this->fromJson($value);
333 12
            case 'collection':
334 4
                return new Collection($this->fromJson($value));
335
        }
336
337 8
        return $value;
338
    }
339
340
    /**
341
     * Get the type of cast for a model attribute.
342
     *
343
     * @param  string  $key
344
     * @return string
345
     */
346 48
    protected function getCastType($key)
347
    {
348 48
        return trim(strtolower($this->getCasts()[$key]));
349
    }
350
351
    /**
352
     * Set a given attribute on the model.
353
     *
354
     * @param  string  $key
355
     * @param  mixed  $value
356
     * @return mixed
357
     */
358 268
    public function setAttribute($key, $value)
359
    {
360
        // First we will check for the presence of a mutator for the set operation
361
        // which simply lets the developers tweak the attribute as it is set on
362
        // the model, such as "json_encoding" an listing of data for storage.
363 268
        if ($this->hasSetMutator($key)) {
364 8
            return $this->setMutatedAttributeValue($key, $value);
365
        }
366
367 260
        if (! is_null($value) && $this->isJsonCastable($key)) {
368 4
            $value = $this->asJson($value);
369
        }
370
371 260
        $this->attributes[$key] = $value;
372
373 260
        return $this;
374
    }
375
376
    /**
377
     * Determine if a set mutator exists for an attribute.
378
     *
379
     * @param  string  $key
380
     * @return bool
381
     */
382 268
    public function hasSetMutator($key)
383
    {
384 268
        return method_exists($this, 'set'.Util::stringStudly($key).'Attribute');
385
    }
386
387
    /**
388
     * Set the value of an attribute using its mutator.
389
     *
390
     * @param  string  $key
391
     * @param  mixed  $value
392
     * @return mixed
393
     */
394 8
    protected function setMutatedAttributeValue($key, $value)
395
    {
396 8
        return $this->{'set'.Util::stringStudly($key).'Attribute'}($value);
397
    }
398
399
    /**
400
     * Encode the given value as JSON.
401
     *
402
     * @param  mixed  $value
403
     * @return string
404
     */
405 4
    protected function asJson($value)
406
    {
407 4
        return json_encode($value, JSON_THROW_ON_ERROR);
408
    }
409
410
    /**
411
     * Decode the given JSON back into an array or object.
412
     *
413
     * @param  bool  $asObject
414
     * @return mixed
415
     */
416 4
    public function fromJson(string $value, $asObject = false)
417
    {
418 4
        return json_decode($value, ! $asObject, 512, JSON_THROW_ON_ERROR);
419
    }
420
421
    /**
422
     * Decode the given float.
423
     *
424
     * @param  mixed  $value
425
     * @return mixed
426
     */
427 4
    public function fromFloat($value)
428
    {
429 4
        switch ((string) $value) {
430 4
            case 'Infinity':
431 4
                return INF;
432 4
            case '-Infinity':
433 4
                return -INF;
434 4
            case 'NaN':
435 4
                return NAN;
436
            default:
437 4
                return (float) $value;
438
        }
439
    }
440
441
    /**
442
     * Determine whether an attribute should be cast to a native type.
443
     *
444
     * @param  string  $key
445
     * @param  array|string|null  $types
446
     * @return bool
447
     */
448 260
    public function hasCast($key, $types = null)
449
    {
450 260
        if (array_key_exists($key, $this->getCasts())) {
451 48
            return $types ? in_array($this->getCastType($key), (array) $types, true) : true;
452
        }
453
454 252
        return false;
455
    }
456
457
    /**
458
     * Get the casts array.
459
     *
460
     * @return array
461
     */
462 372
    public function getCasts()
463
    {
464 372
        return $this->casts;
465
    }
466
467
    /**
468
     * Determine whether a value is JSON castable for inbound manipulation.
469
     *
470
     * @param  string  $key
471
     * @return bool
472
     */
473 260
    protected function isJsonCastable($key)
474
    {
475 260
        return $this->hasCast($key, ['array', 'json', 'object', 'collection']);
476
    }
477
478
    /**
479
     * Get all of the current attributes on the model.
480
     *
481
     * @return array
482
     */
483 268
    public function getAttributes()
484
    {
485 268
        return $this->attributes;
486
    }
487
488
    /**
489
     * Get a subset of the model's attributes.
490
     *
491
     * @param  array|mixed  $attributes
492
     * @return array
493
     */
494 4
    public function only($attributes)
495
    {
496 4
        $results = [];
497
498 4
        foreach (is_array($attributes) ? $attributes : func_get_args() as $attribute) {
499 4
            $results[$attribute] = $this->getAttribute($attribute);
500
        }
501
502 4
        return $results;
503
    }
504
505
    /**
506
     * Transform a raw model value using mutators, casts, etc.
507
     *
508
     * @param  string  $key
509
     * @param  mixed  $value
510
     * @return mixed
511
     */
512 76
    protected function transformModelValue($key, $value)
513
    {
514
        // If the attribute has a get mutator, we will call that then return what
515
        // it returns as the value, which is useful for transforming values on
516
        // retrieval from the model to a form that is more useful for usage.
517 76
        if ($this->hasGetMutator($key)) {
518 4
            return $this->mutateAttribute($key, $value);
519
        }
520
521
        // If the attribute exists within the cast array, we will convert it to
522
        // an appropriate native PHP type dependent upon the associated value
523
        // given with the key in the pair. Dayle made this comment line up.
524 72
        if ($this->hasCast($key)) {
525 12
            return $this->castAttribute($key, $value);
526
        }
527
528 64
        return $value;
529
    }
530
531
    /**
532
     * Set the accessors to append to model arrays.
533
     *
534
     *
535
     * @return $this
536
     */
537 4
    public function setAppends(array $appends)
538
    {
539 4
        $this->appends = $appends;
540
541 4
        return $this;
542
    }
543
544
    /**
545
     * Add the accessors to append to model arrays.
546
     *
547
     * @param  array  $attributes
548
     * @return $this
549
     */
550 4
    public function mergeAppends($attributes)
551
    {
552 4
        $this->appends = array_merge($this->appends, $attributes);
553
554 4
        return $this;
555
    }
556
557
    /**
558
     * Return whether the accessor attribute has been appended.
559
     *
560
     * @param  string  $attribute
561
     * @return bool
562
     */
563 4
    public function hasAppended($attribute)
564
    {
565 4
        return in_array($attribute, $this->appends);
566
    }
567
568
    /**
569
     * Get the mutated attributes for a given instance.
570
     *
571
     * @return array
572
     */
573 188
    public function getMutatedAttributes()
574
    {
575 188
        $class = static::class;
576
577 188
        if (! isset(static::$mutatorCache[$class])) {
578 16
            static::cacheMutatedAttributes($class);
579
        }
580
581 188
        return static::$mutatorCache[$class];
582
    }
583
584
    /**
585
     * Extract and cache all the mutated attributes of a class.
586
     *
587
     * @param  string  $class
588
     * @return void
589
     */
590 16
    public static function cacheMutatedAttributes($class)
591
    {
592 16
        $mutatedAttributes = [];
593
594
        // Here we will extract all of the mutated attributes so that we can quickly
595
        // spin through them after we export models to their array form, which we
596
        // need to be fast. This'll let us know the attributes that can mutate.
597 16
        foreach (static::getMutatorMethods($class) as $match) {
598 4
            $mutatedAttributes[] = lcfirst(static::$snakeAttributes ? Util::stringSnake($match) : $match);
599
        }
600
601 16
        static::$mutatorCache[$class] = $mutatedAttributes;
602 8
    }
603
604
    /**
605
     * Get all of the attribute mutator methods.
606
     *
607
     * @param  mixed  $class
608
     * @return array
609
     */
610 16
    protected static function getMutatorMethods($class)
611
    {
612 16
        preg_match_all('/(?<=^|;)get([^;]+?)Attribute(;|$)/', implode(';', get_class_methods($class)), $matches);
613
614 16
        return $matches[1];
615
    }
616
}
617