Passed
Push — main ( c5ac27...5a77c7 )
by Anatoly
13:01 queued 10:26
created

Hydrator::hydratePropertyWithEnumerableValue()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 27
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 14
c 0
b 0
f 0
nc 3
nop 5
dl 0
loc 27
rs 9.7998
ccs 15
cts 15
cp 1
crap 3
1
<?php declare(strict_types=1);
2
3
/**
4
 * It's free open-source software released under the MIT License.
5
 *
6
 * @author Anatoly Fenric <[email protected]>
7
 * @copyright Copyright (c) 2021, Anatoly Fenric
8
 * @license https://github.com/sunrise-php/hydrator/blob/master/LICENSE
9
 * @link https://github.com/sunrise-php/hydrator
10
 */
11
12
namespace Sunrise\Hydrator;
13
14
/**
15
 * Import classes
16
 */
17
use Doctrine\Common\Annotations\SimpleAnnotationReader;
18
use ArrayAccess;
19
use DateTimeInterface;
20
use ReflectionClass;
21
use ReflectionProperty;
22
use ReflectionNamedType;
23
24
/**
25
 * Import functions
26
 */
27
use function array_key_exists;
28
use function class_exists;
29
use function constant;
30
use function defined;
31
use function in_array;
32
use function is_array;
33
use function is_scalar;
34
use function is_string;
35
use function is_subclass_of;
36
use function sprintf;
37
use function strtotime;
38
39
/**
40
 * Hydrator
41
 */
