Passed
Pull Request — main (#8)
by Anatoly
11:42
created

Hydrator::hydratePropertyWithObject()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 6
dl 0
loc 16
c 1
b 0
f 0
ccs 0
cts 7
cp 0
rs 10
cc 2
nc 2
nop 5
crap 6
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 InvalidArgumentException;
20
use DateTimeInterface;
21
use ReflectionClass;
22
use ReflectionProperty;
23
use ReflectionNamedType;
24
use ReflectionUnionType;
25
26
/**
27
 * Import functions
28
 */
29
use function array_key_exists;
30
use function class_exists;
31
use function ctype_digit;
32
use function filter_var;
33
use function is_array;
34
use function is_bool;
35
use function is_float;
36
use function is_int;
37
use function is_object;
38
use function is_string;
39
use function is_subclass_of;
40
use function json_decode;
41
use function json_last_error;
42
use function json_last_error_msg;
43
use function sprintf;
44
use function strtotime;
45
46
/**
47
 * Import constants
48
 */
49
use const FILTER_NULL_ON_FAILURE;
50
use const FILTER_VALIDATE_BOOLEAN;
51
use const FILTER_VALIDATE_FLOAT;
52
use const FILTER_VALIDATE_INT;
53
use const JSON_ERROR_NONE;
54
use const PHP_MAJOR_VERSION;
55
56
/**
57
 * Hydrator
58
 */
59
class Hydrator implements HydratorInterface
60
{
61
62
    /**
63
     * @var SimpleAnnotationReader|null
64
     */
65
    private $annotationReader = null;
66
67
    /**
68
     * Constructor of the class
69
     */
70 38
    public function __construct()
71
    {
72 38
        if (PHP_MAJOR_VERSION < 8) {
73
            // @codeCoverageIgnoreStart
74
            $this->useAnnotations();
75
            // @codeCoverageIgnoreEnd
76
        }
77 38
    }
78
79
    /**
80
     * Enables support for annotations
81
     *
82
     * @return self
83
     */
84
    public function useAnnotations() : self
85
    {
86
        if (isset($this->annotationReader)) {
87
            return $this;
88
        }
89
90
        if (class_exists(SimpleAnnotationReader::class)) {
91
            $this->annotationReader = /** @scrutinizer ignore-deprecated */ new SimpleAnnotationReader();
92
            $this->annotationReader->addNamespace(Annotation::class);
93
        }
94
95
        return $this;
96
    }
97
98
    /**
99
     * {@inheritdoc}
100
     *
101
     * @throws Exception\UntypedPropertyException
102
     *         If one of the object properties isn't typed.
103
     *
104
     * @throws Exception\UnsupportedPropertyTypeException
105
     *         If one of the object properties contains an unsupported type.
106
     *
107
     * @throws Exception\MissingRequiredValueException
108
     *         If the given data doesn't contain required value.
109
     *
110
     * @throws Exception\InvalidValueException
111
     *         If the given data contains an invalid value.
112
     */
113 37
    public function hydrate($object, array $data) : object
114
    {
115 37
        $object = $this->initializeObject($object);
116
117 37
        $class = new ReflectionClass($object);
118 37
        $properties = $class->getProperties();
119 37
        foreach ($properties as $property) {
120
            // statical properties cannot be hydrated...
121 37
            if ($property->isStatic()) {
122 1
                continue;
123
            }
124
125 37
            $property->setAccessible(true);
126
127 37
            if (!$property->hasType()) {
128 1
                throw new Exception\UntypedPropertyException(sprintf(
129 1
                    'The <%s.%s> property is not typed.',
130 1
                    $class->getShortName(),
131 1
                    $property->getName()
132
                ));
133
            }
134
135 36
            if ($property->getType() instanceof ReflectionUnionType) {
136
                throw new Exception\UnsupportedPropertyTypeException(sprintf(
137
                    'The <%s.%s> property contains an unsupported type <%s>.',
138
                    $class->getShortName(),
139
                    $property->getName(),
140
                    \implode('|', \array_map(function (ReflectionNamedType $type) : string {
141
                        return $type->getName();
142
                    }, $property->getType()->getTypes()))
143
                ));
144
            }
145
146 36
            $key = $property->getName();
147 36
            if (!array_key_exists($key, $data)) {
148 34
                $alias = $this->getPropertyAlias($property);
149 34
                if (isset($alias)) {
150 1
                    $key = $alias->value;
151
                }
152
            }
153
154 36
            if (!array_key_exists($key, $data)) {
155 34
                if (!$property->isInitialized($object)) {
156 1
                    throw new Exception\MissingRequiredValueException(sprintf(
157 1
                        'The <%s.%s> property is required.',
158 1
                        $class->getShortName(),
159 1
                        $property->getName()
160
                    ));
161
                }
162
163 33
                continue;
164
            }
165
166 35
            $this->hydrateProperty($object, $class, $property, $property->getType(), $data[$key]);
167
        }
168
169 1
        return $object;
170
    }
171
172
    /**
173
     * {@inheritdoc}
174
     *
175
     * @throws InvalidArgumentException
176
     *         If the given JSON cannot be decoded.
177
     */
178
    public function hydrateWithJson($object, string $json) : object
179
    {
180
        json_decode(''); // reset previous error...
181
        $data = (array) json_decode($json, true);
182
        if (JSON_ERROR_NONE <> json_last_error()) {
183
            throw new InvalidArgumentException(sprintf(
184
                'Unable to decode JSON: %s',
185
                json_last_error_msg()
186
            ));
187
        }
188
189
        return $this->hydrate($object, $data);
190
    }
191
192
    /**
193
     * Initializes the given object
194
     *
195
     * @param object|class-string $object
0 ignored issues
show
Documentation Bug introduced by
The doc comment object|class-string at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in object|class-string.
Loading history...
196
     *
197
     * @return object
198
     *
199
     * @throws InvalidArgumentException
200
     *         If the given object cannot be initialized.
201
     */
202 37
    private function initializeObject($object) : object
203
    {
204 37
        if (is_object($object)) {
205 37
            return $object;
206
        }
207
208 1
        if (!is_string($object) || !class_exists($object)) {
209
            throw new InvalidArgumentException(sprintf(
210
                'The method %s::hydrate() expects an object or name of an existing class.',
211
                __CLASS__
212
            ));
213
        }
214
215 1
        $class = new ReflectionClass($object);
216 1
        $constructor = $class->getConstructor();
217 1
        if (isset($constructor) && $constructor->getNumberOfRequiredParameters() > 0) {
218
            throw new InvalidArgumentException(sprintf(
219
                'The object %s cannot be hydrated because its constructor has required parameters.',
220
                $class->getName()
221
            ));
222
        }
223
224 1
        return $class->newInstance();
225
    }
226
227
    /**
228
     * Gets an alias for the given property
229
     *
230
     * @param ReflectionProperty $property
231
     *
232
     * @return Alias|null
233
     */
234 34
    private function getPropertyAlias(ReflectionProperty $property) : ?Alias
235
    {
236 34
        if (PHP_MAJOR_VERSION >= 8) {
237 34
            $attributes = $property->getAttributes(Alias::class);
238 34
            if (isset($attributes[0])) {
239 1
                return $attributes[0]->newInstance();
240
            }
241
        }
242
243 34
        if (isset($this->annotationReader)) {
244
            $annotation = $this->annotationReader->getPropertyAnnotation($property, Alias::class);
0 ignored issues
show
Bug introduced by
The method getPropertyAnnotation() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

244
            /** @scrutinizer ignore-call */ 
245
            $annotation = $this->annotationReader->getPropertyAnnotation($property, Alias::class);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
245
            if (isset($annotation)) {
246
                return $annotation;
247
            }
248
        }
249
250 34
        return null;
251
    }
252
253
    /**
254
     * Hydrates the given property with the given value
255
     *
256
     * @param object $object
257
     * @param ReflectionClass $class
258
     * @param ReflectionProperty $property
259
     * @param ReflectionNamedType $type
260
     * @param mixed $value
261
     *
262
     * @return void
263
     *
264
     * @throws Exception\InvalidValueException
265
     *         If the given value isn't valid.
266
     *
267
     * @throws Exception\UnsupportedPropertyTypeException
268
     *         If the given property contains an unsupported type.
269
     */
270 35
    private function hydrateProperty(
271
        object $object,
272
        ReflectionClass $class,
273
        ReflectionProperty $property,
274
        ReflectionNamedType $type,
275
        $value
276
    ) : void {
277 35
        if (null === $value) {
278 2
            $this->hydratePropertyWithNull($object, $class, $property, $type);
279 1
            return;
280
        }
281
282 34
        if ('bool' === $type->getName()) {
283 1
            $this->hydratePropertyWithBooleanValue($object, $class, $property, $type, $value);
284 1
            return;
285
        }
286
287 34
        if ('int' === $type->getName()) {
288 1
            $this->hydratePropertyWithIntegerNumber($object, $class, $property, $type, $value);
289 1
            return;
290
        }
291
292 34
        if ('float' === $type->getName()) {
293 1
            $this->hydratePropertyWithNumber($object, $class, $property, $type, $value);
294 1
            return;
295
        }
296
297 34
        if ('string' === $type->getName()) {
298 5
            $this->hydratePropertyWithString($object, $class, $property, $type, $value);
299 1
            return;
300
        }
301
302 30
        if ('array' === $type->getName()) {
303 8
            $this->hydratePropertyWithArray($object, $class, $property, $type, $value);
304 1
            return;
305
        }
306
307 23
        if ('object' === $type->getName()) {
308
            $this->hydratePropertyWithObject($object, $class, $property, $type, $value);
309
            return;
310
        }
311
312 23
        if (is_subclass_of($type->getName(), DateTimeInterface::class)) {
313 8
            $this->hydratePropertyWithTimestamp($object, $class, $property, $type, $value);
314 1
            return;
315
        }
316
317 16
        if (is_subclass_of($type->getName(), ObjectCollectionInterface::class)) {
318 8
            $this->hydratePropertyWithManyAssociations($object, $class, $property, $type, $value);
319 1
            return;
320
        }
321
322 9
        if (class_exists($type->getName())) {
323 8
            $this->hydratePropertyWithOneAssociation($object, $class, $property, $type, $value);
324 1
            return;
325
        }
326
327 1
        throw new Exception\UnsupportedPropertyTypeException(sprintf(
328 1
            'The <%s.%s> property contains an unsupported type <%s>.',
329 1
            $class->getShortName(),
330 1
            $property->getName(),
331 1
            $type->getName()
332
        ));
333
    }
334
335
    /**
336
     * Hydrates the given property with null
337
     *
338
     * @param object $object
339
     * @param ReflectionClass $class
340
     * @param ReflectionProperty $property
341
     * @param ReflectionNamedType $type
342
     *
343
     * @return void
344
     *
345
     * @throws Exception\InvalidValueException
346
     *         If the given value isn't valid.
347
     */
348 2
    private function hydratePropertyWithNull(
349
        object $object,
350
        ReflectionClass $class,
351
        ReflectionProperty $property,
352
        ReflectionNamedType $type
353
    ) : void {
354 2
        if (!$type->allowsNull()) {
355 1
            throw new Exception\InvalidValueException(sprintf(
356 1
                'The <%s.%s> property cannot accept null.',
357 1
                $class->getShortName(),
358 1
                $property->getName()
359
            ));
360
        }
361
362 1
        $property->setValue($object, null);
363 1
    }
364
365
    /**
366
     * Hydrates the given property with the given boolean value
367
     *
368
     * @param object $object
369
     * @param ReflectionClass $class
370
     * @param ReflectionProperty $property
371
     * @param ReflectionNamedType $type
372
     * @param mixed $value
373
     *
374
     * @return void
375
     *
376
     * @throws Exception\InvalidValueException
377
     *         If the given value isn't valid.
378
     */
379 1
    private function hydratePropertyWithBooleanValue(
380
        object $object,
381
        ReflectionClass $class,
382
        ReflectionProperty $property,
383
        ReflectionNamedType $type,
0 ignored issues
show
Unused Code introduced by
The parameter $type is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

383
        /** @scrutinizer ignore-unused */ ReflectionNamedType $type,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
384
        $value
385
    ) : void {
386 1
        if (!is_bool($value)) {
387
            // if the value isn't boolean, then we will use filter_var, because it will give us the ability to specify
388
            // boolean values as strings. this behavior is great for html forms. details at:
389
            // https://github.com/php/php-src/blob/b7d90f09d4a1688f2692f2fa9067d0a07f78cc7d/ext/filter/logical_filters.c#L273
390
            $value = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
391
392
            if (!isset($value)) {
393
                throw new Exception\InvalidValueException(sprintf(
394
                    'The <%s.%s> property accepts a boolean value only.',
395
                    $class->getShortName(),
396
                    $property->getName()
397
                ));
398
            }
399
        }
400
401 1
        $property->setValue($object, $value);
402 1
    }
403
404
    /**
405
     * Hydrates the given property with the given integer number
406
     *
407
     * @param object $object
408
     * @param ReflectionClass $class
409
     * @param ReflectionProperty $property
410
     * @param ReflectionNamedType $type
411
     * @param mixed $value
412
     *
413
     * @return void
414
     *
415
     * @throws Exception\InvalidValueException
416
     *         If the given value isn't valid.
417
     */
418 1
    private function hydratePropertyWithIntegerNumber(
419
        object $object,
420
        ReflectionClass $class,
421
        ReflectionProperty $property,
422
        ReflectionNamedType $type,
0 ignored issues
show
Unused Code introduced by
The parameter $type is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

422
        /** @scrutinizer ignore-unused */ ReflectionNamedType $type,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
423
        $value
424
    ) : void {
425 1
        if (!is_int($value)) {
426
            // it's senseless to convert the value type if it's not a number, so we will use filter_var to correct
427
            // converting the value type to int. also remember that string numbers must be between PHP_INT_MIN and
428
            // PHP_INT_MAX, otherwise the result will be null. this behavior is great for html forms. details at:
429
            // https://github.com/php/php-src/blob/b7d90f09d4a1688f2692f2fa9067d0a07f78cc7d/ext/filter/logical_filters.c#L197
430
            // https://github.com/php/php-src/blob/b7d90f09d4a1688f2692f2fa9067d0a07f78cc7d/ext/filter/logical_filters.c#L94
431
            $value = filter_var($value, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE);
432
433
            if (!isset($value)) {
434
                throw new Exception\InvalidValueException(sprintf(
435
                    'The <%s.%s> property accepts an integer number only.',
436
                    $class->getShortName(),
437
                    $property->getName()
438
                ));
439
            }
440
        }
441
442 1
        $property->setValue($object, $value);
443 1
    }
444
445
    /**
446
     * Hydrates the given property with the given number
447
     *
448
     * @param object $object
449
     * @param ReflectionClass $class
450
     * @param ReflectionProperty $property
451
     * @param ReflectionNamedType $type
452
     * @param mixed $value
453
     *
454
     * @return void
455
     *
456
     * @throws Exception\InvalidValueException
457
     *         If the given value isn't valid.
458
     */
459 1
    private function hydratePropertyWithNumber(
460
        object $object,
461
        ReflectionClass $class,
462
        ReflectionProperty $property,
463
        ReflectionNamedType $type,
0 ignored issues
show
Unused Code introduced by
The parameter $type is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

463
        /** @scrutinizer ignore-unused */ ReflectionNamedType $type,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
464
        $value
465
    ) : void {
466 1
        if (!is_float($value)) {
467
            // it's senseless to convert the value type if it's not a number, so we will use filter_var to correct
468
            // converting the value type to float. this behavior is great for html forms. details at:
469
            // https://github.com/php/php-src/blob/b7d90f09d4a1688f2692f2fa9067d0a07f78cc7d/ext/filter/logical_filters.c#L342
470
            $value = filter_var($value, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE);
471
472
            if (!isset($value)) {
473
                throw new Exception\InvalidValueException(sprintf(
474
                    'The <%s.%s> property accepts a number only.',
475
                    $class->getShortName(),
476
                    $property->getName()
477
                ));
478
            }
479
        }
480
481 1
        $property->setValue($object, $value);
482 1
    }
483
484
    /**
485
     * Hydrates the given property with the given string
486
     *
487
     * @param object $object
488
     * @param ReflectionClass $class
489
     * @param ReflectionProperty $property
490
     * @param ReflectionNamedType $type
491
     * @param mixed $value
492
     *
493
     * @return void
494
     *
495
     * @throws Exception\InvalidValueException
496
     *         If the given value isn't valid.
497
     */
498 5
    private function hydratePropertyWithString(
499
        object $object,
500
        ReflectionClass $class,
501
        ReflectionProperty $property,
502
        ReflectionNamedType $type,
0 ignored issues
show
Unused Code introduced by
The parameter $type is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

502
        /** @scrutinizer ignore-unused */ ReflectionNamedType $type,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
503
        $value
504
    ) : void {
505 5
        if (!is_string($value)) {
506 4
            throw new Exception\InvalidValueException(sprintf(
507 4
                'The <%s.%s> property accepts a string only.',
508 4
                $class->getShortName(),
509 4
                $property->getName()
510
            ));
511
        }
512
513 1
        $property->setValue($object, $value);
514 1
    }
515
516
    /**
517
     * Hydrates the given property with the given array
518
     *
519
     * @param object $object
520
     * @param ReflectionClass $class
521
     * @param ReflectionProperty $property
522
     * @param ReflectionNamedType $type
523
     * @param mixed $value
524
     *
525
     * @return void
526
     *
527
     * @throws Exception\InvalidValueException
528
     *         If the given value isn't valid.
529
     */
530 8
    private function hydratePropertyWithArray(
531
        object $object,
532
        ReflectionClass $class,
533
        ReflectionProperty $property,
534
        ReflectionNamedType $type,
0 ignored issues
show
Unused Code introduced by
The parameter $type is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

534
        /** @scrutinizer ignore-unused */ ReflectionNamedType $type,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
535
        $value
536
    ) : void {
537 8
        if (!is_array($value)) {
538 7
            throw new Exception\InvalidValueException(sprintf(
539 7
                'The <%s.%s> property accepts an array only.',
540 7
                $class->getShortName(),
541 7
                $property->getName()
542
            ));
543
        }
544
545 1
        $property->setValue($object, $value);
546 1
    }
547
548
    /**
549
     * Hydrates the given property with the given object
550
     *
551
     * @param object $object
552
     * @param ReflectionClass $class
553
     * @param ReflectionProperty $property
554
     * @param ReflectionNamedType $type
555
     * @param mixed $value
556
     *
557
     * @return void
558
     *
559
     * @throws Exception\InvalidValueException
560
     *         If the given value isn't valid.
561
     */
562
    private function hydratePropertyWithObject(
563
        object $object,
564
        ReflectionClass $class,
565
        ReflectionProperty $property,
566
        ReflectionNamedType $type,
0 ignored issues
show
Unused Code introduced by
The parameter $type is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

566
        /** @scrutinizer ignore-unused */ ReflectionNamedType $type,

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
567
        $value
568
    ) : void {
569
        if (!is_object($value)) {
570
            throw new Exception\InvalidValueException(sprintf(
571
                'The <%s.%s> property accepts an object only.',
572
                $class->getShortName(),
573
                $property->getName()
574
            ));
575
        }
576
577
        $property->setValue($object, $value);
578
    }
579
580
    /**
581
     * Hydrates the given property with the given timestamp
582
     *
583
     * @param object $object
584
     * @param ReflectionClass $class
585
     * @param ReflectionProperty $property
586
     * @param ReflectionNamedType $type
587
     * @param mixed $value
588
     *
589
     * @return void
590
     *
591
     * @throws Exception\InvalidValueException
592
     *         If the given value isn't valid.
593
     */
594 8
    private function hydratePropertyWithTimestamp(
595
        object $object,
596
        ReflectionClass $class,
597
        ReflectionProperty $property,
598
        ReflectionNamedType $type,
599
        $value
600
    ) : void {
601 8
        $target = $type->getName();
602
603 8
        if (is_int($value) || ctype_digit($value)) {
604
            $property->setValue($object, (new $target)->setTimestamp($value));
605
            return;
606
        }
607
608 8
        if (is_string($value) && false !== strtotime($value)) {
609 1
            $property->setValue($object, new $target($value));
610 1
            return;
611
        }
612
613 7
        throw new Exception\InvalidValueException(sprintf(
614 7
            'The <%s.%s> property accepts a valid date-time string or a timestamp only.',
615 7
            $class->getShortName(),
616 7
            $property->getName()
617
        ));
618
    }
619
620
    /**
621
     * Hydrates the given property with the given many associations
622
     *
623
     * @param object $object
624
     * @param ReflectionClass $class
625
     * @param ReflectionProperty $property
626
     * @param ReflectionNamedType $type
627
     * @param mixed $value
628
     *
629
     * @return void
630
     *
631
     * @throws Exception\InvalidValueException
632
     *         If the given value isn't valid.
633
     */
634 8
    private function hydratePropertyWithManyAssociations(
635
        object $object,
636
        ReflectionClass $class,
637
        ReflectionProperty $property,
638
        ReflectionNamedType $type,
639
        $value
640
    ) : void {
641 8
        if (!is_array($value)) {
642 7
            throw new Exception\InvalidValueException(sprintf(
643 7
                'The <%s.%s> property accepts an array only.',
644 7
                $class->getShortName(),
645 7
                $property->getName()
646
            ));
647
        }
648
649 1
        $target = $type->getName();
650 1
        $collection = new $target();
651 1
        foreach ($value as $key => $item) {
652 1
            if (!is_array($item)) {
653
                throw new Exception\InvalidValueException(sprintf(
654
                    'The <%s.%s> property accepts an array with arrays only.',
655
                    $class->getShortName(),
656
                    $property->getName()
657
                ));
658
            }
659
660 1
            $collection->add($key, $this->hydrate($collection->getItemClassName(), $item));
661
        }
662
663 1
        $property->setValue($object, $collection);
664 1
    }
665
666
    /**
667
     * Hydrates the given property with the given one association
668
     *
669
     * @param object $object
670
     * @param ReflectionClass $class
671
     * @param ReflectionProperty $property
672
     * @param ReflectionNamedType $type
673
     * @param mixed $value
674
     *
675
     * @return void
676
     *
677
     * @throws Exception\InvalidValueException
678
     *         If the given value isn't valid.
679
     */
680 8
    private function hydratePropertyWithOneAssociation(
681
        object $object,
682
        ReflectionClass $class,
683
        ReflectionProperty $property,
684
        ReflectionNamedType $type,
685
        $value
686
    ) : void {
687 8
        if (!is_array($value)) {
688 7
            throw new Exception\InvalidValueException(sprintf(
689 7
                'The <%s.%s> property accepts an array only.',
690 7
                $class->getShortName(),
691 7
                $property->getName()
692
            ));
693
        }
694
695 1
        $property->setValue($object, $this->hydrate($type->getName(), $value));
696 1
    }
697
}
698