CastableTrait::isCustomDateTimeCast()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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