42
class Hydrator implements HydratorInterface
43
{
44
45
    /**
46
     * @var SimpleAnnotationReader
47
     */
48
    private $annotationReader;
49
50
    /**
51
     * Constructor of the class
52
     */
53 43
    public function __construct()
54
    {
55 43
        if (class_exists(SimpleAnnotationReader::class)) {
56 43
            $this->annotationReader = /** @scrutinizer ignore-deprecated */ new SimpleAnnotationReader();
57 43
            $this->annotationReader->addNamespace(Annotation::class);
58
        }
59 43
    }
60
61
    /**
62
     * {@inheritDoc}
63
     *
64
     * @throws Exception\MissingRequiredValueException
65
     *         If the given data does not contain required value.
66
     *         * data error.
67
     *
68
     * @throws Exception\InvalidValueException
69
     *         If the given data contains an invalid value.
70
     *         * data error.
71
     *
72
     * @throws Exception\UntypedObjectPropertyException
73
     *         If one of the properties of the given object is not typed.
74
     *         * DTO error.
75
     *
76
     * @throws Exception\UnsupportedObjectPropertyTypeException
77
     *         If one of the properties of the given object contains an unsupported type.
78
     *         * DTO error.
79
     */
80 42
    public function hydrate(HydrableObjectInterface $object, array $data) : HydrableObjectInterface
81
    {
82 42
        $class = new ReflectionClass($object);
83 42
        $properties = $class->getProperties();
84 42
        foreach ($properties as $property) {
85 42
            $property->setAccessible(true);
86
87 42
            if ($property->isStatic()) {
88 1
                continue;
89
            }
90
91 42
            $key = $property->getName();
92
93 42
            if (isset($this->annotationReader) && !array_key_exists($key, $data)) {
94 36
                $alias =  $this->annotationReader->getPropertyAnnotation($property, Annotation\Alias::class);
95 36
                if ($alias instanceof Annotation\Alias) {
96 1
                    $key = $alias->value;
97
                }
98
            }
99
100 42
            if (!array_key_exists($key, $data)) {
101 36
                if (!$property->isInitialized($object)) {
102 1
                    throw new Exception\MissingRequiredValueException(sprintf(
103 1
                        'The <%s.%s> property is required.',
104 1
                        $class->getShortName(),
105 1
                        $property->getName(),
106
                    ));
107
                }
108
109 35
                continue;
110
            }
111
112 41
            if (!$property->hasType()) {
113 1
                throw new Exception\UntypedObjectPropertyException(sprintf(
114 1
                    'The <%s.%s> property is not typed.',
115 1
                    $class->getShortName(),
116 1
                    $property->getName(),
117
                ));
118
            }
119
120 40
            $this->hydrateProperty($object, $class, $property, $property->getType(), $data[$key]);
121
        }
122
123 2
        return $object;
124
    }
125
126
    /**
127
     * Hydrates the given property with the given value
128
     *
129
     * @param HydrableObjectInterface $object
130
     * @param ReflectionClass $class
131
     * @param ReflectionProperty $property
132
     * @param ReflectionNamedType $type
133
     * @param mixed $value
134
     *
135
     * @return void
136
     *
137
     * @throws Exception\UnsupportedObjectPropertyTypeException
138
     *         If the given property contains an unsupported type.
139
     *
140
     * @throws Exception\InvalidValueException
141
     *         If the given value isn't valid.
142
     */
143 40
    private function hydrateProperty(
144
        HydrableObjectInterface $object,
145
        ReflectionClass $class,
146
        ReflectionProperty $property,
147
        ReflectionNamedType $type,
148
        $value
149
    ) : void {
150 40
        if (null === $value) {
151 2
            $this->hydratePropertyWithNull($object, $class, $property, $type);
152 1
            return;
153
        }
154
155 39
        if (in_array($type->getName(), ['bool', 'int', 'float', 'string'])) {
156 5
            $this->hydratePropertyWithScalar($object, $class, $property, $type, $value);
157 1
            return;
158
        }
159
160 35
        if ('array' === $type->getName() || is_subclass_of($type->getName(), ArrayAccess::class)) {
161 8
            $this->hydratePropertyWithArray($object, $class, $property, $type, $value);
162 1
            return;
163
        }
164
165 28
        if (is_subclass_of($type->getName(), DateTimeInterface::class)) {
166 10
            $this->hydratePropertyWithDateTime($object, $class, $property, $type, $value);
167 1
            return;
168
        }
169
170 19
        if (is_subclass_of($type->getName(), EnumerableObjectInterface::class)) {
171 3
            $this->hydratePropertyWithEnumerableValue($object, $class, $property, $type, $value);
172 1
            return;
173
        }
174
175 16
        if (is_subclass_of($type->getName(), HydrableObjectInterface::class)) {
176 8
            $this->hydratePropertyWithOneToOneAssociation($object, $class, $property, $type, $value);
177 1
            return;
178
        }
179
180 9
        if (is_subclass_of($type->getName(), HydrableObjectCollectionInterface::class)) {
181 8
            $this->hydratePropertyWithOneToManyAssociation($object, $class, $property, $type, $value);
182 1
            return;
183
        }
184
185 1
        throw new Exception\UnsupportedObjectPropertyTypeException(sprintf(
186 1
            'The <%s.%s> property contains the <%s> unhydrable type.',
187 1
            $class->getShortName(),
188 1
            $property->getName(),
189 1
            $type->getName(),
190
        ));
191
    }
192
193
    /**
194
     * Hydrates the given property with null
195
     *
196
     * @param HydrableObjectInterface $object
197
     * @param ReflectionClass $class
198
     * @param ReflectionProperty $property
199
     * @param ReflectionNamedType $type
200
     *
201
     * @return void
202
     *
203
     * @throws Exception\InvalidValueException
204
     *         If the given value isn't valid.
205
     */
206 2
    private function hydratePropertyWithNull(
207
        HydrableObjectInterface $object,
208
        ReflectionClass $class,
209
        ReflectionProperty $property,
210
        ReflectionNamedType $type
211
    ) : void {
212 2
        if (!$type->allowsNull()) {
213 1
            throw new Exception\InvalidValueException(sprintf(
214 1
                'The <%s.%s> property does not support null.',
215 1
                $class->getShortName(),
216 1
                $property->getName(),
217
            ));
218
        }
219
220 1
        $property->setValue($object, null);
221 1
    }
222
223
    /**
224
     * Hydrates the given property with the given scalar value
225
     *
226
     * @param HydrableObjectInterface $object
227
     * @param ReflectionClass $class
228
     * @param ReflectionProperty $property
229
     * @param ReflectionNamedType $type
230
     * @param mixed $value
231
     *
232
     * @return void
233
     *
234
     * @throws Exception\InvalidValueException
235
     *         If the given value isn't valid.
236
     */
237 5
    private function hydratePropertyWithScalar(
238
        HydrableObjectInterface $object,
239
        ReflectionClass $class,
240
        ReflectionProperty $property,
241
        ReflectionNamedType $type,
242
        $value
243
    ) : void {
244 5
        if (!is_scalar($value)) {
245 4
            throw new Exception\InvalidValueException(sprintf(
246 4
                'The <%s.%s> property only accepts a scalar value.',
247 4
                $class->getShortName(),
248 4
                $property->getName(),
249
            ));
250
        }
251
252 1
        switch ($type->getName()) {
253 1
            case 'bool':
254 1
                $property->setValue($object, (bool) $value);
255 1
                break;
256 1
            case 'int':
257 1
                $property->setValue($object, (int) $value);
258 1
                break;
259 1
            case 'float':
260 1
                $property->setValue($object, (float) $value);
261 1
                break;
262 1
            case 'string':
263 1
                $property->setValue($object, (string) $value);
264 1
                break;
265
        }
266 1
    }
267
268
    /**
269
     * Hydrates the given property with the given array value
270
     *
271
     * @param HydrableObjectInterface $object
272
     * @param ReflectionClass $class
273
     * @param ReflectionProperty $property
274
     * @param ReflectionNamedType $type
275
     * @param mixed $value
276
     *
277
     * @return void
278
     *
279
     * @throws Exception\InvalidValueException
280
     *         If the given value isn't valid.
281
     */
282 8
    private function hydratePropertyWithArray(
283
        HydrableObjectInterface $object,
284
        ReflectionClass $class,
285
        ReflectionProperty $property,
286
        ReflectionNamedType $type,
287
        $value
288
    ) : void {
289 8
        if (!is_array($value)) {
290 7
            throw new Exception\InvalidValueException(sprintf(
291 7
                'The <%s.%s> property only accepts an array.',
292 7
                $class->getShortName(),
293 7
                $property->getName(),
294
            ));
295
        }
296
297 1
        if ('array' === $type->getName()) {
298 1
            $property->setValue($object, $value);
299 1
            return;
300
        }
301
302 1
        $arrayClassName = $type->getName();
303 1
        $array = new $arrayClassName();
304 1
        foreach ($value as $offset => $element) {
305 1
            $array->offsetSet($offset, $element);
306
        }
307
308 1
        $property->setValue($object, $array);
309 1
    }
310
311
    /**
312
     * Hydrates the given property with the given enumerable value
313
     *
314
     * @param HydrableObjectInterface $object
315
     * @param ReflectionClass $class
316
     * @param ReflectionProperty $property
317
     * @param ReflectionNamedType $type
318
     * @param mixed $value
319
     *
320
     * @return void
321
     *
322
     * @throws Exception\InvalidValueException
323
     *         If the given value isn't valid.
324
     */
325 3
    private function hydratePropertyWithEnumerableValue(
326
        HydrableObjectInterface $object,
327
        ReflectionClass $class,
328
        ReflectionProperty $property,
329
        ReflectionNamedType $type,
330
        $value
331
    ) : void {
332 3
        if (!is_string($value)) {
333 1
            throw new Exception\InvalidValueException(sprintf(
334 1
                'The <%s.%s> property only accepts a string.',
335 1
                $class->getShortName(),
336 1
                $property->getName(),
337
            ));
338
        }
339
340 2
        $enum = $type->getName();
341 2
        $constant = sprintf('%s::%s', $enum, $value);
342 2
        if (!defined($constant)) {
343 1
            throw new Exception\InvalidValueException(sprintf(
344 1
                'The <%s.%s> property only accepts one of the <%s> enum values.',
345 1
                $class->getShortName(),
346 1
                $property->getName(),
347 1
                (new ReflectionClass($enum))->getShortName(),
348
            ));
349
        }
350
351 1
        $property->setValue($object, new $enum(constant($constant)));
352 1
    }
353
354
    /**
355
     * Hydrates the given property with the given date-time value
356
     *
357
     * @param HydrableObjectInterface $object
358
     * @param ReflectionClass $class
359
     * @param ReflectionProperty $property
360
     * @param ReflectionNamedType $type
361
     * @param mixed $value
362
     *
363
     * @return void
364
     *
365
     * @throws Exception\InvalidValueException
366
     *         If the given value isn't valid.
367
     */
368 10
    private function hydratePropertyWithDateTime(
369
        HydrableObjectInterface $object,
370
        ReflectionClass $class,
371
        ReflectionProperty $property,
372
        ReflectionNamedType $type,
373
        $value
374
    ) : void {
375 10
        if (!is_string($value) || false === strtotime($value)) {
376 9
            throw new Exception\InvalidValueException(sprintf(
377 9
                'The <%s.%s> property only accepts a valid date-time string.',
378 9
                $class->getShortName(),
379 9
                $property->getName(),
380
            ));
381
        }
382
383 1
        $dateTimeClassName = $type->getName();
384 1
        $dateTime = new $dateTimeClassName($value);
385 1
        $property->setValue($object, $dateTime);
386 1
    }
387
388
    /**
389
     * Hydrates the given property with the given one-to-one value
390
     *
391
     * @param HydrableObjectInterface $object
392
     * @param ReflectionClass $class
393
     * @param ReflectionProperty $property
394
     * @param ReflectionNamedType $type
395
     * @param mixed $value
396
     *
397
     * @return void
398
     *
399
     * @throws Exception\InvalidValueException
400
     *         If the given value isn't valid.
401
     */
402 8
    private function hydratePropertyWithOneToOneAssociation(
403
        HydrableObjectInterface $object,
404
        ReflectionClass $class,
405
        ReflectionProperty $property,
406
        ReflectionNamedType $type,
407
        $value
408
    ) : void {
409 8
        if (!is_array($value)) {
410 7
            throw new Exception\InvalidValueException(sprintf(
411 7
                'The <%s.%s> property only accepts an array.',
412 7
                $class->getShortName(),
413 7
                $property->getName(),
414
            ));
415
        }
416
417 1
        $childObjectClassName = $type->getName();
418 1
        $childObject = new $childObjectClassName();
419 1
        $this->hydrate($childObject, $value);
420 1
        $property->setValue($object, $childObject);
421 1
    }
422
423
    /**
424
     * Hydrates the given property with the given one-to-many value
425
     *
426
     * @param HydrableObjectInterface $object
427
     * @param ReflectionClass $class
428
     * @param ReflectionProperty $property
429
     * @param ReflectionNamedType $type
430
     * @param mixed $value
431
     *
432
     * @return void
433
     *
434
     * @throws Exception\InvalidValueException
435
     *         If the given value isn't valid.
436
     */
437 8
    private function hydratePropertyWithOneToManyAssociation(
438
        HydrableObjectInterface $object,
439
        ReflectionClass $class,
440
        ReflectionProperty $property,
441
        ReflectionNamedType $type,
442
        $value
443
    ) : void {
444 8
        if (!is_array($value)) {
445 7
            throw new Exception\InvalidValueException(sprintf(
446 7
                'The <%s.%s> property only accepts an array.',
447 7
                $class->getShortName(),
448 7
                $property->getName(),
449
            ));
450
        }
451
452 1
        $objectCollectionClassName = $type->getName();
453 1
        $objectCollection = new $objectCollectionClassName();
454 1
        $collectionObjectClassName = $objectCollectionClassName::T;
455 1
        foreach ($value as $item) {
456 1
            $collectionObject = new $collectionObjectClassName();
457 1
            $this->hydrate($collectionObject, (array) $item);
458 1
            $objectCollection->add($collectionObject);
459
        }
460
461 1
        $property->setValue($object, $objectCollection);
462 1
    }
463
}
464