Passed
Push — main ( c2020d...c060b3 )
by Gabriel
13:36
created

CastableTrait::isClassCastable()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
cc 4
dl 0
loc 17
rs 10
c 0
b 0
f 0
eloc 8
nc 4
nop 1
ccs 0
cts 0
cp 0
crap 20
1
<?php
2
3
namespace ByTIC\DataObjects\Behaviors\Castable;
4
5
use ByTIC\DataObjects\Casts\Castable;
6
use ByTIC\DataObjects\Casts\CastsInboundAttributes;
7
use ByTIC\DataObjects\Exceptions\InvalidCastException;
8
use ByTIC\DataObjects\ValueCaster;
9
10
/**
11
 * Trait CastableTrait
12
 * @package ByTIC\DataObjects\Behaviors\Castable
13
 */
14
trait CastableTrait
15
{
16
    /**
17
     * The attributes that should be cast.
18
     *
19
     * @var array
20
     */
21
    protected $casts = [];
22
23
    /**
24
     * The attributes that have been cast using custom classes.
25 6
     *
26
     * @var array
27
     */
28
    protected $classCastCache = [];
29
30 6
    /**
31 1
     * @param $key
32
     * @param $value
33
     * @return mixed
34 5
     */
35
    public function transformValue($key, $value)
36
    {
37
        // If the attribute exists within the cast array, we will convert it to
38
        // an appropriate native PHP type dependent upon the associated value
39
        // given with the key in the pair. Dayle made this comment line up.
40
        if ($this->hasCast($key)) {
41
            return $this->castValue($key, $value);
42
        }
43
44 1
        return $value;
45
    }
46 1
47
    /**
48 1
     * @param $key
49
     * @param $value
50
     * @return mixed
51
     */
52 1
    public function transformInboundValue($key, $value)
53
    {
54
        if ($value && $this->isDateCastable($key)) {
55
            return ValueCaster::asDateTime($value)->format('Y-m-d H:i:s');
56
        }
57
        if ($this->isClassCastable($key)) {
58
            return $this->transformClassCastableAttribute($key, $value);
59
        }
60
        return $value;
61 1
    }
62
63 1
    /**
64
     * Determine whether an attribute should be cast to a native type.
65
     *
66
     * @param string $key
67 1
     * @param array|string|null $types
68
     * @return bool
69
     */
70
    public function hasCast($key, $types = null): bool
71 1
    {
72
        if (array_key_exists($key, $this->getCasts())) {
73
            return $types ? in_array($this->getCastType($key), (array)$types, true) : true;
74
        }
75
76
        return false;
77
    }
78
79
80 1
    /**
81
     * Get the casts array.
82 1
     *
83 1
     * @return array
84
     */
85
    public function getCasts(): array
86
    {
87
        return $this->casts;
88
    }
89
90
    /**
91
     * @param $attribute
92 1
     * @param $cast
93
     * @return self
94 1
     */
95
    public function addCast($attribute, $cast): self
96
    {
97
        $this->casts[$attribute] = $cast;
98
        return $this;
99
    }
100
101
    /**
102
     * Cast an attribute to a native PHP type.
103
     *
104 6
     * @param string $key
105
     * @param mixed $value
106 6
     * @return mixed
107 1
     */
108
    protected function castValue(string $key, $value)
109
    {
110 5
        $castType = $this->getCastType($key);
111
112
        $isPrimitiveType = in_array($castType, ValueCaster::$primitiveTypes);
113
114
        if (is_null($value) && $isPrimitiveType) {
115
            return $value;
116
        }
117
118
        if ($isPrimitiveType) {
119 6
            return ValueCaster::as($value, $castType);
120
        }
121 6
122
        if ($this->isClassCastable($key)) {
123
            return $this->getClassCastableAttributeValue($key, $value);
124
        }
125
126
        return $value;
127
    }
128
129
    /**
130
     * @param $key
131
     * @param $value
132
     */
133
    protected function transformClassCastableAttribute($key, $value)
134
    {
135
        $caster = $this->resolveCasterClass($key);
136
        $responseValues = $this->normalizeCastClassResponse(
137
            $key,
138
            $caster->set($this, $key, $value, $this->attributes)
139
        );
140
141
        if (isset($responseValues[$key])) {
142
            $return = $responseValues[$key];
143
            unset($responseValues[$key]);
144
        }
145
146
        if ($caster instanceof CastsInboundAttributes || !is_object($value)) {
147
            unset($this->classCastCache[$key]);
148
        } else {
149
            $this->classCastCache[$key] = $value;
150
        }
151
        return $return;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $return does not seem to be defined for all execution paths leading up to this point.
Loading history...
152
    }
153
154
    /**
155
     * Cast the given attribute using a custom cast class.
156
     *
157
     * @param string $key
158
     * @param mixed $value
159
     * @return mixed
160
     */
161
    protected function getClassCastableAttributeValue(string $key, $value)
162
    {
163
        if (isset($this->classCastCache[$key])) {
164
            return $this->classCastCache[$key];
165
        } else {
166
            $caster = $this->resolveCasterClass($key);
167
168
            $value = $caster instanceof CastsInboundAttributes
169
                ? $value
170
                : $caster->get($this, $key, $value, $this->attributes);
171
172
            if ($caster instanceof CastsInboundAttributes || !is_object($value)) {
173
                unset($this->classCastCache[$key]);
174
            } else {
175
                $this->classCastCache[$key] = $value;
176
            }
177
178
            return $value;
179
        }
180
    }
181
182
    /**
183
     * Get the type of cast for a model attribute.
184
     *
185
     * @param string $key
186
     * @return string
187
     */
188
    protected function getCastType(string $key): string
189
    {
190
        if ($this->isCustomDateTimeCast($this->getCasts()[$key])) {
191
            return 'custom_datetime';
192
        }
193
194
        if ($this->isDecimalCast($this->getCasts()[$key])) {
195
            return 'decimal';
196
        }
197
198
        return trim(strtolower($this->getCasts()[$key]));
199
    }
200
201
    /**
202
     * Determine if the cast type is a custom date time cast.
203
     *
204
     * @param string $cast
205
     * @return bool
206
     */
207
    protected function isCustomDateTimeCast(string $cast): bool
208
    {
209
        return strncmp($cast, 'date:', 5) === 0 ||
210
            strncmp($cast, 'datetime:', 9) === 0;
211
    }
212
213
    /**
214
     * Determine if the cast type is a decimal cast.
215
     *
216
     * @param string $cast
217
     * @return bool
218
     */
219
    protected function isDecimalCast(string $cast): bool
220
    {
221
        return strncmp($cast, 'decimal:', 8) === 0;
222
    }
223
224
    /**
225
     * Determine whether a value is Date / DateTime castable for inbound manipulation.
226
     *
227
     * @param string $key
228
     * @return bool
229
     */
230
    protected function isDateCastable(string $key): bool
231
    {
232
        return $this->hasCast($key, ['date', 'datetime']);
233
    }
234
235
    /**
236
     * Determine whether a value is JSON castable for inbound manipulation.
237
     *
238
     * @param string $key
239
     * @return bool
240
     */
241
    protected function isJsonCastable(string $key): bool
242
    {
243
        return $this->hasCast(
244
            $key,
245
            [
246
                'array',
247
                'json',
248
                'object',
249
                'collection',
250
                'encrypted:array',
251
                'encrypted:collection',
252
                'encrypted:json',
253
                'encrypted:object'
254
            ]
255
        );
256
    }
257
258
259
    /**
260
     * Determine if the given key is cast using a custom class.
261
     *
262
     * @param string $key
263
     * @return bool
264
     */
265
    protected function isClassCastable(string $key): bool
266
    {
267
        if (!array_key_exists($key, $this->getCasts())) {
268
            return false;
269
        }
270
271
        $castType = $this->parseCasterClass($this->getCasts()[$key]);
272
273
        if (in_array($castType, ValueCaster::$primitiveTypes)) {
274
            return false;
275
        }
276
277
        if (class_exists($castType)) {
278
            return true;
279
        }
280
281
        throw new InvalidCastException($this, $key, $castType);
282
    }
283
284
285
    /**
286
     * Resolve the custom caster class for a given key.
287
     *
288
     * @param string $key
289
     * @return mixed
290
     */
291
    protected function resolveCasterClass(string $key)
292
    {
293
        $castType = $this->getCasts()[$key];
294
295
        $arguments = [];
296
297
        if (is_string($castType) && strpos($castType, ':') !== false) {
298
            $segments = explode(':', $castType, 2);
299
300
            $castType = $segments[0];
301
            $arguments = explode(',', $segments[1]);
302
        }
303
304
        if (is_subclass_of($castType, Castable::class)) {
305
            $castType = $castType::castUsing($arguments);
306
        }
307
308
        if (is_object($castType)) {
309
            return $castType;
310
        }
311
312
        return new $castType(...$arguments);
313
    }
314
315
    /**
316
     * Parse the given caster class, removing any arguments.
317
     *
318
     * @param string $class
319
     * @return string
320
     */
321
    protected function parseCasterClass(string $class): string
322
    {
323
        return strpos($class, ':') === false
324
            ? $class
325
            : explode(':', $class, 2)[0];
326
    }
327
328
    /**
329
     * Normalize the response from a custom class caster.
330
     *
331
     * @param string $key
332
     * @param mixed $value
333
     * @return array
334
     */
335
    protected function normalizeCastClassResponse(string $key, $value): array
336
    {
337
        return is_array($value) ? $value : [$key => $value];
338
    }
339
}
340