Test Failed
Pull Request — main (#28)
by Anatoly
04:37
created

Hydrator::hydrate()   B

Complexity

Conditions 11
Paths 34

Size

Total Lines 54
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 11

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 29
c 2
b 0
f 0
dl 0
loc 54
rs 7.3166
ccs 20
cts 20
cp 1
cc 11
nc 34
nop 4
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 Generator;
17
use JsonException;
18
use LogicException;
19
use ReflectionClass;
20
use ReflectionNamedType;
21
use ReflectionProperty;
22
use Sunrise\Hydrator\Annotation\Alias;
23
use Sunrise\Hydrator\Annotation\Context;
24
use Sunrise\Hydrator\Annotation\Ignore;
25
use Sunrise\Hydrator\AnnotationReader\BuiltinAnnotationReader;
26
use Sunrise\Hydrator\AnnotationReader\DoctrineAnnotationReader;
27
use Sunrise\Hydrator\AnnotationReader\NullAnnotationReader;
28
use Sunrise\Hydrator\Dictionary\BuiltinType;
29
use Sunrise\Hydrator\Exception\InvalidDataException;
30
use Sunrise\Hydrator\Exception\InvalidObjectException;
31
use Sunrise\Hydrator\Exception\InvalidValueException;
32
use Sunrise\Hydrator\TypeConverter\ArrayAccessTypeConverter;
33
use Sunrise\Hydrator\TypeConverter\ArrayTypeConverter;
34
use Sunrise\Hydrator\TypeConverter\BackedEnumTypeConverter;
35
use Sunrise\Hydrator\TypeConverter\BooleanTypeConverter;
36
use Sunrise\Hydrator\TypeConverter\IntegerTypeConverter;
37
use Sunrise\Hydrator\TypeConverter\MixedTypeConverter;
38
use Sunrise\Hydrator\TypeConverter\MyclabsEnumTypeConverter;
39
use Sunrise\Hydrator\TypeConverter\NumberTypeConverter;
40
use Sunrise\Hydrator\TypeConverter\ObjectTypeConverter;
41
use Sunrise\Hydrator\TypeConverter\RamseyUuidTypeConverter;
42
use Sunrise\Hydrator\TypeConverter\StringTypeConverter;
43
use Sunrise\Hydrator\TypeConverter\SymfonyUidTypeConverter;
44
use Sunrise\Hydrator\TypeConverter\TimestampTypeConverter;
45
use Sunrise\Hydrator\TypeConverter\TimezoneTypeConverter;
46
use TypeError;
47
48
use function array_key_exists;
49
use function class_exists;
50
use function extension_loaded;
51
use function gettype;
52
use function is_array;
53
use function is_object;
54
use function is_string;
55
use function json_decode;
56
use function sprintf;
57
use function usort;
58
59
use const JSON_BIGINT_AS_STRING;
60
use const JSON_THROW_ON_ERROR;
61
use const PHP_MAJOR_VERSION;
62
use const PHP_VERSION_ID;
63
64
/**
65
 * Hydrator
66
 */
67
class Hydrator implements HydratorInterface
68
{
69 506
70
    /**
71 506
     * @var array<string, mixed>
72
     */
73 506
    private array $context;
74 506
75 506
    /**
76 506
     * @var AnnotationReaderInterface
77 506
     */
78 506
    private AnnotationReaderInterface $annotationReader;
79 506
80 506
    /**
81 506
     * @var list<TypeConverterInterface>
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...
82 506
     */
83 506
    private array $typeConverters = [];
84 506
85
    /**
86
     * Constructor of the class
87
     *
88
     * @param array<string, mixed> $context
89
     */
90 506
    public function __construct(array $context = [])
91
    {
92 506
        $this->context = $context;
93 506
94 506
        $this->annotationReader = PHP_MAJOR_VERSION >= 8 ?
95
            new BuiltinAnnotationReader() :
96 506
            new NullAnnotationReader();
97 506
98
        $this->addTypeConverter(...self::defaultTypeConverters());
99
    }
100 506
101
    /**
102
     * Sets the given annotation reader to the hydrator
103
     *
104 506
     * @param AnnotationReaderInterface|\Doctrine\Common\Annotations\Reader $annotationReader
105
     *
106
     * @return self
107
     *
108
     * @since 3.0.0
109
     */
110 441
    public function setAnnotationReader($annotationReader): self
111
    {
112 441
        if ($annotationReader instanceof AnnotationReaderInterface) {
113 441
            $this->annotationReader = $annotationReader;
114 441
            return $this;
115 325
        }
116
117
        // BC with previous versions...
118
        if ($annotationReader instanceof \Doctrine\Common\Annotations\Reader) {
0 ignored issues
show
introduced by
$annotationReader is always a sub-type of Doctrine\Common\Annotations\Reader.
Loading history...
119 1
            $this->annotationReader = new DoctrineAnnotationReader($annotationReader);
120
            return $this;
121
        }
122
123
        throw new TypeError(sprintf(
124
            'Argument #1 ($annotationReader) must be of type %s or %s, %s given',
125
            AnnotationReaderInterface::class,
126
            \Doctrine\Common\Annotations\Reader::class,
127
            gettype($annotationReader),
128
        ));
129
    }
130
131
    /**
132
     * Sets the doctrine's default annotation reader to the hydrator
133
     *
134
     * @return self
135
     *
136
     * @since 3.0.0
137
     *
138
     * @deprecated 3.2.0 Use the {@see setAnnotationReader()} method
139
     *                   with the {@see DoctrineAnnotationReader::default()} attribute.
140
     */
141
    public function useDefaultAnnotationReader(): self
142
    {
143
        return $this->setAnnotationReader(DoctrineAnnotationReader::default());
144
    }
145
146
    /**
147
     * Adds the given type converter(s) to the hydrator
148
     *
149
     * @param TypeConverterInterface ...$typeConverters
150
     *
151
     * @return self
152
     *
153
     * @since 3.1.0
154 1
     */
155
    public function addTypeConverter(TypeConverterInterface ...$typeConverters): self
156 1
    {
157
        foreach ($typeConverters as $typeConverter) {
158 1
            if ($typeConverter instanceof AnnotationReaderAwareInterface) {
159
                $typeConverter->setAnnotationReader($this->annotationReader);
160
            }
161
            if ($typeConverter instanceof HydratorAwareInterface) {
162
                $typeConverter->setHydrator($this);
163
            }
164
165
            $this->typeConverters[] = $typeConverter;
166
        }
167
168
        // phpcs:ignore Generic.Files.LineLength
169
        usort($this->typeConverters, static fn(TypeConverterInterface $a, TypeConverterInterface $b): int => $b->getWeight() <=> $a->getWeight());
170
171
        return $this;
172
    }
173
174
    /**
175
     * @inheritDoc
176
     */
177
    public function castValue($value, Type $type, array $path = [], array $context = [])
178
    {
179
        if ($value === null) {
180
            if ($type->allowsNull()) {
181
                return null;
182
            }
183
184 504
            throw InvalidValueException::mustNotBeEmpty($path);
185
        }
186 504
187 503
        // phpcs:ignore Generic.Files.LineLength
188 503
        $context = ($this->annotationReader->getAnnotations(Context::class, $type->getHolder())->current()->value ?? []) + $context + $this->context;
189 503
190 503
        foreach ($this->typeConverters as $typeConverter) {
191 503
            $result = $typeConverter->castValue($value, $type, $path, $context);
192
            if ($result->valid()) {
193
                return $result->current();
194
            }
195
        }
196 503
197 1
        throw InvalidObjectException::unsupportedType($type);
198
    }
199
200 502
    /**
201 1
     * @inheritDoc
202
     */
203
    public function hydrate($object, array $data, array $path = [], array $context = []): object
204 501
    {
205 501
        [$object, $class] = $this->instantObject($object);
206 501
        $properties = $class->getProperties();
207 2
        $constructorDefaultValues = $this->getClassConstructorDefaultValues($class);
208
209
        $violations = [];
210 501
        foreach ($properties as $property) {
211 37
            // @codeCoverageIgnoreStart
212 15
            if (PHP_VERSION_ID < 80100) {
213
                /** @psalm-suppress UnusedMethodCall */
214
                $property->setAccessible(true);
215 22
            } // @codeCoverageIgnoreEnd
216 1
217 1
            if ($property->isStatic()) {
218
                continue;
219
            }
220 21
221 21
            if ($this->annotationReader->getAnnotations(Ignore::class, $property)->valid()) {
222
                continue;
223
            }
224
225 471
            // phpcs:ignore Generic.Files.LineLength
226 133
            $key = $this->annotationReader->getAnnotations(Alias::class, $property)->current()->value ?? $property->getName();
227 17
228 129
            if (array_key_exists($key, $data) === false) {
229 123
                if ($property->isInitialized($object)) {
230
                    continue;
231
                }
232
233 497
                if (array_key_exists($property->getName(), $constructorDefaultValues)) {
234 145
                    $property->setValue($object, $constructorDefaultValues[$property->getName()]);
235
                    continue;
236
                }
237 355
238
                $violations[] = InvalidValueException::mustBeProvided([...$path, $key]);
239
                continue;
240
            }
241
242
            try {
243
                // phpcs:ignore Generic.Files.LineLength
244
                $property->setValue($object, $this->castValue($data[$key], $this->getPropertyType($property), [...$path, $key], $context));
245
            } catch (InvalidValueException $e) {
246
                $violations[] = $e;
247
            } catch (InvalidDataException $e) {
248
                $violations = [...$violations, ...$e->getExceptions()];
249
            }
250
        }
251
252
        if (!empty($violations)) {
253
            throw new InvalidDataException('Invalid data', $violations);
254
        }
255
256
        return $object;
257
    }
258
259
    /**
260
     * @inheritDoc
261
     */
262
    // phpcs:ignore Generic.Files.LineLength
263
    public function hydrateWithJson($object, string $json, int $flags = 0, int $depth = 512, array $path = [], array $context = []): object
264
    {
265 3
        // @codeCoverageIgnoreStart
266
        if (!extension_loaded('json')) {
267
            throw new LogicException('The JSON extension is required.');
268
        } // @codeCoverageIgnoreEnd
269
270
        try {
271
            $data = json_decode($json, true, $depth, $flags | JSON_BIGINT_AS_STRING | JSON_THROW_ON_ERROR);
272
        } catch (JsonException $e) {
273
            // phpcs:ignore Generic.Files.LineLength
274 3
            throw new InvalidDataException(sprintf('The JSON is invalid and couldn‘t be decoded due to: %s', $e->getMessage()));
275 1
        }
276
277 1
        if (!is_array($data)) {
278
            throw new InvalidDataException('The JSON must be in the form of an array or an object.');
279
        }
280 2
281 1
        return $this->hydrate($object, $data, $path, $context);
282
    }
283
284 1
    /**
285
     * Instantiates the given object
286
     *
287
     * @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...
288
     *
289
     * @return array{0: T, 1: ReflectionClass}
290
     *
291
     * @throws InvalidObjectException If the object couldn't be instantiated.
292
     *
293
     * @template T of object
294
     */
295
    private function instantObject($object): array
296
    {
297
        if (is_object($object)) {
298
            return [$object, new ReflectionClass($object)];
299 504
        }
300
301 504
        /** @psalm-suppress DocblockTypeContradiction */
302
        if (!is_string($object)) {
303 504
            // phpcs:ignore Generic.Files.LineLength
304 73
            throw new TypeError(sprintf('Argument #1 ($object) must be of type object or string, %s given', gettype($object)));
305
        }
306
307 503
        if (!class_exists($object)) {
308 1
            throw InvalidObjectException::uninstantiableObject($object);
309 1
        }
310 1
311 1
        $class = new ReflectionClass($object);
312
        if (!$class->isInstantiable()) {
313
            throw InvalidObjectException::uninstantiableObject($class->getName());
314 502
        }
315
316
        return [$class->newInstanceWithoutConstructor(), $class];
317
    }
318
319
    /**
320
     * Gets the given property's type
321
     *
322
     * @param ReflectionProperty $property
323
     *
324
     * @return Type
325
     */
326
    private function getPropertyType(ReflectionProperty $property): Type
327
    {
328
        $type = $property->getType();
329
        if ($type === null) {
330
            return new Type($property, BuiltinType::MIXED, true);
331
        }
332
333
        if ($type instanceof ReflectionNamedType) {
334
            return new Type($property, $type->getName(), $type->allowsNull());
335 471
        }
336
337 471
        return new Type($property, (string) $type, $type->allowsNull());
338
    }
339 469
340 32
    /**
341 17
     * Gets default values from the given class's constructor
342
     *
343
     * @param ReflectionClass<T> $class
344 15
     *
345 15
     * @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...
346
     *
347
     * @template T of object
348 441
     */
349
    private function getClassConstructorDefaultValues(ReflectionClass $class): array
350
    {
351
        $constructor = $class->getConstructor();
352
        if ($constructor === null) {
353
            return [];
354
        }
355
356
        $result = [];
357
        foreach ($constructor->getParameters() as $parameter) {
358
            if ($parameter->isDefaultValueAvailable()) {
359
                /** @psalm-suppress MixedAssignment */
360
                $result[$parameter->getName()] = $parameter->getDefaultValue();
361
            }
362 471
        }
363
364 471
        return $result;
365
    }
366 471
367 1
    /**
368 1
     * Gets the default type converters for this environment
369 1
     *
370 1
     * @return Generator<int, TypeConverterInterface>
371 1
     */
372
    private static function defaultTypeConverters(): Generator
373
    {
374 470
        yield new MixedTypeConverter();
375 1
        yield new BooleanTypeConverter();
376
        yield new IntegerTypeConverter();
377
        yield new NumberTypeConverter();
378 469
        yield new StringTypeConverter();
379
        yield new TimestampTypeConverter();
380
        yield new TimezoneTypeConverter();
381
        yield new ArrayTypeConverter();
382
        yield new ArrayAccessTypeConverter();
383
        yield new ObjectTypeConverter();
384
385
        if (PHP_MAJOR_VERSION >= 8) {
386
            yield new BackedEnumTypeConverter();
387
        }
388
        if (class_exists(\MyCLabs\Enum\Enum::class)) {
389
            yield new MyclabsEnumTypeConverter();
390 503
        }
391
        if (class_exists(\Ramsey\Uuid\UuidInterface::class)) {
392 503
            yield new RamseyUuidTypeConverter();
393 503
        }
394 502
        if (class_exists(\Symfony\Component\Uid\AbstractUid::class)) {
395
            yield new SymfonyUidTypeConverter();
396
        }
397 1
    }
398
}
399