Passed
Pull Request — main (#24)
by Anatoly
04:51 queued 15s
created

Hydrator::hydrate()   B

Complexity

Conditions 11
Paths 30

Size

Total Lines 52
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 32
CRAP Score 11

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 32
c 2
b 0
f 0
dl 0
loc 52
ccs 32
cts 32
cp 1
rs 7.3166
cc 11
nc 30
nop 3
crap 11

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * It's free open-source software released under the MIT License.
5
 *
6
 * @author Anatoly Nekhay <[email protected]>
7
 * @copyright Copyright (c) 2021, Anatoly Nekhay
8
 * @license https://github.com/sunrise-php/hydrator/blob/master/LICENSE
9
 * @link https://github.com/sunrise-php/hydrator
10
 */
11
12
declare(strict_types=1);
13
14
namespace Sunrise\Hydrator;
15
16
use BackedEnum;
0 ignored issues
show
Bug introduced by
The type BackedEnum was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
17
use Doctrine\Common\Annotations\AnnotationReader;
18
use Doctrine\Common\Annotations\Reader as AnnotationReaderInterface;
19
use JsonException;
20
use Sunrise\Hydrator\Annotation\Alias;
21
use Sunrise\Hydrator\Annotation\Format;
22
use Sunrise\Hydrator\Annotation\Ignore;
23
use Sunrise\Hydrator\Annotation\Relationship;
24
use Sunrise\Hydrator\Exception\InvalidDataException;
25
use Sunrise\Hydrator\Exception\InvalidValueException;
26
use DateTimeImmutable;
27
use LogicException;
28
use ReflectionAttribute;
29
use ReflectionClass;
30
use ReflectionEnum;
0 ignored issues
show
Bug introduced by
The type ReflectionEnum was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
31
use ReflectionNamedType;
32
use ReflectionProperty;
33
use ValueError;
34
35
use function array_key_exists;
36
use function class_exists;
37
use function extension_loaded;
38
use function filter_var;
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 sprintf;
48
use function trim;
49
50
use const FILTER_NULL_ON_FAILURE;
51
use const FILTER_VALIDATE_BOOLEAN;
52
use const FILTER_VALIDATE_FLOAT;
53
use const FILTER_VALIDATE_INT;
54
use const JSON_THROW_ON_ERROR;
55
use const PHP_MAJOR_VERSION;
56
57
/**
58
 * Hydrator
59
 */
60
class Hydrator implements HydratorInterface
61
{
62
63
    /**
64
     * @var AnnotationReaderInterface|null
65
     */
66
    private ?AnnotationReaderInterface $annotationReader = null;
67
68
    /**
69
     * Gets the annotation reader
70
     *
71
     * @return AnnotationReaderInterface|null
72
     */
73 1
    public function getAnnotationReader(): ?AnnotationReaderInterface
74
    {
75 1
        return $this->annotationReader;
76
    }
77
78
    /**
79
     * Sets the given annotation reader
80
     *
81
     * @param AnnotationReaderInterface|null $annotationReader
82
     *
83
     * @return self
84
     */
85 1
    public function setAnnotationReader(?AnnotationReaderInterface $annotationReader): self
86
    {
87 1
        $this->annotationReader = $annotationReader;
88
89 1
        return $this;
90
    }
91
92
    /**
93
     * Uses the default annotation reader
94
     *
95
     * @return self
96
     *
97
     * @throws LogicException
98
     *         If the doctrine/annotations package isn't installed.
99
     */
100 1
    public function useDefaultAnnotationReader(): self
101
    {
102
        // @codeCoverageIgnoreStart
103
        if (!class_exists(AnnotationReader::class)) {
104
            throw new LogicException('The package doctrine/annotations is required.');
105
        } // @codeCoverageIgnoreEnd
106
107 1
        $this->annotationReader = new AnnotationReader();
108
109 1
        return $this;
110
    }
111
112
    /**
113
     * Hydrates the given object with the given data
114
     *
115
     * @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...
116
     * @param array<array-key, mixed> $data
117
     * @param list<array-key> $path
118
     *
119
     * @return T
120
     *
121
     * @throws Exception\InvalidDataException
122
     *         If the given data is invalid.
123
     *
124
     * @throws Exception\UninitializableObjectException
125
     *         If the object cannot be initialized.
126
     *
127
     * @throws Exception\UntypedPropertyException
128
     *         If one of the object properties isn't typed.
129
     *
130
     * @throws Exception\UnsupportedPropertyTypeException
131
     *         If one of the object properties contains an unsupported type.
132
     *
133
     * @template T of object
134
     */
135 454
    public function hydrate($object, array $data, array $path = []): object
136
    {
137 454
        $object = $this->instantObject($object);
138 453
        $class = new ReflectionClass($object);
139 453
        $properties = $class->getProperties();
140 453
        $defaultValues = $this->getClassConstructorDefaultValues($class);
141 453
        $violations = [];
142 453
        foreach ($properties as $property) {
143 453
            if ($property->isStatic()) {
144 1
                continue;
145
            }
146
147 452
            $ignore = $this->getPropertyAnnotation($property, Ignore::class);
148 452
            if (isset($ignore)) {
149 1
                continue;
150
            }
151
152 451
            $key = $property->getName();
153 451
            $alias = $this->getPropertyAnnotation($property, Alias::class);
154 451
            if (isset($alias)) {
155 2
                $key = $alias->value;
156
            }
157
158 451
            if (array_key_exists($key, $data) === false) {
159 30
                if ($property->isInitialized($object)) {
160 12
                    continue;
161
                }
162
163 18
                if (array_key_exists($property->getName(), $defaultValues)) {
164 1
                    $property->setValue($object, $defaultValues[$property->getName()]);
165 1
                    continue;
166
                }
167
168 17
                $violations[] = InvalidValueException::shouldBeProvided([...$path, $key]);
169
170 17
                continue;
171
            }
172
173
            try {
174 428
                $this->hydrateProperty($object, $property, $data[$key], [...$path, $key]);
175 106
            } catch (InvalidDataException $e) {
176 17
                $violations = [...$violations, ...$e->getExceptions()];
177 102
            } catch (InvalidValueException $e) {
178 96
                $violations[] = $e;
179
            }
180
        }
181
182 447
        if (!empty($violations)) {
183 114
            throw new InvalidDataException('Invalid data.', $violations);
184
        }
185
186 336
        return $object;
187
    }
188
189
    /**
190
     * Hydrates the given object with the given JSON
191
     *
192
     * @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...
193
     * @param string $json
194
     * @param int<0, max> $flags
195
     * @param int<1, 2147483647> $depth
196
     *
197
     * @return T
198
     *
199
     * @throws Exception\InvalidDataException
200
     *         If the given data is invalid.
201
     *
202
     * @throws Exception\UninitializableObjectException
203
     *         If the object cannot be initialized.
204
     *
205
     * @throws Exception\UntypedPropertyException
206
     *         If one of the object properties isn't typed.
207
     *
208
     * @throws Exception\UnsupportedPropertyTypeException
209
     *         If one of the object properties contains an unsupported type.
210
     *
211
     * @template T of object
212
     */
213 3
    public function hydrateWithJson($object, string $json, int $flags = 0, int $depth = 512): object
214
    {
215
        // @codeCoverageIgnoreStart
216
        if (!extension_loaded('json')) {
217
            throw new LogicException('JSON extension is required.');
218
        } // @codeCoverageIgnoreEnd
219
220
        try {
221 3
            $data = json_decode($json, true, $depth, $flags | JSON_THROW_ON_ERROR);
222 1
        } catch (JsonException $e) {
223 1
            throw new InvalidDataException(sprintf('Invalid JSON: %s', $e->getMessage()));
224
        }
225
226 2
        if (!is_array($data)) {
227 1
            throw new InvalidDataException('JSON must be an object.');
228
        }
229
230 1
        return $this->hydrate($object, $data);
231
    }
232
233
    /**
234
     * Instantiates the given object
235
     *
236
     * @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...
237
     *
238
     * @return T
239
     *
240
     * @throws Exception\UninitializableObjectException
241
     *         If the given object cannot be instantiated.
242
     *
243
     * @template T of object
244
     */
245 454
    private function instantObject($object): object
246
    {
247 454
        if (is_object($object)) {
248 73
            return $object;
249
        }
250
251 453
        $class = new ReflectionClass($object);
252 453
        if (!$class->isInstantiable()) {
253 1
            throw new Exception\UninitializableObjectException(sprintf(
254 1
                'The class %s cannot be hydrated because it is an uninstantiable class.',
255 1
                $class->getName(),
256 1
            ));
257
        }
258
259
        /** @var T */
260 452
        return $class->newInstanceWithoutConstructor();
261
    }
262
263
    /**
264
     * Hydrates the given property with the given value
265
     *
266
     * @param object $object
267
     * @param ReflectionProperty $property
268
     * @param mixed $value
269
     * @param list<array-key> $path
0 ignored issues
show
Bug introduced by
The type Sunrise\Hydrator\list was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
270
     *
271
     * @return void
272
     *
273
     * @throws InvalidValueException
274
     *         If the given value is invalid.
275
     *
276
     * @throws InvalidDataException
277
     *         If the given value is invalid.
278
     *
279
     * @throws Exception\UntypedPropertyException
280
     *         If the given property isn't typed.
281
     *
282
     * @throws Exception\UnsupportedPropertyTypeException
283
     *         If the given property contains an unsupported type.
284
     */
285 428
    private function hydrateProperty(
286
        object $object,
287
        ReflectionProperty $property,
288
        $value,
289
        array $path
290
    ): void {
291 428
        $property->setAccessible(true);
292
293 428
        $type = $this->getPropertyType($property);
294 426
        $typeName = $type->getName();
295
296 426
        if ($value === null) {
297 26
            $this->hydratePropertyWithNull($object, $property, $type, $path);
298 12
            return;
299
        }
300 404
        if ($typeName === 'bool') {
301 40
            $this->hydrateBooleanProperty($object, $property, $type, $value, $path);
302 32
            return;
303
        }
304 364
        if ($typeName === 'int') {
305 27
            $this->hydrateIntegerProperty($object, $property, $type, $value, $path);
306 20
            return;
307
        }
308 337
        if ($typeName === 'float') {
309 107
            $this->hydrateNumericProperty($object, $property, $type, $value, $path);
310 101
            return;
311
        }
312 230
        if ($typeName === 'string') {
313 114
            $this->hydrateStringProperty($object, $property, $value, $path);
314 98
            return;
315
        }
316 182
        if ($typeName === 'array') {
317 26
            $this->hydrateArrayProperty($object, $property, $value, $path);
318 5
            return;
319
        }
320 166
        if ($typeName === DateTimeImmutable::class) {
321 42
            $this->hydrateTimestampProperty($object, $property, $type, $value, $path);
322 26
            return;
323
        }
324 125
        if (is_subclass_of($typeName, BackedEnum::class)) {
325 48
            $this->hydrateEnumerableProperty($object, $property, $type, $typeName, $value, $path);
326 32
            return;
327
        }
328 78
        if (class_exists($typeName)) {
329 77
            $this->hydrateRelationshipProperty($object, $property, $typeName, $value, $path);
330 56
            return;
331
        }
332
333 1
        throw new Exception\UnsupportedPropertyTypeException(sprintf(
334 1
            'The property %s.%s contains an unsupported type %s.',
335 1
            $property->getDeclaringClass()->getName(),
336 1
            $property->getName(),
337 1
            $typeName,
338 1
        ));
339
    }
340
341
    /**
342
     * Hydrates the given property with null
343
     *
344
     * @param object $object
345
     * @param ReflectionProperty $property
346
     * @param ReflectionNamedType $type
347
     * @param list<array-key> $path
348
     *
349
     * @return void
350
     *
351
     * @throws InvalidValueException
352
     *         If the given value isn't valid.
353
     */
354 54
    private function hydratePropertyWithNull(
355
        object $object,
356
        ReflectionProperty $property,
357
        ReflectionNamedType $type,
358
        array $path
359
    ): void {
360 54
        if (!$type->allowsNull()) {
361 28
            throw InvalidValueException::shouldNotBeEmpty($path);
362
        }
363
364 26
        $property->setValue($object, null);
365
    }
366
367
    /**
368
     * Hydrates the given boolean property with the given value
369
     *
370
     * @param object $object
371
     * @param ReflectionProperty $property
372
     * @param ReflectionNamedType $type
373
     * @param mixed $value
374
     * @param list<array-key> $path
375
     *
376
     * @return void
377
     *
378
     * @throws InvalidValueException
379
     *         If the given value isn't valid.
380
     */
381 40
    private function hydrateBooleanProperty(
382
        object $object,
383
        ReflectionProperty $property,
384
        ReflectionNamedType $type,
385
        $value,
386
        array $path
387
    ): void {
388 40
        if (is_string($value)) {
389
            // As part of the support for HTML forms and other untyped data sources,
390
            // an empty string should not be cast to a boolean type, therefore,
391
            // such values should be treated as NULL.
392 29
            if (trim($value) === '') {
393 4
                $this->hydratePropertyWithNull($object, $property, $type, $path);
394 2
                return;
395
            }
396
397
            // https://github.com/php/php-src/blob/b7d90f09d4a1688f2692f2fa9067d0a07f78cc7d/ext/filter/logical_filters.c#L273
398 25
            $value = filter_var($value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
399
        }
400
401 36
        if (!is_bool($value)) {
402 6
            throw InvalidValueException::shouldBeBoolean($path);
403
        }
404
405 30
        $property->setValue($object, $value);
406
    }
407
408
    /**
409
     * Hydrates the given integer property with the given value
410
     *
411
     * @param object $object
412
     * @param ReflectionProperty $property
413
     * @param ReflectionNamedType $type
414
     * @param mixed $value
415
     * @param list<array-key> $path
416
     *
417
     * @return void
418
     *
419
     * @throws InvalidValueException
420
     *         If the given value isn't valid.
421
     */
422 27
    private function hydrateIntegerProperty(
423
        object $object,
424
        ReflectionProperty $property,
425
        ReflectionNamedType $type,
426
        $value,
427
        array $path
428
    ): void {
429 27
        if (is_string($value)) {
430
            // As part of the support for HTML forms and other untyped data sources,
431
            // an empty string cannot be cast to an integer type, therefore,
432
            // such values should be treated as NULL.
433 14
            if (trim($value) === '') {
434 4
                $this->hydratePropertyWithNull($object, $property, $type, $path);
435 2
                return;
436
            }
437
438
            // https://github.com/php/php-src/blob/b7d90f09d4a1688f2692f2fa9067d0a07f78cc7d/ext/filter/logical_filters.c#L94
439
            // https://github.com/php/php-src/blob/b7d90f09d4a1688f2692f2fa9067d0a07f78cc7d/ext/filter/logical_filters.c#L197
440 10
            $value = filter_var($value, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE);
441
        }
442
443 23
        if (!is_int($value)) {
444 5
            throw InvalidValueException::shouldBeInteger($path);
445
        }
446
447 18
        $property->setValue($object, $value);
448
    }
449
450
    /**
451
     * Hydrates the given numeric property with the given value
452
     *
453
     * @param object $object
454
     * @param ReflectionProperty $property
455
     * @param ReflectionNamedType $type
456
     * @param mixed $value
457
     * @param list<array-key> $path
458
     *
459
     * @return void
460
     *
461
     * @throws InvalidValueException
462
     *         If the given value isn't valid.
463
     */
464 107
    private function hydrateNumericProperty(
465
        object $object,
466
        ReflectionProperty $property,
467
        ReflectionNamedType $type,
468
        $value,
469
        array $path
470
    ): void {
471 107
        if (is_string($value)) {
472
            // As part of the support for HTML forms and other untyped data sources,
473
            // an empty string cannot be cast to a number type, therefore,
474
            // such values should be treated as NULL.
475 77
            if (trim($value) === '') {
476 4
                $this->hydratePropertyWithNull($object, $property, $type, $path);
477 2
                return;
478
            }
479
480
            // https://github.com/php/php-src/blob/b7d90f09d4a1688f2692f2fa9067d0a07f78cc7d/ext/filter/logical_filters.c#L342
481 73
            $value = filter_var($value, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE);
482
        }
483
484 103
        if (is_int($value)) {
485 9
            $value = (float) $value;
486
        }
487
488 103
        if (!is_float($value)) {
489 4
            throw InvalidValueException::shouldBeNumber($path);
490
        }
491
492 99
        $property->setValue($object, $value);
493
    }
494
495
    /**
496
     * Hydrates the given string property with the given value
497
     *
498
     * @param object $object
499
     * @param ReflectionProperty $property
500
     * @param mixed $value
501
     * @param list<array-key> $path
502
     *
503
     * @return void
504
     *
505
     * @throws InvalidValueException
506
     *         If the given value isn't valid.
507
     */
508 114
    private function hydrateStringProperty(
509
        object $object,
510
        ReflectionProperty $property,
511
        $value,
512
        array $path
513
    ): void {
514 114
        if (!is_string($value)) {
515 16
            throw InvalidValueException::shouldBeString($path);
516
        }
517
518 98
        $property->setValue($object, $value);
519
    }
520
521
    /**
522
     * Hydrates the given array property with the given value
523
     *
524
     * @param object $object
525
     * @param ReflectionProperty $property
526
     * @param mixed $value
527
     * @param list<array-key> $path
528
     *
529
     * @return void
530
     *
531
     * @throws InvalidValueException
532
     *         If the given value isn't valid.
533
     */
534 26
    private function hydrateArrayProperty(
535
        object $object,
536
        ReflectionProperty $property,
537
        $value,
538
        array $path
539
    ): void {
540 26
        $relationship = $this->getPropertyAnnotation($property, Relationship::class);
541 26
        if (isset($relationship)) {
542 18
            $this->hydrateRelationshipsProperty($object, $property, $relationship, $value, $path);
543 2
            return;
544
        }
545
546 8
        if (!is_array($value)) {
547 5
            throw InvalidValueException::shouldBeArray($path);
548
        }
549
550 3
        $property->setValue($object, $value);
551
    }
552
553
    /**
554
     * Hydrates the given timestamp property with the given value
555
     *
556
     * @param object $object
557
     * @param ReflectionProperty $property
558
     * @param ReflectionNamedType $type
559
     * @param mixed $value
560
     * @param list<array-key> $path
561
     *
562
     * @return void
563
     *
564
     * @throws InvalidValueException
565
     *         If the given value isn't valid.
566
     *
567
     * @throws Exception\UnsupportedPropertyTypeException
568
     *         If the given property doesn't contain the Format attribute.
569
     */
570 42
    private function hydrateTimestampProperty(
571
        object $object,
572
        ReflectionProperty $property,
573
        ReflectionNamedType $type,
574
        $value,
575
        array $path
576
    ): void {
577 42
        $format = $this->getPropertyAnnotation($property, Format::class);
578 42
        if (!isset($format)) {
579 1
            throw new Exception\UnsupportedPropertyTypeException(sprintf(
580 1
                'The property %1$s.%2$s must contain the attribute %3$s, ' .
581 1
                'for example: #[\%3$s(\DateTimeInterface::DATE_RFC3339)].',
582 1
                $property->getDeclaringClass()->getName(),
583 1
                $property->getName(),
584 1
                Format::class,
585 1
            ));
586
        }
587
588 41
        if (is_string($value)) {
589
            // As part of the support for HTML forms and other untyped data sources,
590
            // an instance of DateTimeImmutable should not be created from an empty string, therefore,
591
            // such values should be treated as NULL.
592 23
            if (trim($value) === '') {
593 8
                $this->hydratePropertyWithNull($object, $property, $type, $path);
594 4
                return;
595
            }
596
597 15
            if ($format->value === 'U') {
598
                // https://github.com/php/php-src/blob/b7d90f09d4a1688f2692f2fa9067d0a07f78cc7d/ext/filter/logical_filters.c#L94
599
                // https://github.com/php/php-src/blob/b7d90f09d4a1688f2692f2fa9067d0a07f78cc7d/ext/filter/logical_filters.c#L197
600 10
                $value = filter_var($value, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE);
601
            }
602
        }
603
604 33
        if ($format->value === 'U' && !is_int($value)) {
605 5
            throw InvalidValueException::shouldBeInteger($path);
606
        }
607 28
        if ($format->value !== 'U' && !is_string($value)) {
608 5
            throw InvalidValueException::shouldBeString($path);
609
        }
610
611
        /** @var int|string $value */
612
613 23
        $timestamp = DateTimeImmutable::createFromFormat($format->value, (string) $value);
614 23
        if ($timestamp === false) {
615 1
            throw InvalidValueException::invalidTimestamp($path, $format->value);
616
        }
617
618 22
        $property->setValue($object, $timestamp);
619
    }
620
621
    /**
622
     * Hydrates the given enumerable property with the given value
623
     *
624
     * @param object $object
625
     * @param ReflectionProperty $property
626
     * @param ReflectionNamedType $type
627
     * @param class-string<BackedEnum> $enumName
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<BackedEnum> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<BackedEnum>.
Loading history...
628
     * @param mixed $value
629
     * @param list<array-key> $path
630
     *
631
     * @return void
632
     *
633
     * @throws InvalidValueException
634
     *         If the given value isn't valid.
635
     */
636 48
    private function hydrateEnumerableProperty(
637
        object $object,
638
        ReflectionProperty $property,
639
        ReflectionNamedType $type,
640
        string $enumName,
641
        $value,
642
        array $path
643
    ): void {
644 48
        $enumType = (string) (new ReflectionEnum($enumName))->getBackingType();
645
646 48
        if (is_string($value)) {
647
            // As part of the support for HTML forms and other untyped data sources,
648
            // an instance of BackedEnum should not be created from an empty string, therefore,
649
            // such values should be treated as NULL.
650 28
            if (trim($value) === '') {
651 8
                $this->hydratePropertyWithNull($object, $property, $type, $path);
652 4
                return;
653
            }
654
655 20
            if ($enumType === 'int') {
656
                // https://github.com/php/php-src/blob/b7d90f09d4a1688f2692f2fa9067d0a07f78cc7d/ext/filter/logical_filters.c#L94
657
                // https://github.com/php/php-src/blob/b7d90f09d4a1688f2692f2fa9067d0a07f78cc7d/ext/filter/logical_filters.c#L197
658 10
                $value = filter_var($value, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE);
659
            }
660
        }
661
662 40
        if ($enumType === 'int' && !is_int($value)) {
663 5
            throw InvalidValueException::shouldBeInteger($path);
664
        }
665 35
        if ($enumType === 'string' && !is_string($value)) {
666 5
            throw InvalidValueException::shouldBeString($path);
667
        }
668
669
        /** @var int|string $value */
670
671
        try {
672 30
            $property->setValue($object, $enumName::from($value));
673 2
        } catch (ValueError $e) {
674 2
            throw InvalidValueException::invalidChoice($path, $enumName);
675
        }
676
    }
677
678
    /**
679
     * Hydrates the given relationship property with the given value
680
     *
681
     * @param object $object
682
     * @param ReflectionProperty $property
683
     * @param class-string $className
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
684
     * @param mixed $value
685
     * @param list<array-key> $path
686
     *
687
     * @return void
688
     *
689
     * @throws InvalidValueException
690
     *         If the given value isn't valid.
691
     *
692
     * @throws Exception\UnsupportedPropertyTypeException
693
     *         If the given property refers to a non-instantiable class.
694
     */
695 77
    private function hydrateRelationshipProperty(
696
        object $object,
697
        ReflectionProperty $property,
698
        string $className,
699
        $value,
700
        array $path
701
    ): void {
702 77
        $classReflection = new ReflectionClass($className);
703 77
        if (!$classReflection->isInstantiable()) {
704 1
            throw new Exception\UnsupportedPropertyTypeException(sprintf(
705 1
                'The property %s.%s refers to a non-instantiable class %s.',
706 1
                $property->getDeclaringClass()->getName(),
707 1
                $property->getName(),
708 1
                $classReflection->getName(),
709 1
            ));
710
        }
711
712 76
        if (!is_array($value)) {
713 5
            throw InvalidValueException::shouldBeArray($path);
714
        }
715
716 71
        $classInstance = $classReflection->newInstanceWithoutConstructor();
717
718 71
        $property->setValue($object, $this->hydrate($classInstance, $value, $path));
719
    }
720
721
    /**
722
     * Hydrates the given relationships property with the given value
723
     *
724
     * @param object $object
725
     * @param ReflectionProperty $property
726
     * @param Relationship $relationship
727
     * @param mixed $value
728
     * @param list<array-key> $path
729
     *
730
     * @return void
731
     *
732
     * @throws InvalidDataException
733
     *         If the given value isn't valid.
734
     *
735
     * @throws Exception\UnsupportedPropertyTypeException
736
     *         If the given property refers to a non-instantiable class.
737
     */
738 18
    private function hydrateRelationshipsProperty(
739
        object $object,
740
        ReflectionProperty $property,
741
        Relationship $relationship,
742
        $value,
743
        array $path
744
    ): void {
745 18
        $classReflection = new ReflectionClass($relationship->target);
746 18
        if (!$classReflection->isInstantiable()) {
747 1
            throw new Exception\UnsupportedPropertyTypeException(sprintf(
748 1
                'The property %s.%s refers to a non-instantiable class %s.',
749 1
                $property->getDeclaringClass()->getName(),
750 1
                $property->getName(),
751 1
                $classReflection->getName(),
752 1
            ));
753
        }
754
755 17
        if (!is_array($value)) {
756 5
            throw InvalidValueException::shouldBeArray($path);
757
        }
758
759 12
        $counter = 0;
760 12
        $violations = [];
761 12
        $classInstances = [];
762 12
        $classPrototype = $classReflection->newInstanceWithoutConstructor();
763 12
        foreach ($value as $key => $data) {
764 12
            if (isset($relationship->limit) && ++$counter > $relationship->limit) {
765 1
                $violations[] = InvalidValueException::redundantElement([...$path, $key], $relationship->limit);
0 ignored issues
show
Bug introduced by
It seems like $relationship->limit can also be of type null; however, parameter $limit of Sunrise\Hydrator\Excepti...ion::redundantElement() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

765
                $violations[] = InvalidValueException::redundantElement([...$path, $key], /** @scrutinizer ignore-type */ $relationship->limit);
Loading history...
766 1
                break;
767
            }
768
769 12
            if (!is_array($data)) {
770 1
                $violations[] = InvalidValueException::shouldBeArray([...$path, $key]);
771 1
                continue;
772
            }
773
774
            try {
775 11
                $classInstances[$key] = $this->hydrate(clone $classPrototype, $data, [...$path, $key]);
776 8
            } catch (InvalidDataException $e) {
777 8
                $violations = [...$violations, ...$e->getExceptions()];
778
            }
779
        }
780
781 12
        if (!empty($violations)) {
782 10
            throw new InvalidDataException('Invalid data.', $violations);
783
        }
784
785 2
        $property->setValue($object, $classInstances);
786
    }
787
788
    /**
789
     * Gets a type from the given property
790
     *
791
     * @param ReflectionProperty $property
792
     *
793
     * @return ReflectionNamedType
794
     *
795
     * @throws Exception\UntypedPropertyException
796
     *         If the given property isn't typed.
797
     *
798
     * @throws Exception\UnsupportedPropertyTypeException
799
     *         If the given property contains an unsupported type.
800
     */
801 428
    private function getPropertyType(ReflectionProperty $property): ReflectionNamedType
802
    {
803 428
        $type = $property->getType();
804
805 428
        if (!isset($type)) {
806 1
            throw new Exception\UntypedPropertyException(sprintf(
807 1
                'The property %s.%s is not typed.',
808 1
                $property->getDeclaringClass()->getName(),
809 1
                $property->getName(),
810 1
            ));
811
        }
812
813 427
        if (!($type instanceof ReflectionNamedType)) {
814 1
            throw new Exception\UnsupportedPropertyTypeException(sprintf(
815 1
                'The property %s.%s contains an unsupported type %s.',
816 1
                $property->getDeclaringClass()->getName(),
817 1
                $property->getName(),
818 1
                (string) $type,
819 1
            ));
820
        }
821
822 426
        return $type;
823
    }
824
825
    /**
826
     * Gets an annotation from the given property
827
     *
828
     * @param ReflectionProperty $property
829
     * @param class-string<T> $annotationName
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string<T> at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string<T>.
Loading history...
830
     *
831
     * @return T|null
832
     *
833
     * @template T of object
834
     */
835 452
    private function getPropertyAnnotation(ReflectionProperty $property, string $annotationName): ?object
836
    {
837 452
        if (PHP_MAJOR_VERSION >= 8) {
838
            /**
839
             * @psalm-var list<ReflectionAttribute> $annotations
840
             * @phpstan-var list<ReflectionAttribute<T>> $annotations
841
             * @psalm-suppress TooManyTemplateParams
842
             */
843 452
            $annotations = $property->getAttributes($annotationName);
844
845 452
            if (isset($annotations[0])) {
846
                /** @var T */
847 60
                return $annotations[0]->newInstance();
848
            }
849
        }
850
851 451
        if (isset($this->annotationReader)) {
852 1
            $annotations = $this->annotationReader->getPropertyAnnotations($property);
0 ignored issues
show
Bug introduced by
The method getPropertyAnnotations() 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

852
            /** @scrutinizer ignore-call */ 
853
            $annotations = $this->annotationReader->getPropertyAnnotations($property);

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...
853 1
            foreach ($annotations as $annotation) {
854 1
                if ($annotation instanceof $annotationName) {
855 1
                    return $annotation;
856
                }
857
            }
858
        }
859
860 451
        return null;
861
    }
862
863
    /**
864
     * Gets default values from the given class's constructor
865
     *
866
     * @param ReflectionClass<T> $class
867
     *
868
     * @return array<non-empty-string, mixed>
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<non-empty-string, mixed> at position 2 could not be parsed: Unknown type name 'non-empty-string' at position 2 in array<non-empty-string, mixed>.
Loading history...
869
     *
870
     * @template T of object
871
     */
872 453
    private function getClassConstructorDefaultValues(ReflectionClass $class): array
873
    {
874 453
        $result = [];
875 453
        $constructor = $class->getConstructor();
876 453
        if (isset($constructor)) {
877 1
            foreach ($constructor->getParameters() as $parameter) {
878 1
                if ($parameter->isDefaultValueAvailable()) {
879
                    /** @psalm-suppress MixedAssignment */
880 1
                    $result[$parameter->getName()] = $parameter->getDefaultValue();
881
                }
882
            }
883
        }
884
885 453
        return $result;
886
    }
887
}
888