Passed
Push — main ( 28c7e4...80f234 )
by Anatoly
03:57 queued 01:33
created

Hydrator::hydratePropertyWithBackedEnum()   B

Complexity

Conditions 9
Paths 8

Size

Total Lines 46
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 9

Importance

Changes 0
Metric Value
eloc 24
c 0
b 0
f 0
dl 0
loc 46
ccs 22
cts 22
cp 1
rs 8.0555
cc 9
nc 8
nop 5
crap 9
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 Sunrise\Hydrator\Annotation\Alias;
19
use BackedEnum;
20
use DateInterval;
21
use DateTime;
22
use DateTimeImmutable;
23
use InvalidArgumentException;
24
use ReflectionClass;
25
use ReflectionEnum;
26
use ReflectionProperty;
27
use ReflectionNamedType;
28
use ReflectionUnionType;
29
30
/**
31
 * Import functions
32
 */
33
use function array_key_exists;
34
use function class_exists;
35
use function ctype_digit;
36
use function filter_var;
37
use function get_object_vars;
38
use function implode;
39
use function is_array;
40
use function is_bool;
41
use function is_float;
42
use function is_int;
43
use function is_object;
44
use function is_string;
45
use function is_subclass_of;
46
use function json_decode;
47
use function json_last_error;
48
use function json_last_error_msg;
49
use function sprintf;
50
use function strtotime;
51
52
/**
53
 * Import constants
54
 */
55
use const FILTER_NULL_ON_FAILURE;
56
use const FILTER_VALIDATE_BOOLEAN;
57
use const FILTER_VALIDATE_FLOAT;
58
use const FILTER_VALIDATE_INT;
59
use const JSON_ERROR_NONE;
60
use const JSON_OBJECT_AS_ARRAY;
61
use const PHP_MAJOR_VERSION;
62
63
/**
64
 * Hydrator
65
 */
