Test Failed
Pull Request — main (#20)
by Anatoly
31:11 queued 02:30
created

Hydrator::hydratePropertyWithEnum()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 26
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 12
c 0
b 0
f 0
dl 0
loc 26
ccs 5
cts 5
cp 1
rs 9.8666
cc 3
nc 3
nop 5
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 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
     * @codeCoverageIgnoreStart
99
     */
100
    public function useAnnotations() : self
101
    {
102
        if (isset($this->annotationReader)) {
103
            return $this;
104
        }
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 InvalidArgumentException
123
     *         If the data isn't valid.
124
     *
125
     * @throws Exception\HydrationException
126
     *         If the object cannot be hydrated.
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 InvalidArgumentException
219
     *         If the JSON cannot be decoded.
220
     *
221
     * @throws Exception\HydrationException
222
     *         If the object cannot be hydrated.
223
     *
224
     * @template T
225
     */
226 4
    public function hydrateWithJson($object, string $json, ?int $flags = JSON_OBJECT_AS_ARRAY) : object
227
    {
228 4
        json_decode('');
229 3
        $data = json_decode($json, null, 512, $flags);
230
        if (JSON_ERROR_NONE <> json_last_error()) {
231
            throw new InvalidArgumentException(sprintf(
232 4
                'Unable to decode JSON: %s',
233 4
                json_last_error_msg()
234 4
            ));
235 1
        }
236
237 1
        return $this->hydrate($object, $data);
238
    }
239
240
    /**
241 3
     * Initializes the given object
242
     *
243
     * @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...
244
     *
245
     * @return T
246
     *
247
     * @throws InvalidArgumentException
248
     *         If the object cannot be initialized.
249
     *
250
     * @template T
251
     */
252
    private function initializeObject($object) : object
253
    {
254
        if (is_object($object)) {
255
            return $object;
256 81
        }
257
258 81
        if (!is_string($object) || !class_exists($object)) {
259 1
            throw new InvalidArgumentException(sprintf(
260
                'The %s::hydrate() method expects an object or name of an existing class.',
261
                __CLASS__
262 80
            ));
263 1
        }
264
265
        $class = new ReflectionClass($object);
266
        $constructor = $class->getConstructor();
267
        if (isset($constructor) && $constructor->getNumberOfRequiredParameters() > 0) {
268
            throw new InvalidArgumentException(sprintf(
269 79
                'The %s object cannot be hydrated because its constructor has required parameters.',
270 79
                $class->getName()
271 79
            ));
272 1
        }
273
274 1
        /** @var T */
275
        return $class->newInstance();
276
    }
277
278
    /**
279 78
     * Gets an alias for the given property
280
     *
281
     * @param ReflectionProperty $property
282
     *
283
     * @return Alias|null
284
     */
285
    private function getPropertyAlias(ReflectionProperty $property) : ?Alias
286
    {
287
        if (PHP_MAJOR_VERSION >= 8) {
288
            $attributes = $property->getAttributes(Alias::class);
289 5
            if (isset($attributes[0])) {
290
                return $attributes[0]->newInstance();
291 5
            }
292 5
        }
293 5
294 1
        if (isset($this->annotationReader)) {
295
            $annotation = $this->annotationReader->getPropertyAnnotation($property, Alias::class);
296
            if (isset($annotation)) {
297
                return $annotation;
298 4
            }
299 1
        }
300 1
301 1
        return null;
302
    }
303
304
    /**
305 3
     * Hydrates the given property with the given value
306
     *
307
     * @param object $object
308
     * @param ReflectionClass $class
309
     * @param ReflectionProperty $property
310
     * @param ReflectionNamedType $type
311
     * @param mixed $value
312
     *
313
     * @return void
314
     *
315
     * @throws Exception\InvalidValueException
316
     *         If the given value isn't valid.
317
     *
318
     * @throws Exception\UnsupportedPropertyTypeException
319
     *         If the given property contains an unsupported type.
320
     */
321
    private function hydrateProperty(
322
        object $object,
323
        ReflectionClass $class,
324
        ReflectionProperty $property,
325 71
        ReflectionNamedType $type,
326
        $value
327
    ) : void {
328
        // an empty string for a non-string type is always processes as null...
329
        if ('' === $value && 'string' !== $type->getName()) {
330
            $value = null;
331
        }
332
333 71
        if (null === $value) {
334 1
            $this->hydratePropertyWithNull($object, $class, $property, $type);
335
            return;
336
        }
337 71
338 3
        if ('bool' === $type->getName()) {
339 2
            $this->hydratePropertyWithBool($object, $class, $property, $type, $value);
340
            return;
341
        }
342 68
343 11
        if ('int' === $type->getName()) {
344 10
            $this->hydratePropertyWithInt($object, $class, $property, $type, $value);
345
            return;
346
        }
347 57
348 3
        if ('float' === $type->getName()) {
349 2
            $this->hydratePropertyWithFloat($object, $class, $property, $type, $value);
350
            return;
351
        }
352 54
353 5
        if ('string' === $type->getName()) {
354 4
            $this->hydratePropertyWithString($object, $class, $property, $type, $value);
355
            return;
356
        }
357 49
358 17
        if ('array' === $type->getName()) {
359 15
            $this->hydratePropertyWithArray($object, $class, $property, $type, $value);
360
            return;
361
        }
362 38
363 2
        if ('object' === $type->getName()) {
364 1
            $this->hydratePropertyWithObject($object, $class, $property, $type, $value);
365
            return;
366
        }
367 36
368 2
        if (DateTime::class === $type->getName()) {
369 1
            $this->hydratePropertyWithDateTime($object, $class, $property, $type, $value);
370
            return;
371
        }
372 34
373 4
        if (DateTimeImmutable::class === $type->getName()) {
374 3
            $this->hydratePropertyWithDateTime($object, $class, $property, $type, $value);
375
            return;
376
        }
377 30
378 4
        if (DateInterval::class === $type->getName()) {
379 3
            $this->hydratePropertyWithDateInterval($object, $class, $property, $type, $value);
380
            return;
381
        }
382 26
383 3
        if (is_subclass_of($type->getName(), BackedEnum::class)) {
384 1
            $this->hydratePropertyWithBackedEnum($object, $class, $property, $type, $value);
385
            return;
386
        }
387 23
388 15
        if (is_subclass_of($type->getName(), Enum::class)) {
389 11
            $this->hydratePropertyWithEnum($object, $class, $property, $type, $value);
390
            return;
391
        }
392 10
393 6
        if (is_subclass_of($type->getName(), ObjectCollectionInterface::class)) {
394 4
            $this->hydratePropertyWithManyAssociations($object, $class, $property, $type, $value);
395
            return;
396
        }
397 6
398 5
        if (class_exists($type->getName())) {
399 4
            $this->hydratePropertyWithOneAssociation($object, $class, $property, $type, $value);
400
            return;
401
        }
402 1
403
        throw new Exception\UnsupportedPropertyTypeException(sprintf(
404 1
            'The %s.%s property contains an unsupported type %s.',
405 1
            $class->getShortName(),
406 1
            $property->getName(),
407
            $type->getName()
408
        ));
409
    }
410
411
    /**
412
     * Hydrates the given property with null
413
     *
414
     * @param object $object
415
     * @param ReflectionClass $class
416
     * @param ReflectionProperty $property
417
     * @param ReflectionNamedType $type
418
     *
419
     * @return void
420
     *
421
     * @throws Exception\InvalidValueException
422
     *         If the given value isn't valid.
423 3
     */
424
    private function hydratePropertyWithNull(
425
        object $object,
426
        ReflectionClass $class,
427
        ReflectionProperty $property,
428
        ReflectionNamedType $type
429 3
    ) : void {
430 1
        if (!$type->allowsNull()) {
431
            throw new Exception\InvalidValueException($property, sprintf(
432 1
                'The %s.%s property cannot accept null.',
433 1
                $class->getShortName(),
434
                $property->getName()
435
            ));
436
        }
437 2
438
        $property->setValue($object, null);
439
    }
440
441
    /**
442
     * Hydrates the given property with the given boolean value
443
     *
444
     * @param object $object
445
     * @param ReflectionClass $class
446
     * @param ReflectionProperty $property
447
     * @param ReflectionNamedType $type
448
     * @param mixed $value
449
     *
450
     * @return void
451
     *
452
     * @throws Exception\InvalidValueException
453
     *         If the given value isn't valid.
454 11
     */
455
    private function hydratePropertyWithBool(
456
        object $object,
457
        ReflectionClass $class,
458
        ReflectionProperty $property,
459
        ReflectionNamedType $type,
460
        $value
461 11
    ) : void {
462
        if (!is_bool($value)) {
463
            // if the value isn't boolean, then we will use filter_var, because it will give us the ability to specify
464
            // boolean values as strings. this behavior is great for html forms. details at:
465 9
            // https://github.com/php/php-src/blob/b7d90f09d4a1688f2692f2fa9067d0a07f78cc7d/ext/filter/logical_filters.c#L273
466
            $value = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
467 9
468 1
            if (!isset($value)) {
469
                throw new Exception\InvalidValueException($property, sprintf(
470 1
                    'The %s.%s property expects a boolean.',
471 1
                    $class->getShortName(),
472
                    $property->getName()
473
                ));
474
            }
475
        }
476 10
477
        $property->setValue($object, $value);
478
    }
479
480
    /**
481
     * Hydrates the given property with the given integer number
482
     *
483
     * @param object $object
484
     * @param ReflectionClass $class
485
     * @param ReflectionProperty $property
486
     * @param ReflectionNamedType $type
487
     * @param mixed $value
488
     *
489
     * @return void
490
     *
491
     * @throws Exception\InvalidValueException
492
     *         If the given value isn't valid.
493 3
     */
494
    private function hydratePropertyWithInt(
495
        object $object,
496
        ReflectionClass $class,
497
        ReflectionProperty $property,
498
        ReflectionNamedType $type,
499
        $value
500 3
    ) : void {
501
        if (!is_int($value)) {
502
            // it's senseless to convert the value type if it's not a number, so we will use filter_var to correct
503
            // converting the value type to int. also remember that string numbers must be between PHP_INT_MIN and
504
            // PHP_INT_MAX, otherwise the result will be null. this behavior is great for html forms. details at:
505
            // https://github.com/php/php-src/blob/b7d90f09d4a1688f2692f2fa9067d0a07f78cc7d/ext/filter/logical_filters.c#L197
506 2
            // https://github.com/php/php-src/blob/b7d90f09d4a1688f2692f2fa9067d0a07f78cc7d/ext/filter/logical_filters.c#L94
507
            $value = filter_var($value, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE);
508 2
509 1
            if (!isset($value)) {
510
                throw new Exception\InvalidValueException($property, sprintf(
511 1
                    'The %s.%s property expects an integer.',
512 1
                    $class->getShortName(),
513
                    $property->getName()
514
                ));
515
            }
516
        }
517 2
518
        $property->setValue($object, $value);
519
    }
520
521
    /**
522
     * Hydrates the given property with the given number
523
     *
524
     * @param object $object
525
     * @param ReflectionClass $class
526
     * @param ReflectionProperty $property
527
     * @param ReflectionNamedType $type
528
     * @param mixed $value
529
     *
530
     * @return void
531
     *
532
     * @throws Exception\InvalidValueException
533
     *         If the given value isn't valid.
534 5
     */
535
    private function hydratePropertyWithFloat(
536
        object $object,
537
        ReflectionClass $class,
538
        ReflectionProperty $property,
539
        ReflectionNamedType $type,
540
        $value
541 5
    ) : void {
542
        if (!is_float($value)) {
543
            // it's senseless to convert the value type if it's not a number, so we will use filter_var to correct
544
            // converting the value type to float. this behavior is great for html forms. details at:
545 4
            // https://github.com/php/php-src/blob/b7d90f09d4a1688f2692f2fa9067d0a07f78cc7d/ext/filter/logical_filters.c#L342
546
            $value = filter_var($value, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE);
547 4
548 1
            if (!isset($value)) {
549
                throw new Exception\InvalidValueException($property, sprintf(
550 1
                    'The %s.%s property expects a number.',
551 1
                    $class->getShortName(),
552
                    $property->getName()
553
                ));
554
            }
555
        }
556 4
557
        $property->setValue($object, $value);
558
    }
559
560
    /**
561
     * Hydrates the given property with the given string
562
     *
563
     * @param object $object
564
     * @param ReflectionClass $class
565
     * @param ReflectionProperty $property
566
     * @param ReflectionNamedType $type
567
     * @param mixed $value
568
     *
569
     * @return void
570
     *
571
     * @throws Exception\InvalidValueException
572
     *         If the given value isn't valid.
573 17
     */
574
    private function hydratePropertyWithString(
575
        object $object,
576
        ReflectionClass $class,
577
        ReflectionProperty $property,
578
        ReflectionNamedType $type,
579
        $value
580 17
    ) : void {
581 2
        if (!is_string($value)) {
582
            throw new Exception\InvalidValueException($property, sprintf(
583 2
                'The %s.%s property expects a string.',
584 2
                $class->getShortName(),
585
                $property->getName()
586
            ));
587
        }
588 15
589
        $property->setValue($object, $value);
590
    }
591
592
    /**
593
     * Hydrates the given property with the given array
594
     *
595
     * @param object $object
596
     * @param ReflectionClass $class
597
     * @param ReflectionProperty $property
598
     * @param ReflectionNamedType $type
599
     * @param mixed $value
600
     *
601
     * @return void
602
     *
603
     * @throws Exception\InvalidValueException
604
     *         If the given value isn't valid.
605 2
     */
606
    private function hydratePropertyWithArray(
607
        object $object,
608
        ReflectionClass $class,
609
        ReflectionProperty $property,
610
        ReflectionNamedType $type,
611
        $value
612 2
    ) : void {
613 1
        if (!is_array($value)) {
614
            throw new Exception\InvalidValueException($property, sprintf(
615 1
                'The %s.%s property expects an array.',
616 1
                $class->getShortName(),
617
                $property->getName()
618
            ));
619
        }
620 1
621
        $property->setValue($object, $value);
622
    }
623
624
    /**
625
     * Hydrates the given property with the given object
626
     *
627
     * @param object $object
628
     * @param ReflectionClass $class
629
     * @param ReflectionProperty $property
630
     * @param ReflectionNamedType $type
631
     * @param mixed $value
632
     *
633
     * @return void
634
     *
635
     * @throws Exception\InvalidValueException
636
     *         If the given value isn't valid.
637 2
     */
638
    private function hydratePropertyWithObject(
639
        object $object,
640
        ReflectionClass $class,
641
        ReflectionProperty $property,
642
        ReflectionNamedType $type,
643
        $value
644 2
    ) : void {
645 1
        if (!is_object($value)) {
646
            throw new Exception\InvalidValueException($property, sprintf(
647 1
                'The %s.%s property expects an object.',
648 1
                $class->getShortName(),
649
                $property->getName()
650
            ));
651
        }
652 1
653
        $property->setValue($object, $value);
654
    }
655
656
    /**
657
     * Hydrates the given property with the given date-time
658
     *
659
     * @param object $object
660
     * @param ReflectionClass $class
661
     * @param ReflectionProperty $property
662
     * @param ReflectionNamedType $type
663
     * @param mixed $value
664
     *
665
     * @return void
666
     *
667
     * @throws Exception\InvalidValueException
668
     *         If the given value isn't valid.
669 8
     */
670
    private function hydratePropertyWithDateTime(
671
        object $object,
672
        ReflectionClass $class,
673
        ReflectionProperty $property,
674
        ReflectionNamedType $type,
675
        $value
676
    ) : void {
677 8
        /** @var class-string<DateTime|DateTimeImmutable> */
678
        $className = $type->getName();
679 8
680 2
        if (is_int($value)) {
681 2
            $property->setValue($object, (new $className)->setTimestamp($value));
682
            return;
683
        }
684 6
685 2
        if (is_string($value) && ctype_digit($value)) {
686 2
            $property->setValue($object, (new $className)->setTimestamp((int) $value));
687
            return;
688
        }
689 4
690 2
        if (is_string($value) && false !== strtotime($value)) {
691 2
            $property->setValue($object, new $className($value));
692
            return;
693
        }
694 2
695
        throw new Exception\InvalidValueException($property, sprintf(
696 2
            'The %s.%s property expects a valid date-time string or timestamp.',
697 2
            $class->getShortName(),
698
            $property->getName()
699
        ));
700
    }
701
702
    /**
703
     * Hydrates the given property with the given date-interval
704
     *
705
     * @param object $object
706
     * @param ReflectionClass $class
707
     * @param ReflectionProperty $property
708
     * @param ReflectionNamedType $type
709
     * @param mixed $value
710
     *
711
     * @return void
712
     *
713
     * @throws Exception\InvalidValueException
714
     *         If the given value isn't valid.
715 3
     */
716
    private function hydratePropertyWithDateInterval(
717
        object $object,
718
        ReflectionClass $class,
719
        ReflectionProperty $property,
720
        ReflectionNamedType $type,
721
        $value
722 3
    ) : void {
723 1
        if (!is_string($value)) {
724
            throw new Exception\InvalidValueException($property, sprintf(
725 1
                'The %s.%s property expects a string.',
726 1
                $class->getShortName(),
727
                $property->getName()
728
            ));
729
        }
730
731 2
        /** @var class-string<DateInterval> */
732
        $className = $type->getName();
733
734 2
        try {
735 1
            $dateInterval = new $className($value);
736 1
        } catch (\Exception $e) {
737
            throw new Exception\InvalidValueException($property, sprintf(
738 1
                'The %s.%s property expects a valid date-interval string based on ISO 8601.',
739 1
                $class->getShortName(),
740
                $property->getName()
741
            ));
742
        }
743 1
744
        $property->setValue($object, $dateInterval);
745
    }
746
747
    /**
748
     * Hydrates the given property with the given backed-enum
749
     *
750
     * @param object $object
751
     * @param ReflectionClass $class
752
     * @param ReflectionProperty $property
753
     * @param ReflectionNamedType $type
754
     * @param mixed $value
755
     *
756
     * @return void
757
     *
758
     * @throws Exception\InvalidValueException
759
     *         If the given value isn't valid.
760 15
     */
761
    private function hydratePropertyWithBackedEnum(
762
        object $object,
763
        ReflectionClass $class,
764
        ReflectionProperty $property,
765
        ReflectionNamedType $type,
766
        $value
767
    ) : void {
768 15
        /** @var class-string<BackedEnum> */
769 15
        $enumName = $type->getName();
770
        $enumReflection = new ReflectionEnum($enumName);
771
772 15
        /** @var ReflectionNamedType */
773 15
        $enumType = $enumReflection->getBackingType();
774
        $enumTypeName = $enumType->getName();
775
776 15
        // support for HTML forms...
777 4
        if ('int' === $enumTypeName && is_string($value)) {
778
            $value = filter_var($value, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE);
779
        }
780 15
781 15
        if (('int' === $enumTypeName && !is_int($value)) ||
782 2
            ('string' === $enumTypeName && !is_string($value))) {
783
            throw new Exception\InvalidValueException($property, sprintf(
784 2
                'The %s.%s property expects the following type: %s.',
785 2
                $class->getShortName(),
786
                $property->getName(),
787
                $enumTypeName
788
            ));
789
        }
790 13
791 13
        $enum = $enumName::tryFrom($value);
792 2
        if (!isset($enum)) {
793 2
            $allowedCases = [];
794 2
            foreach ($enumName::cases() as $case) {
795
                $allowedCases[] = $case->value;
796
            }
797 2
798
            throw new Exception\InvalidValueException($property, sprintf(
799 2
                'The %s.%s property expects one of the following values: %s.',
800 2
                $class->getShortName(),
801 2
                $property->getName(),
802
                implode(', ', $allowedCases)
803
            ));
804
        }
805 11
806
        $property->setValue($object, $enum);
807
    }
808
809
    /**
810
     * Hydrates the given property with the given enum
811
     *
812
     * @param object $object
813
     * @param ReflectionClass $class
814
     * @param ReflectionProperty $property
815
     * @param ReflectionNamedType $type
816
     * @param mixed $value
817
     *
818
     * @return void
819
     *
820
     * @throws Exception\InvalidValueException
821
     *         If the given value isn't valid.
822 5
     */
823
    private function hydratePropertyWithEnum(
824
        object $object,
825
        ReflectionClass $class,
826
        ReflectionProperty $property,
827
        ReflectionNamedType $type,
828
        $value
829 5
    ) : void {
830 1
        /** @var class-string<Enum> */
831
        $enumName = $type->getName();
832 1
833 1
        $enum = $enumName::tryFrom($value);
834
        if (!isset($enum)) {
835
            $allowedCases = [];
836
            foreach ($enumName::cases() as $case) {
837 4
                $allowedCases[] = $case->value();
838
            }
839
840
            throw new Exception\InvalidValueException($property, sprintf(
841
                'The %s.%s property expects one of the following values: %s.',
842
                $class->getShortName(),
843
                $property->getName(),
844
                implode(', ', $allowedCases)
845
            ));
846
        }
847
848
        $property->setValue($object, $enum);
849
    }
850
851
    /**
852
     * Hydrates the given property with the given one association
853
     *
854 6
     * @param object $object
855
     * @param ReflectionClass $class
856
     * @param ReflectionProperty $property
857
     * @param ReflectionNamedType $type
858
     * @param mixed $value
859
     *
860
     * @return void
861 6
     *
862 1
     * @throws Exception\InvalidValueException
863
     *         If the given value isn't valid.
864 1
     */
865 1
    private function hydratePropertyWithOneAssociation(
866
        object $object,
867
        ReflectionClass $class,
868
        ReflectionProperty $property,
869 5
        ReflectionNamedType $type,
870 5
        $value
871 5
    ) : void {
872 5
        if (!is_array($value) && !is_object($value)) {
873 1
            throw new Exception\InvalidValueException($property, sprintf(
874
                'The %s.%s property expects an associative array or object.',
875 1
                $class->getShortName(),
876 1
                $property->getName()
877
            ));
878
        }
879
880
        $property->setValue($object, $this->hydrate($type->getName(), $value));
881 4
    }
882
883
    /**
884 4
     * Hydrates the given property with the given many associations
885
     *
886
     * @param object $object
887
     * @param ReflectionClass $class
888
     * @param ReflectionProperty $property
889
     * @param ReflectionNamedType $type
890
     * @param mixed $value
891
     *
892
     * @return void
893
     *
894
     * @throws Exception\InvalidValueException
895
     *         If the given value isn't valid.
896
     */
897
    private function hydratePropertyWithManyAssociations(
898
        object $object,
899
        ReflectionClass $class,
900
        ReflectionProperty $property,
901
        ReflectionNamedType $type,
902
        $value
903
    ) : void {
904
        if (!is_array($value) && !is_object($value)) {
905
            throw new Exception\InvalidValueException($property, sprintf(
906
                'The %s.%s property expects an associative array or object.',
907
                $class->getShortName(),
908
                $property->getName()
909
            ));
910
        }
911
912
        $className = $type->getName();
913
        $collection = new $className();
914
        foreach ($value as $key => $child) {
915
            if (!is_array($child) && !is_object($child)) {
916
                throw new Exception\InvalidValueException($property, sprintf(
917
                    'The %s.%s[%s] property expects an associative array or object.',
918
                    $class->getShortName(),
919
                    $property->getName(),
920
                    $key
921
                ));
922
            }
923
924
            $collection->add($key, $this->hydrate($collection->getItemClassName(), $child));
925
        }
926
927
        $property->setValue($object, $collection);
928
    }
929
}
930