Hydrator::getConstructorDefaultValues()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4

Importance

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