Passed
Pull Request — main (#6)
by Anatoly
11:48
created

hydratePropertyWithJsonOneToOneAssociation()   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 ctype_digit;
31
use function defined;
32
use function in_array;
33
use function is_array;
34
use function is_int;
35
use function is_scalar;
36
use function is_string;
37
use function is_subclass_of;
38
use function json_decode;
39
use function json_last_error;
40
use function json_last_error_msg;
41
use function sprintf;
42
use function strtotime;
43
44
/**
45
 * Import constants
46
 */
47
use const JSON_ERROR_NONE;
48
49
/**
50
 * Hydrator
51
 */
52
class Hydrator implements HydratorInterface
53
{
54
55
    /**
56
     * @var SimpleAnnotationReader
57
     */
58
    private $annotationReader;
59
60
    /**
61
     * Constructor of the class
62
     */
63 46
    public function __construct()
64
    {
65 46
        if (class_exists(SimpleAnnotationReader::class)) {
66 46
            $this->annotationReader = /** @scrutinizer ignore-deprecated */ new SimpleAnnotationReader();
67 46
            $this->annotationReader->addNamespace(Annotation::class);
68
        }
69 46
    }
70
71
    /**
72
     * {@inheritDoc}
73
     *
74
     * @throws Exception\MissingRequiredValueException
75
     *         If the given data does not contain required value.
76
     *         * data error.
77
     *
78
     * @throws Exception\InvalidValueException
79
     *         If the given data contains an invalid value.
80
     *         * data error.
81
     *
82
     * @throws Exception\UntypedObjectPropertyException
83
     *         If one of the properties of the given object is not typed.
84
     *         * DTO error.
85
     *
86
     * @throws Exception\UnsupportedObjectPropertyTypeException
87
     *         If one of the properties of the given object contains an unsupported type.
88
     *         * DTO error.
89
     */
90 45
    public function hydrate(HydrableObjectInterface $object, array $data) : HydrableObjectInterface
91
    {
92 45
        $class = new ReflectionClass($object);
93 45
        $properties = $class->getProperties();
94 45
        foreach ($properties as $property) {
95 45
            $property->setAccessible(true);
96
97 45
            if ($property->isStatic()) {
98 1
                continue;
99
            }
100
101 45
            $key = $property->getName();
102
103 45
            if (isset($this->annotationReader) && !array_key_exists($key, $data)) {
104 36
                $alias =  $this->annotationReader->getPropertyAnnotation($property, Annotation\Alias::class);
105 36
                if ($alias instanceof Annotation\Alias) {
106 1
                    $key = $alias->value;
107
                }
108
            }
109
110 45
            if (!array_key_exists($key, $data)) {
111 36
                if (!$property->isInitialized($object)) {
112 1
                    throw new Exception\MissingRequiredValueException(sprintf(
113 1
                        'The <%s.%s> property is required.',
114 1
                        $class->getShortName(),
115 1
                        $property->getName(),
116
                    ));
117
                }
118
119 35
                continue;
120
            }
121
122 44
            if (!$property->hasType()) {
123 1
                throw new Exception\UntypedObjectPropertyException(sprintf(
124 1
                    'The <%s.%s> property is not typed.',
125 1
                    $class->getShortName(),
126 1
                    $property->getName(),
127
                ));
128
            }
129
130 43
            $this->hydrateProperty($object, $class, $property, $property->getType(), $data[$key]);
131
        }
132
133 3
        return $object;
134
    }
135
136
    /**
137
     * Hydrates the given property with the given value
138
     *
139
     * @param HydrableObjectInterface $object
140
     * @param ReflectionClass $class
141
     * @param ReflectionProperty $property
142
     * @param ReflectionNamedType $type
143
     * @param mixed $value
144
     *
145
     * @return void
146
     *
147
     * @throws Exception\UnsupportedObjectPropertyTypeException
148
     *         If the given property contains an unsupported type.
149
     *
150
     * @throws Exception\InvalidValueException
151
     *         If the given value isn't valid.
152
     */
153 43
    private function hydrateProperty(
154
        HydrableObjectInterface $object,
155
        ReflectionClass $class,
156
        ReflectionProperty $property,
157
        ReflectionNamedType $type,
158
        $value
159
    ) : void {
160 43
        if (null === $value) {
161 2
            $this->hydratePropertyWithNull($object, $class, $property, $type);
162 1
            return;
163
        }
164
165 42
        if (in_array($type->getName(), ['bool', 'int', 'float', 'string'])) {
166 6
            $this->hydratePropertyWithScalar($object, $class, $property, $type, $value);
167 2
            return;
168
        }
169
170 38
        if ('array' === $type->getName() || is_subclass_of($type->getName(), ArrayAccess::class)) {
171 8
            $this->hydratePropertyWithArray($object, $class, $property, $type, $value);
172 1
            return;
173
        }
174
175 31
        if (is_subclass_of($type->getName(), DateTimeInterface::class)) {
176 10
            $this->hydratePropertyWithDateTime($object, $class, $property, $type, $value);
177 1
            return;
178
        }
179
180 22
        if (is_subclass_of($type->getName(), EnumerableObjectInterface::class)) {
181 3
            $this->hydratePropertyWithEnumerableValue($object, $class, $property, $type, $value);
182 1
            return;
183
        }
184
185 19
        if (is_subclass_of($type->getName(), JsonableObjectInterface::class)) {
186 3
            $this->hydratePropertyWithJsonOneToOneAssociation($object, $class, $property, $type, $value);
187 1
            return;
188
        }
189
190 16
        if (is_subclass_of($type->getName(), HydrableObjectInterface::class)) {
191 8
            $this->hydratePropertyWithOneToOneAssociation($object, $class, $property, $type, $value);
192 1
            return;
193
        }
194
195 9
        if (is_subclass_of($type->getName(), HydrableObjectCollectionInterface::class)) {
196 8
            $this->hydratePropertyWithOneToManyAssociation($object, $class, $property, $type, $value);
197 1
            return;
198
        }
199
200 1
        throw new Exception\UnsupportedObjectPropertyTypeException(sprintf(
201 1
            'The <%s.%s> property contains the <%s> unhydrable type.',
202 1
            $class->getShortName(),
203 1
            $property->getName(),
204 1
            $type->getName(),
205
        ));
206
    }
207
208
    /**
209
     * Hydrates the given property with null
210
     *
211
     * @param HydrableObjectInterface $object
212
     * @param ReflectionClass $class
213
     * @param ReflectionProperty $property
214
     * @param ReflectionNamedType $type
215
     *
216
     * @return void
217
     *
218
     * @throws Exception\InvalidValueException
219
     *         If the given value isn't valid.
220
     */
221 2
    private function hydratePropertyWithNull(
222
        HydrableObjectInterface $object,
223
        ReflectionClass $class,
224
        ReflectionProperty $property,
225
        ReflectionNamedType $type
226
    ) : void {
227 2
        if (!$type->allowsNull()) {
228 1
            throw new Exception\InvalidValueException(sprintf(
229 1
                'The <%s.%s> property does not support null.',
230 1
                $class->getShortName(),
231 1
                $property->getName(),
232
            ));
233
        }
234
235 1
        $property->setValue($object, null);
236 1
    }
237
238
    /**
239
     * Hydrates the given property with the given scalar value
240
     *
241
     * @param HydrableObjectInterface $object
242
     * @param ReflectionClass $class
243
     * @param ReflectionProperty $property
244
     * @param ReflectionNamedType $type
245
     * @param mixed $value
246
     *
247
     * @return void
248
     *
249
     * @throws Exception\InvalidValueException
250
     *         If the given value isn't valid.
251
     */
252 6
    private function hydratePropertyWithScalar(
253
        HydrableObjectInterface $object,
254
        ReflectionClass $class,
255
        ReflectionProperty $property,
256
        ReflectionNamedType $type,
257
        $value
258
    ) : void {
259 6
        if (!is_scalar($value)) {
260 4
            throw new Exception\InvalidValueException(sprintf(
261 4
                'The <%s.%s> property only accepts a scalar value.',
262 4
                $class->getShortName(),
263 4
                $property->getName(),
264
            ));
265
        }
266
267 2
        switch ($type->getName()) {
268 2
            case 'bool':
269 1
                $property->setValue($object, (bool) $value);
270 1
                break;
271 2
            case 'int':
272 1
                $property->setValue($object, (int) $value);
273 1
                break;
274 2
            case 'float':
275 1
                $property->setValue($object, (float) $value);
276 1
                break;
277 2
            case 'string':
278 2
                $property->setValue($object, (string) $value);
279 2
                break;
280
        }
281 2
    }
282
283
    /**
284
     * Hydrates the given property with the given array value
285
     *
286
     * @param HydrableObjectInterface $object
287
     * @param ReflectionClass $class
288
     * @param ReflectionProperty $property
289
     * @param ReflectionNamedType $type
290
     * @param mixed $value
291
     *
292
     * @return void
293
     *
294
     * @throws Exception\InvalidValueException
295
     *         If the given value isn't valid.
296
     */
297 8
    private function hydratePropertyWithArray(
298
        HydrableObjectInterface $object,
299
        ReflectionClass $class,
300
        ReflectionProperty $property,
301
        ReflectionNamedType $type,
302
        $value
303
    ) : void {
304 8
        if (!is_array($value)) {
305 7
            throw new Exception\InvalidValueException(sprintf(
306 7
                'The <%s.%s> property only accepts an array.',
307 7
                $class->getShortName(),
308 7
                $property->getName(),
309
            ));
310
        }
311
312 1
        if ('array' === $type->getName()) {
313 1
            $property->setValue($object, $value);
314 1
            return;
315
        }
316
317 1
        $arrayClassName = $type->getName();
318 1
        $array = new $arrayClassName();
319 1
        foreach ($value as $offset => $element) {
320 1
            $array->offsetSet($offset, $element);
321
        }
322
323 1
        $property->setValue($object, $array);
324 1
    }
325
326
    /**
327
     * Hydrates the given property with the given enumerable value
328
     *
329
     * @param HydrableObjectInterface $object
330
     * @param ReflectionClass $class
331
     * @param ReflectionProperty $property
332
     * @param ReflectionNamedType $type
333
     * @param mixed $value
334
     *
335
     * @return void
336
     *
337
     * @throws Exception\InvalidValueException
338
     *         If the given value isn't valid.
339
     */
340 3
    private function hydratePropertyWithEnumerableValue(
341
        HydrableObjectInterface $object,
342
        ReflectionClass $class,
343
        ReflectionProperty $property,
344
        ReflectionNamedType $type,
345
        $value
346
    ) : void {
347 3
        if (!is_int($value) && !is_string($value)) {
348 1
            throw new Exception\InvalidValueException(sprintf(
349 1
                'The <%s.%s> property only accepts an integer or a string.',
350 1
                $class->getShortName(),
351 1
                $property->getName(),
352
            ));
353
        }
354
355
        // support for integer cases...
356 2
        if (is_int($value) || ctype_digit($value)) {
357 1
            $value = '_' . $value;
358
        }
359
360 2
        $enum = $type->getName();
361 2
        $constant = sprintf('%s::%s', $enum, $value);
362 2
        if (!defined($constant)) {
363 1
            throw new Exception\InvalidValueException(sprintf(
364 1
                'The <%s.%s> property only accepts one of the <%s> enum values.',
365 1
                $class->getShortName(),
366 1
                $property->getName(),
367 1
                (new ReflectionClass($enum))->getShortName(),
368
            ));
369
        }
370
371 1
        $property->setValue($object, new $enum(constant($constant)));
372 1
    }
373
374
    /**
375
     * Hydrates the given property with the given date-time value
376
     *
377
     * @param HydrableObjectInterface $object
378
     * @param ReflectionClass $class
379
     * @param ReflectionProperty $property
380
     * @param ReflectionNamedType $type
381
     * @param mixed $value
382
     *
383
     * @return void
384
     *
385
     * @throws Exception\InvalidValueException
386
     *         If the given value isn't valid.
387
     */
388 10
    private function hydratePropertyWithDateTime(
389
        HydrableObjectInterface $object,
390
        ReflectionClass $class,
391
        ReflectionProperty $property,
392
        ReflectionNamedType $type,
393
        $value
394
    ) : void {
395 10
        if (!is_string($value) || false === strtotime($value)) {
396 9
            throw new Exception\InvalidValueException(sprintf(
397 9
                'The <%s.%s> property only accepts a valid date-time string.',
398 9
                $class->getShortName(),
399 9
                $property->getName(),
400
            ));
401
        }
402
403 1
        $dateTimeClassName = $type->getName();
404 1
        $dateTime = new $dateTimeClassName($value);
405 1
        $property->setValue($object, $dateTime);
406 1
    }
407
408
    /**
409
     * Hydrates the given property with the given JSON one-to-one value
410
     *
411
     * @param HydrableObjectInterface $object
412
     * @param ReflectionClass $class
413
     * @param ReflectionProperty $property
414
     * @param ReflectionNamedType $type
415
     * @param mixed $value
416
     *
417
     * @return void
418
     *
419
     * @throws Exception\InvalidValueException
420
     *         If the given value isn't valid.
421
     */
422 3
    public function hydratePropertyWithJsonOneToOneAssociation(
423
        HydrableObjectInterface $object,
424
        ReflectionClass $class,
425
        ReflectionProperty $property,
426
        ReflectionNamedType $type,
427
        $value
428
    ) : void {
429 3
        if (!is_string($value)) {
430 1
            throw new Exception\InvalidValueException(sprintf(
431 1
                'The <%s.%s> property only accepts a string.',
432 1
                $class->getShortName(),
433 1
                $property->getName(),
434
            ));
435
        }
436
437 2
        json_decode(''); // reset previous error...
438 2
        $value = (array) json_decode($value, true);
439 2
        if (JSON_ERROR_NONE <> json_last_error()) {
440 1
            throw new Exception\InvalidValueException(sprintf(
441 1
                'The <%s.%s> property only accepts valid JSON data (%s).',
442 1
                $class->getShortName(),
443 1
                $property->getName(),
444 1
                json_last_error_msg(),
445
            ));
446
        }
447
448 1
        $this->hydratePropertyWithOneToOneAssociation($object, $class, $property, $type, $value);
449 1
    }
450
451
    /**
452
     * Hydrates the given property with the given one-to-one value
453
     *
454
     * @param HydrableObjectInterface $object
455
     * @param ReflectionClass $class
456
     * @param ReflectionProperty $property
457
     * @param ReflectionNamedType $type
458
     * @param mixed $value
459
     *
460
     * @return void
461
     *
462
     * @throws Exception\InvalidValueException
463
     *         If the given value isn't valid.
464
     */
465 9
    private function hydratePropertyWithOneToOneAssociation(
466
        HydrableObjectInterface $object,
467
        ReflectionClass $class,
468
        ReflectionProperty $property,
469
        ReflectionNamedType $type,
470
        $value
471
    ) : void {
472 9
        if (!is_array($value)) {
473 7
            throw new Exception\InvalidValueException(sprintf(
474 7
                'The <%s.%s> property only accepts an array.',
475 7
                $class->getShortName(),
476 7
                $property->getName(),
477
            ));
478
        }
479
480 2
        $childObjectClassName = $type->getName();
481 2
        $childObject = new $childObjectClassName();
482 2
        $this->hydrate($childObject, $value);
483 2
        $property->setValue($object, $childObject);
484 2
    }
485
486
    /**
487
     * Hydrates the given property with the given one-to-many value
488
     *
489
     * @param HydrableObjectInterface $object
490
     * @param ReflectionClass $class
491
     * @param ReflectionProperty $property
492
     * @param ReflectionNamedType $type
493
     * @param mixed $value
494
     *
495
     * @return void
496
     *
497
     * @throws Exception\InvalidValueException
498
     *         If the given value isn't valid.
499
     */
500 8
    private function hydratePropertyWithOneToManyAssociation(
501
        HydrableObjectInterface $object,
502
        ReflectionClass $class,
503
        ReflectionProperty $property,
504
        ReflectionNamedType $type,
505
        $value
506
    ) : void {
507 8
        if (!is_array($value)) {
508 7
            throw new Exception\InvalidValueException(sprintf(
509 7
                'The <%s.%s> property only accepts an array.',
510 7
                $class->getShortName(),
511 7
                $property->getName(),
512
            ));
513
        }
514
515 1
        $objectCollectionClassName = $type->getName();
516 1
        $objectCollection = new $objectCollectionClassName();
517 1
        $collectionObjectClassName = $objectCollectionClassName::T;
518 1
        foreach ($value as $item) {
519 1
            $collectionObject = new $collectionObjectClassName();
520 1
            $this->hydrate($collectionObject, (array) $item);
521 1
            $objectCollection->add($collectionObject);
522
        }
523
524 1
        $property->setValue($object, $objectCollection);
525 1
    }
526
}
527