Passed
Pull Request — main (#1)
by Anatoly
02:35
created

Hydrator::hydratePropertyWithArray()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 22
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 10
c 1
b 0
f 0
nc 3
nop 5
dl 0
loc 22
ccs 11
cts 11
cp 1
crap 3
rs 9.9332
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 ArrayAccess;
19
use DateTimeInterface;
20
use ReflectionClass;
21
use ReflectionProperty;
22
use ReflectionNamedType;
23
24
/**
25
 * Import functions
26
 */
27
use function array_key_exists;
28
use function in_array;
29
use function is_array;
30
use function is_scalar;
31
use function is_string;
32
use function is_subclass_of;
33
use function sprintf;
34
use function strtotime;
35
36
/**
37
 * Hydrator
38
 */
39
class Hydrator implements HydratorInterface
40
{
41
42
    /**
43
     * @var SimpleAnnotationReader
44
     */
45
    private $annotationReader;
46
47
    /**
48
     * Constructor of the class
49
     */
50 40
    public function __construct()
51
    {
52 40
        $this->annotationReader = new SimpleAnnotationReader();
0 ignored issues
show
Deprecated Code introduced by
The class Doctrine\Common\Annotations\SimpleAnnotationReader has been deprecated: Deprecated in favour of using AnnotationReader ( Ignorable by Annotation )

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

52
        $this->annotationReader = /** @scrutinizer ignore-deprecated */ new SimpleAnnotationReader();
Loading history...
53 40
        $this->annotationReader->addNamespace(Annotation::class);
54 40
    }
55
56
    /**
57
     * {@inheritDoc}
58
     *
59
     * @throws Exception\MissingRequiredValueException
60
     *         If the given data does not contain required value.
61
     *         * data error.
62
     *
63
     * @throws Exception\InvalidValueException
64
     *         If the given data contains an invalid value.
65
     *         * data error.
66
     *
67
     * @throws Exception\UntypedObjectPropertyException
68
     *         If one of the properties of the given object is not typed.
69
     *         * DTO error.
70
     *
71
     * @throws Exception\UnsupportedObjectPropertyTypeException
72
     *         If one of the properties of the given object contains an unsupported type.
73
     *         * DTO error.
74
     */
75 39
    public function hydrate(HydrableObjectInterface $object, array $data) : void
76
    {
77 39
        $class = new ReflectionClass($object);
78 39
        $properties = $class->getProperties();
79 39
        foreach ($properties as $property) {
80 39
            if ($property->isStatic()) {
81 1
                continue;
82
            }
83
84 39
            $key = $property->getName();
85 39
            $alias = /** @scrutinizer ignore-deprecated */  $this->annotationReader
86 39
                ->getPropertyAnnotation($property, Annotation\Alias::class);
87 39
            if ($alias instanceof Annotation\Alias) {
88 1
                $key = $alias->value;
89
            }
90
91 39
            $property->setAccessible(true);
92
93 39
            if (!array_key_exists($key, $data)) {
94 36
                if (!$property->isInitialized($object)) {
95 1
                    throw new Exception\MissingRequiredValueException(sprintf(
96 1
                        'The <%s.%s> property is required.',
97 1
                        $class->getShortName(),
98 1
                        $property->getName(),
99
                    ));
100
                }
101
102 35
                continue;
103
            }
104
105 38
            if (!$property->hasType()) {
106 1
                throw new Exception\UntypedObjectPropertyException(sprintf(
107 1
                    'The <%s.%s> property is not typed.',
108 1
                    $class->getShortName(),
109 1
                    $property->getName(),
110
                ));
111
            }
112
113 37
            $this->hydrateProperty($object, $class, $property, $property->getType(), $data[$key]);
114
        }
115 1
    }
116
117
    /**
118
     * Hydrates the given property with the given value
119
     *
120
     * @param HydrableObjectInterface $object
121
     * @param ReflectionClass $class
122
     * @param ReflectionProperty $property
123
     * @param ReflectionNamedType $type
124
     * @param mixed $value
125
     *
126
     * @return void
127
     *
128
     * @throws Exception\UnsupportedObjectPropertyTypeException
129
     *         If the given property contains an unsupported type.
130
     *
131
     * @throws Exception\InvalidValueException
132
     *         If the given value isn't valid.
133
     */
134 37
    private function hydrateProperty(
135
        HydrableObjectInterface $object,
136
        ReflectionClass $class,
137
        ReflectionProperty $property,
138
        ReflectionNamedType $type,
139
        $value
140
    ) : void {
141 37
        if (null === $value) {
142 2
            $this->hydratePropertyWithNull($object, $class, $property, $type);
143 1
            return;
144
        }
145
146 36
        if (in_array($type->getName(), ['bool', 'int', 'float', 'string'])) {
147 5
            $this->hydratePropertyWithScalar($object, $class, $property, $type, $value);
148 1
            return;
149
        }
150
151 32
        if (is_subclass_of($type->getName(), ArrayAccess::class)) {
152 8
            $this->hydratePropertyWithArray($object, $class, $property, $type, $value);
153 1
            return;
154
        }
155
156 25
        if (is_subclass_of($type->getName(), DateTimeInterface::class)) {
157 10
            $this->hydratePropertyWithDateTime($object, $class, $property, $type, $value);
158 1
            return;
159
        }
160
161 16
        if (is_subclass_of($type->getName(), HydrableObjectInterface::class)) {
162 8
            $this->hydratePropertyWithOneToOneAssociation($object, $class, $property, $type, $value);
163 1
            return;
164
        }
165
166 9
        if (is_subclass_of($type->getName(), HydrableObjectCollectionInterface::class)) {
167 8
            $this->hydratePropertyWithOneToManyAssociation($object, $class, $property, $type, $value);
168 1
            return;
169
        }
170
171 1
        throw new Exception\UnsupportedObjectPropertyTypeException(sprintf(
172 1
            'The <%s.%s> property contains the <%s> unhydrable type.',
173 1
            $class->getShortName(),
174 1
            $property->getName(),
175 1
            $type->getName(),
176
        ));
177
    }
178
179
    /**
180
     * Hydrates the given property with null
181
     *
182
     * @param HydrableObjectInterface $object
183
     * @param ReflectionClass $class
184
     * @param ReflectionProperty $property
185
     * @param ReflectionNamedType $type
186
     *
187
     * @return void
188
     *
189
     * @throws Exception\InvalidValueException
190
     *         If the given value isn't valid.
191
     */
192 2
    private function hydratePropertyWithNull(
193
        HydrableObjectInterface $object,
194
        ReflectionClass $class,
195
        ReflectionProperty $property,
196
        ReflectionNamedType $type
197
    ) : void {
198 2
        if (!$type->allowsNull()) {
199 1
            throw new Exception\InvalidValueException(sprintf(
200 1
                'The <%s.%s> property does not support null.',
201 1
                $class->getShortName(),
202 1
                $property->getName(),
203
            ));
204
        }
205
206 1
        $property->setValue($object, null);
207 1
    }
208
209
    /**
210
     * Hydrates the given property with the given scalar value
211
     *
212
     * @param HydrableObjectInterface $object
213
     * @param ReflectionClass $class
214
     * @param ReflectionProperty $property
215
     * @param ReflectionNamedType $type
216
     * @param mixed $value
217
     *
218
     * @return void
219
     *
220
     * @throws Exception\InvalidValueException
221
     *         If the given value isn't valid.
222
     */
223 5
    private function hydratePropertyWithScalar(
224
        HydrableObjectInterface $object,
225
        ReflectionClass $class,
226
        ReflectionProperty $property,
227
        ReflectionNamedType $type,
228
        $value
229
    ) : void {
230 5
        if (!is_scalar($value)) {
231 4
            throw new Exception\InvalidValueException(sprintf(
232 4
                'The <%s.%s> property only accepts a scalar value.',
233 4
                $class->getShortName(),
234 4
                $property->getName(),
235
            ));
236
        }
237
238 1
        switch ($type->getName()) {
239 1
            case 'bool':
240 1
                $property->setValue($object, (bool) $value);
241 1
                break;
242 1
            case 'int':
243 1
                $property->setValue($object, (int) $value);
244 1
                break;
245 1
            case 'float':
246 1
                $property->setValue($object, (float) $value);
247 1
                break;
248 1
            case 'string':
249 1
                $property->setValue($object, (string) $value);
250 1
                break;
251
        }
252 1
    }
253
254
    /**
255
     * Hydrates the given property with the given array value
256
     *
257
     * @param HydrableObjectInterface $object
258
     * @param ReflectionClass $class
259
     * @param ReflectionProperty $property
260
     * @param ReflectionNamedType $type
261
     * @param mixed $value
262
     *
263
     * @return void
264
     *
265
     * @throws Exception\InvalidValueException
266
     *         If the given value isn't valid.
267
     */
268 8
    private function hydratePropertyWithArray(
269
        HydrableObjectInterface $object,
270
        ReflectionClass $class,
271
        ReflectionProperty $property,
272
        ReflectionNamedType $type,
273
        $value
274
    ) : void {
275 8
        if (!is_array($value)) {
276 7
            throw new Exception\InvalidValueException(sprintf(
277 7
                'The <%s.%s> property only accepts an array.',
278 7
                $class->getShortName(),
279 7
                $property->getName(),
280
            ));
281
        }
282
283 1
        $arrayClassName = $type->getName();
284 1
        $array = new $arrayClassName();
285 1
        foreach ($value as $offset => $element) {
286 1
            $array->offsetSet($offset, $element);
287
        }
288
289 1
        $property->setValue($object, $array);
290 1
    }
291
292
    /**
293
     * Hydrates the given property with the given date-time value
294
     *
295
     * @param HydrableObjectInterface $object
296
     * @param ReflectionClass $class
297
     * @param ReflectionProperty $property
298
     * @param ReflectionNamedType $type
299
     * @param mixed $value
300
     *
301
     * @return void
302
     *
303
     * @throws Exception\InvalidValueException
304
     *         If the given value isn't valid.
305
     */
306 10
    private function hydratePropertyWithDateTime(
307
        HydrableObjectInterface $object,
308
        ReflectionClass $class,
309
        ReflectionProperty $property,
310
        ReflectionNamedType $type,
311
        $value
312
    ) : void {
313 10
        if (!is_string($value) || false === strtotime($value)) {
314 9
            throw new Exception\InvalidValueException(sprintf(
315 9
                'The <%s.%s> property only accepts a valid date-time string.',
316 9
                $class->getShortName(),
317 9
                $property->getName(),
318
            ));
319
        }
320
321 1
        $dateTimeClassName = $type->getName();
322 1
        $dateTime = new $dateTimeClassName($value);
323 1
        $property->setValue($object, $dateTime);
324 1
    }
325
326
    /**
327
     * Hydrates the given property with the given one-to-one value
328
     *
329
     * @param HydrableObjectInterface $object
330
     * @param ReflectionClass $class
331
     * @param ReflectionProperty $property
332
     * @param ReflectionNamedType $type
333
     * @param mixed $value
334
     *
335
     * @return void
336
     *
337
     * @throws Exception\InvalidValueException
338
     *         If the given value isn't valid.
339
     */
340 8
    private function hydratePropertyWithOneToOneAssociation(
341
        HydrableObjectInterface $object,
342
        ReflectionClass $class,
343
        ReflectionProperty $property,
344
        ReflectionNamedType $type,
345
        $value
346
    ) : void {
347 8
        if (!is_array($value)) {
348 7
            throw new Exception\InvalidValueException(sprintf(
349 7
                'The <%s.%s> property only accepts an array.',
350 7
                $class->getShortName(),
351 7
                $property->getName(),
352
            ));
353
        }
354
355 1
        $childObjectClassName = $type->getName();
356 1
        $childObject = new $childObjectClassName();
357 1
        $this->hydrate($childObject, $value);
358 1
        $property->setValue($object, $childObject);
359 1
    }
360
361
    /**
362
     * Hydrates the given property with the given one-to-many value
363
     *
364
     * @param HydrableObjectInterface $object
365
     * @param ReflectionClass $class
366
     * @param ReflectionProperty $property
367
     * @param ReflectionNamedType $type
368
     * @param mixed $value
369
     *
370
     * @return void
371
     *
372
     * @throws Exception\InvalidValueException
373
     *         If the given value isn't valid.
374
     */
375 8
    private function hydratePropertyWithOneToManyAssociation(
376
        HydrableObjectInterface $object,
377
        ReflectionClass $class,
378
        ReflectionProperty $property,
379
        ReflectionNamedType $type,
380
        $value
381
    ) : void {
382 8
        if (!is_array($value)) {
383 7
            throw new Exception\InvalidValueException(sprintf(
384 7
                'The <%s.%s> property only accepts an array.',
385 7
                $class->getShortName(),
386 7
                $property->getName(),
387
            ));
388
        }
389
390 1
        $objectCollectionClassName = $type->getName();
391 1
        $objectCollection = new $objectCollectionClassName();
392 1
        $collectionObjectClassName = $objectCollectionClassName::T;
393 1
        foreach ($value as $item) {
394 1
            $collectionObject = new $collectionObjectClassName();
395 1
            $this->hydrate($collectionObject, (array) $item);
396 1
            $objectCollection->add($collectionObject);
397
        }
398
399 1
        $property->setValue($object, $objectCollection);
400 1
    }
401
}
402