66
class Hydrator implements HydratorInterface
67
{
68
69
    /**
70
     * @var bool
71
     */
72
    private $aliasSupport = true;
73
74
    /**
75
     * @var SimpleAnnotationReader|null
76
     */
77
    private $annotationReader = null;
78
79
    /**
80
     * Enables or disables the alias support mechanism
81
     *
82
     * @param bool $enabled
83
     *
84
     * @return self
85
     */
86 2
    public function aliasSupport(bool $enabled) : self
87
    {
88 2
        $this->aliasSupport = $enabled;
89
90 2
        return $this;
91
    }
92
93
    /**
94
     * Enables support for annotations
95
     *
96
     * @return self
97
     */
98 3
    public function useAnnotations() : self
99
    {
100
        // @codeCoverageIgnoreStart
101
        if (isset($this->annotationReader)) {
102
            return $this;
103
        }
104
        // @codeCoverageIgnoreEnd
105
106 3
        if (class_exists(SimpleAnnotationReader::class)) {
107 3
            $this->annotationReader = /** @scrutinizer ignore-deprecated */ new SimpleAnnotationReader();
108 3
            $this->annotationReader->addNamespace('Sunrise\Hydrator\Annotation');
109
        }
110
111 3
        return $this;
112
    }
113
114
    /**
115
     * Hydrates the given object with the given data
116
     *
117
     * @param class-string<T>|T $object
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<T>|T at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<T>|T.
Loading history...
118
     * @param array|object $data
119
     *
120
     * @return T
121
     *
122
     * @throws Exception\HydrationException
123
     *         If the object cannot be hydrated.
124
     *
125
     * @throws InvalidArgumentException
126
     *         If the data isn't valid.
127
     *
128
     * @throws Exception\UntypedPropertyException
129
     *         If one of the object properties isn't typed.
130
     *
131
     * @throws Exception\UnsupportedPropertyTypeException
132
     *         If one of the object properties contains an unsupported type.
133
     *
134
     * @throws Exception\MissingRequiredValueException
135
     *         If the given data doesn't contain required value.
136
     *
137
     * @throws Exception\InvalidValueException
138
     *         If the given data contains an invalid value.
139
     *
140
     * @template T
141
     */
142 82
    public function hydrate($object, $data) : object
143
    {
144 82
        if (is_object($data)) {
145 4
            $data = get_object_vars($data);
146
        }
147
148 82
        if (!is_array($data)) {
149 1
            throw new InvalidArgumentException(sprintf(
150
                'The %s(data) parameter expects an associative array or object.',
151
                __METHOD__
152
            ));
153
        }
154
155 81
        $object = $this->initializeObject($object);
156
157 79
        $class = new ReflectionClass($object);
158 79
        $properties = $class->getProperties();
159 79
        foreach ($properties as $property) {
160
            // statical properties cannot be hydrated...
161 79
            if ($property->isStatic()) {
162 1
                continue;
163
            }
164
165 78
            $property->setAccessible(true);
166
167 78
            if (!$property->hasType()) {
168 1
                throw new Exception\UntypedPropertyException(sprintf(
169
                    'The %s.%s property is not typed.',
170 1
                    $class->getShortName(),
171 1
                    $property->getName()
172
                ));
173
            }
174
175 77
            if ($property->getType() instanceof ReflectionUnionType) {
176 1
                throw new Exception\UnsupportedPropertyTypeException(sprintf(
177
                    'The %s.%s property contains an union type that is not supported.',
178 1
                    $class->getShortName(),
179 1
                    $property->getName()
180
                ));
181
            }
182
183 76
            $key = $property->getName();
184 76
            if ($this->aliasSupport && !array_key_exists($key, $data)) {
185 5
                $alias = $this->getPropertyAlias($property);
186 5
                if (isset($alias)) {
187 2
                    $key = $alias->value;
188
                }
189
            }
190
191 76
            if (!array_key_exists($key, $data)) {
192 5
                if (!$property->isInitialized($object)) {
193 4
                    throw new Exception\MissingRequiredValueException($property, sprintf(
194
                        'The %s.%s property is required.',
195 4
                        $class->getShortName(),
196 4
                        $property->getName()
197
                    ));
198
                }
199
200 1
                continue;
201
            }
202
203 71
            $this->hydrateProperty($object, $class, $property, $property->getType(), $data[$key]);
204
        }
205
206 53
        return $object;
207
    }
208
209
    /**
210
     * Hydrates the given object with the given JSON
211
     *
212
     * @param class-string<T>|T $object
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<T>|T at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<T>|T.
Loading history...
213
     * @param string $json
214
     * @param ?int $flags
215
     *
216
     * @return T
217
     *
218
     * @throws Exception\HydrationException
219
     *         If the object cannot be hydrated.
220
     *
221
     * @throws InvalidArgumentException
222
     *         If the JSON cannot be decoded.
223
     *
224
     * @template T
225
     */
226 4
    public function hydrateWithJson($object, string $json, ?int $flags = null) : object
227
    {
228 4
        if (null === $flags) {
229 3
            $flags = JSON_OBJECT_AS_ARRAY;
230
        }
231
232 4
        json_decode('');
233 4
        $data = json_decode($json, null, 512, $flags);
234 4
        if (JSON_ERROR_NONE <> json_last_error()) {
235 1
            throw new InvalidArgumentException(sprintf(
236
                'Unable to decode JSON: %s',
237 1
                json_last_error_msg()
238
            ));
239
        }
240
241 3
        return $this->hydrate($object, $data);
242
    }
243
244
    /**
245
     * Initializes the given object
246
     *
247
     * @param class-string<T>|T $object
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<T>|T at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<T>|T.
Loading history...
248
     *
249
     * @return T
250
     *
251
     * @throws InvalidArgumentException
252
     *         If the object cannot be initialized.
253
     *
254
     * @template T
255
     */
256 81
    private function initializeObject($object) : object
257
    {
258 81
        if (is_object($object)) {
259 1
            return $object;
260
        }
261
262 80
        if (!is_string($object) || !class_exists($object)) {
263 1
            throw new InvalidArgumentException(sprintf(
264
                'The %s::hydrate() method expects an object or name of an existing class.',
265
                __CLASS__
266
            ));
267
        }
268
269 79
        $class = new ReflectionClass($object);
270 79
        $constructor = $class->getConstructor();
271 79
        if (isset($constructor) && $constructor->getNumberOfRequiredParameters() > 0) {
272 1
            throw new InvalidArgumentException(sprintf(
273
                'The %s object cannot be hydrated because its constructor has required parameters.',
274 1
                $class->getName()
275
            ));
276
        }
277
278
        /** @var T */
279 78
        return $class->newInstance();
280
    }
281
282
    /**
283
     * Gets an alias for the given property
284
     *
285
     * @param ReflectionProperty $property
286
     *
287
     * @return Alias|null
288
     */
289 5
    private function getPropertyAlias(ReflectionProperty $property) : ?Alias
290
    {
291 5
        if (PHP_MAJOR_VERSION >= 8) {
292 5
            $attributes = $property->getAttributes(Alias::class);
293 5
            if (isset($attributes[0])) {
294 1
                return $attributes[0]->newInstance();
295
            }
296
        }
297
298 4
        if (isset($this->annotationReader)) {
299 1
            $annotation = $this->annotationReader->getPropertyAnnotation($property, Alias::class);
300 1
            if (isset($annotation)) {
301 1
                return $annotation;
302
            }
303
        }
304
305 3
        return null;
306
    }
307
308
    /**
309
     * Hydrates the given property with the given value
310
     *
311
     * @param object $object
312
     * @param ReflectionClass $class
313
     * @param ReflectionProperty $property
314
     * @param ReflectionNamedType $type
315
     * @param mixed $value
316
     *
317
     * @return void
318
     *
319
     * @throws Exception\InvalidValueException
320
     *         If the given value isn't valid.
321
     *
322
     * @throws Exception\UnsupportedPropertyTypeException
323
     *         If the given property contains an unsupported type.
324
     */
325 71
    private function hydrateProperty(
326
        object $object,
327
        ReflectionClass $class,
328
        ReflectionProperty $property,
329
        ReflectionNamedType $type,
330
        $value
331
    ) : void {
332
        // an empty string for a non-string type is always processes as null...
333 71
        if ('' === $value && 'string' !== $type->getName()) {
334 1
            $value = null;
335
        }
336
337 71
        if (null === $value) {
338 3
            $this->hydratePropertyWithNull($object, $class, $property, $type);
339 2
            return;
340
        }
341
342 68
        if ('bool' === $type->getName()) {
343 11
            $this->hydratePropertyWithBool($object, $class, $property, $type, $value);
344 10
            return;
345
        }
346
347 57
        if ('int' === $type->getName()) {
348 3
            $this->hydratePropertyWithInt($object, $class, $property, $type, $value);
349 2
            return;
350
        }
351
352 54
        if ('float' === $type->getName()) {
353 5
            $this->hydratePropertyWithFloat($object, $class, $property, $type, $value);
354 4
            return;
355
        }
356
357 49
        if ('string' === $type->getName()) {
358 17
            $this->hydratePropertyWithString($object, $class, $property, $type, $value);
359 15
            return;
360
        }
361
362 38
        if ('array' === $type->getName()) {
363 2
            $this->hydratePropertyWithArray($object, $class, $property, $type, $value);
364 1
            return;
365
        }
366
367 36
        if ('object' === $type->getName()) {
368 2
            $this->hydratePropertyWithObject($object, $class, $property, $type, $value);
369 1
            return;
370
        }
371
372 34
        if (DateTime::class === $type->getName()) {
373 4
            $this->hydratePropertyWithDateTime($object, $class, $property, $type, $value);
374 3
            return;
375
        }
376
377 30
        if (DateTimeImmutable::class === $type->getName()) {
378 4
            $this->hydratePropertyWithDateTime($object, $class, $property, $type, $value);
379 3
            return;
380
        }
381
382 26
        if (DateInterval::class === $type->getName()) {
383 3
            $this->hydratePropertyWithDateInterval($object, $class, $property, $type, $value);
384 1
            return;
385
        }
386
387 23
        if (is_subclass_of($type->getName(), BackedEnum::class)) {
388 15
            $this->hydratePropertyWithBackedEnum($object, $class, $property, $type, $value);
389 11
            return;
390
        }
391
392 10
        if (is_subclass_of($type->getName(), ObjectCollectionInterface::class)) {
393 6
            $this->hydratePropertyWithManyAssociations($object, $class, $property, $type, $value);
394 4
            return;
395
        }
396
397 6
        if (class_exists($type->getName())) {
398 5
            $this->hydratePropertyWithOneAssociation($object, $class, $property, $type, $value);
399 4
            return;
400
        }
401
402 1
        throw new Exception\UnsupportedPropertyTypeException(sprintf(
403
            'The %s.%s property contains an unsupported type %s.',
404 1
            $class->getShortName(),
405 1
            $property->getName(),
406 1
            $type->getName()
407
        ));
408
    }
409
410
    /**
411
     * Hydrates the given property with null
412
     *
413
     * @param object $object
414
     * @param ReflectionClass $class
415
     * @param ReflectionProperty $property
416
     * @param ReflectionNamedType $type
417
     *
418
     * @return void
419
     *
420
     * @throws Exception\InvalidValueException
421
     *         If the given value isn't valid.
422
     */
423 3
    private function hydratePropertyWithNull(
424
        object $object,
425
        ReflectionClass $class,
426
        ReflectionProperty $property,
427
        ReflectionNamedType $type
428
    ) : void {
429 3
        if (!$type->allowsNull()) {
430 1
            throw new Exception\InvalidValueException($property, sprintf(
431
                'The %s.%s property cannot accept null.',
432 1
                $class->getShortName(),
433 1
                $property->getName()
434
            ));
435
        }
436
437 2
        $property->setValue($object, null);
438
    }
439
440
    /**
441
     * Hydrates the given property with the given boolean value
442
     *
443
     * @param object $object
444
     * @param ReflectionClass $class
445
     * @param ReflectionProperty $property
446
     * @param ReflectionNamedType $type
447
     * @param mixed $value
448
     *
449
     * @return void
450
     *
451
     * @throws Exception\InvalidValueException
452
     *         If the given value isn't valid.
453
     */
454 11
    private function hydratePropertyWithBool(
455
        object $object,
456
        ReflectionClass $class,
457
        ReflectionProperty $property,
458
        ReflectionNamedType $type,
459
        $value
460
    ) : void {
461 11
        if (!is_bool($value)) {
462
            // if the value isn't boolean, then we will use filter_var, because it will give us the ability to specify
463
            // boolean values as strings. this behavior is great for html forms. details at:
464
            // https://github.com/php/php-src/blob/b7d90f09d4a1688f2692f2fa9067d0a07f78cc7d/ext/filter/logical_filters.c#L273
465 9
            $value = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
466
467 9
            if (!isset($value)) {
468 1
                throw new Exception\InvalidValueException($property, sprintf(
469
                    'The %s.%s property expects a boolean.',
470 1
                    $class->getShortName(),
471 1
                    $property->getName()
472
                ));
473
            }
474
        }
475
476 10
        $property->setValue($object, $value);
477
    }
478
479
    /**
480
     * Hydrates the given property with the given integer number
481
     *
482
     * @param object $object
483
     * @param ReflectionClass $class
484
     * @param ReflectionProperty $property
485
     * @param ReflectionNamedType $type
486
     * @param mixed $value
487
     *
488
     * @return void
489
     *
490
     * @throws Exception\InvalidValueException
491
     *         If the given value isn't valid.
492
     */
493 3
    private function hydratePropertyWithInt(
494
        object $object,
495
        ReflectionClass $class,
496
        ReflectionProperty $property,
497
        ReflectionNamedType $type,
498
        $value
499
    ) : void {
500 3
        if (!is_int($value)) {
501
            // it's senseless to convert the value type if it's not a number, so we will use filter_var to correct
502
            // converting the value type to int. also remember that string numbers must be between PHP_INT_MIN and
503
            // PHP_INT_MAX, otherwise the result will be null. this behavior is great for html forms. details at:
504
            // https://github.com/php/php-src/blob/b7d90f09d4a1688f2692f2fa9067d0a07f78cc7d/ext/filter/logical_filters.c#L197
505
            // https://github.com/php/php-src/blob/b7d90f09d4a1688f2692f2fa9067d0a07f78cc7d/ext/filter/logical_filters.c#L94
506 2
            $value = filter_var($value, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE);
507
508 2
            if (!isset($value)) {
509 1
                throw new Exception\InvalidValueException($property, sprintf(
510
                    'The %s.%s property expects an integer.',
511 1
                    $class->getShortName(),
512 1
                    $property->getName()
513
                ));
514
            }
515
        }
516
517 2
        $property->setValue($object, $value);
518
    }
519
520
    /**
521
     * Hydrates the given property with the given number
522
     *
523
     * @param object $object
524
     * @param ReflectionClass $class
525
     * @param ReflectionProperty $property
526
     * @param ReflectionNamedType $type
527
     * @param mixed $value
528
     *
529
     * @return void
530
     *
531
     * @throws Exception\InvalidValueException
532
     *         If the given value isn't valid.
533
     */
534 5
    private function hydratePropertyWithFloat(
535
        object $object,
536
        ReflectionClass $class,
537
        ReflectionProperty $property,
538
        ReflectionNamedType $type,
539
        $value
540
    ) : void {
541 5
        if (!is_float($value)) {
542
            // it's senseless to convert the value type if it's not a number, so we will use filter_var to correct
543
            // converting the value type to float. this behavior is great for html forms. details at:
544
            // https://github.com/php/php-src/blob/b7d90f09d4a1688f2692f2fa9067d0a07f78cc7d/ext/filter/logical_filters.c#L342
545 4
            $value = filter_var($value, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE);
546
547 4
            if (!isset($value)) {
548 1
                throw new Exception\InvalidValueException($property, sprintf(
549
                    'The %s.%s property expects a number.',
550 1
                    $class->getShortName(),
551 1
                    $property->getName()
552
                ));
553
            }
554
        }
555
556 4
        $property->setValue($object, $value);
557
    }
558
559
    /**
560
     * Hydrates the given property with the given string
561
     *
562
     * @param object $object
563
     * @param ReflectionClass $class
564
     * @param ReflectionProperty $property
565
     * @param ReflectionNamedType $type
566
     * @param mixed $value
567
     *
568
     * @return void
569
     *
570
     * @throws Exception\InvalidValueException
571
     *         If the given value isn't valid.
572
     */
573 17
    private function hydratePropertyWithString(
574
        object $object,
575
        ReflectionClass $class,
576
        ReflectionProperty $property,
577
        ReflectionNamedType $type,
578
        $value
579
    ) : void {
580 17
        if (!is_string($value)) {
581 2
            throw new Exception\InvalidValueException($property, sprintf(
582
                'The %s.%s property expects a string.',
583 2
                $class->getShortName(),
584 2
                $property->getName()
585
            ));
586
        }
587
588 15
        $property->setValue($object, $value);
589
    }
590
591
    /**
592
     * Hydrates the given property with the given array
593
     *
594
     * @param object $object
595
     * @param ReflectionClass $class
596
     * @param ReflectionProperty $property
597
     * @param ReflectionNamedType $type
598
     * @param mixed $value
599
     *
600
     * @return void
601
     *
602
     * @throws Exception\InvalidValueException
603
     *         If the given value isn't valid.
604
     */
605 2
    private function hydratePropertyWithArray(
606
        object $object,
607
        ReflectionClass $class,
608
        ReflectionProperty $property,
609
        ReflectionNamedType $type,
610
        $value
611
    ) : void {
612 2
        if (!is_array($value)) {
613 1
            throw new Exception\InvalidValueException($property, sprintf(
614
                'The %s.%s property expects an array.',
615 1
                $class->getShortName(),
616 1
                $property->getName()
617
            ));
618
        }
619
620 1
        $property->setValue($object, $value);
621
    }
622
623
    /**
624
     * Hydrates the given property with the given object
625
     *
626
     * @param object $object
627
     * @param ReflectionClass $class
628
     * @param ReflectionProperty $property
629
     * @param ReflectionNamedType $type
630
     * @param mixed $value
631
     *
632
     * @return void
633
     *
634
     * @throws Exception\InvalidValueException
635
     *         If the given value isn't valid.
636
     */
637 2
    private function hydratePropertyWithObject(
638
        object $object,
639
        ReflectionClass $class,
640
        ReflectionProperty $property,
641
        ReflectionNamedType $type,
642
        $value
643
    ) : void {
644 2
        if (!is_object($value)) {
645 1
            throw new Exception\InvalidValueException($property, sprintf(
646
                'The %s.%s property expects an object.',
647 1
                $class->getShortName(),
648 1
                $property->getName()
649
            ));
650
        }
651
652 1
        $property->setValue($object, $value);
653
    }
654
655
    /**
656
     * Hydrates the given property with the given date-time
657
     *
658
     * @param object $object
659
     * @param ReflectionClass $class
660
     * @param ReflectionProperty $property
661
     * @param ReflectionNamedType $type
662
     * @param mixed $value
663
     *
664
     * @return void
665
     *
666
     * @throws Exception\InvalidValueException
667
     *         If the given value isn't valid.
668
     */
669 8
    private function hydratePropertyWithDateTime(
670
        object $object,
671
        ReflectionClass $class,
672
        ReflectionProperty $property,
673
        ReflectionNamedType $type,
674
        $value
675
    ) : void {
676
        /** @var class-string<DateTime|DateTimeImmutable> */
677 8
        $className = $type->getName();
678
679 8
        if (is_int($value)) {
680 2
            $property->setValue($object, (new $className)->setTimestamp($value));
681 2
            return;
682
        }
683
684 6
        if (is_string($value) && ctype_digit($value)) {
685 2
            $property->setValue($object, (new $className)->setTimestamp((int) $value));
686 2
            return;
687
        }
688
689 4
        if (is_string($value) && false !== strtotime($value)) {
690 2
            $property->setValue($object, new $className($value));
691 2
            return;
692
        }
693
694 2
        throw new Exception\InvalidValueException($property, sprintf(
695
            'The %s.%s property expects a valid date-time string or timestamp.',
696 2
            $class->getShortName(),
697 2
            $property->getName()
698
        ));
699
    }
700
701
    /**
702
     * Hydrates the given property with the given date-interval
703
     *
704
     * @param object $object
705
     * @param ReflectionClass $class
706
     * @param ReflectionProperty $property
707
     * @param ReflectionNamedType $type
708
     * @param mixed $value
709
     *
710
     * @return void
711
     *
712
     * @throws Exception\InvalidValueException
713
     *         If the given value isn't valid.
714
     */
715 3
    private function hydratePropertyWithDateInterval(
716
        object $object,
717
        ReflectionClass $class,
718
        ReflectionProperty $property,
719
        ReflectionNamedType $type,
720
        $value
721
    ) : void {
722 3
        if (!is_string($value)) {
723 1
            throw new Exception\InvalidValueException($property, sprintf(
724
                'The %s.%s property expects a string.',
725 1
                $class->getShortName(),
726 1
                $property->getName()
727
            ));
728
        }
729
730
        /** @var class-string<DateInterval> */
731 2
        $className = $type->getName();
732
733
        try {
734 2
            $dateInterval = new $className($value);
735 1
        } catch (\Exception $e) {
736 1
            throw new Exception\InvalidValueException($property, sprintf(
737
                'The %s.%s property expects a valid date-interval string based on ISO 8601.',
738 1
                $class->getShortName(),
739 1
                $property->getName()
740
            ));
741
        }
742
743 1
        $property->setValue($object, $dateInterval);
744
    }
745
746
    /**
747
     * Hydrates the given property with the given backed-enum
748
     *
749
     * @param object $object
750
     * @param ReflectionClass $class
751
     * @param ReflectionProperty $property
752
     * @param ReflectionNamedType $type
753
     * @param mixed $value
754
     *
755
     * @return void
756
     *
757
     * @throws Exception\InvalidValueException
758
     *         If the given value isn't valid.
759
     */
760 15
    private function hydratePropertyWithBackedEnum(
761
        object $object,
762
        ReflectionClass $class,
763
        ReflectionProperty $property,
764
        ReflectionNamedType $type,
765
        $value
766
    ) : void {
767
        /** @var class-string<BackedEnum> */
768 15
        $enumName = $type->getName();
769 15
        $enumReflection = new ReflectionEnum($enumName);
770
771
        /** @var ReflectionNamedType */
772 15
        $enumType = $enumReflection->getBackingType();
773 15
        $enumTypeName = $enumType->getName();
774
775
        // support for HTML forms...
776 15
        if ('int' === $enumTypeName && is_string($value)) {
777 4
            $value = filter_var($value, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE);
778
        }
779
780 15
        if (('int' === $enumTypeName && !is_int($value)) ||
781 15
            ('string' === $enumTypeName && !is_string($value))) {
782 2
            throw new Exception\InvalidValueException($property, sprintf(
783
                'The %s.%s property expects the following type: %s.',
784 2
                $class->getShortName(),
785 2
                $property->getName(),
786
                $enumTypeName
787
            ));
788
        }
789
790 13
        $enum = $enumName::tryFrom($value);
791 13
        if (!isset($enum)) {
792 2
            $allowedCases = [];
793 2
            foreach ($enumName::cases() as $case) {
794 2
                $allowedCases[] = $case->value;
795
            }
796
797 2
            throw new Exception\InvalidValueException($property, sprintf(
798
                'The %s.%s property expects one of the following values: %s.',
799 2
                $class->getShortName(),
800 2
                $property->getName(),
801 2
                implode(', ', $allowedCases)
802
            ));
803
        }
804
805 11
        $property->setValue($object, $enum);
806
    }
807
808
    /**
809
     * Hydrates the given property with the given one association
810
     *
811
     * @param object $object
812
     * @param ReflectionClass $class
813
     * @param ReflectionProperty $property
814
     * @param ReflectionNamedType $type
815
     * @param mixed $value
816
     *
817
     * @return void
818
     *
819
     * @throws Exception\InvalidValueException
820
     *         If the given value isn't valid.
821
     */
822 5
    private function hydratePropertyWithOneAssociation(
823
        object $object,
824
        ReflectionClass $class,
825
        ReflectionProperty $property,
826
        ReflectionNamedType $type,
827
        $value
828
    ) : void {
829 5
        if (!is_array($value) && !is_object($value)) {
830 1
            throw new Exception\InvalidValueException($property, sprintf(
831
                'The %s.%s property expects an associative array or object.',
832 1
                $class->getShortName(),
833 1
                $property->getName()
834
            ));
835
        }
836
837 4
        $property->setValue($object, $this->hydrate($type->getName(), $value));
838
    }
839
840
    /**
841
     * Hydrates the given property with the given many associations
842
     *
843
     * @param object $object
844
     * @param ReflectionClass $class
845
     * @param ReflectionProperty $property
846
     * @param ReflectionNamedType $type
847
     * @param mixed $value
848
     *
849
     * @return void
850
     *
851
     * @throws Exception\InvalidValueException
852
     *         If the given value isn't valid.
853
     */
854 6
    private function hydratePropertyWithManyAssociations(
855
        object $object,
856
        ReflectionClass $class,
857
        ReflectionProperty $property,
858
        ReflectionNamedType $type,
859
        $value
860
    ) : void {
861 6
        if (!is_array($value) && !is_object($value)) {
862 1
            throw new Exception\InvalidValueException($property, sprintf(
863
                'The %s.%s property expects an associative array or object.',
864 1
                $class->getShortName(),
865 1
                $property->getName()
866
            ));
867
        }
868
869 5
        $className = $type->getName();
870 5
        $collection = new $className();
871 5
        foreach ($value as $key => $child) {
872 5
            if (!is_array($child) && !is_object($child)) {
873 1
                throw new Exception\InvalidValueException($property, sprintf(
874
                    'The %s.%s[%s] property expects an associative array or object.',
875 1
                    $class->getShortName(),
876 1
                    $property->getName(),
877
                    $key
878
                ));
879
            }
880
881 4
            $collection->add($key, $this->hydrate($collection->getItemClassName(), $child));
882
        }
883
884 4
        $property->setValue($object, $collection);
885
    }
886
}
887