Completed
Push — master ( 6b2337...35aa85 )
by Jasper
14s queued 11s
created

HasAttributes::only()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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