Test Failed
Branch release/v1.0.0 (d4539f)
by Anatoly
11:43 queued 01:17
created

Hydrator::hydrateProperty()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 42
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 23
c 1
b 0
f 0
nc 7
nop 5
dl 0
loc 42
rs 8.6186
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 ReflectionType;
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
    public function __construct()
51
    {
52
        $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
        $this->annotationReader->addNamespace(Annotation::class);
54
    }
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
    public function hydrate(HydrableObjectInterface $object, array $data) : void
76
    {
77
        $class = new ReflectionClass($object);
78
        $properties = $class->getProperties();
79
        foreach ($properties as $property) {
80
            if ($property->isStatic()) {
81
                continue;
82
            }
83
84
            $key = $property->getName();
85
            $alias = $this->annotationReader->getPropertyAnnotation($property, Annotation\Alias::class);
86
            if ($alias instanceof Annotation\Alias) {
87
                $key = $alias->value;
88
            }
89
90
            $property->setAccessible(true);
91
92
            if (!array_key_exists($key, $data)) {
93
                if (!$property->isInitialized($object)) {
94
                    throw new Exception\MissingRequiredValueException(sprintf(
95
                        'The <%s.%s> property is required.',
96
                        $class->getShortName(),
97
                        $property->getName(),
98
                    ));
99
                }
100
101
                continue;
102
            }
103
104
            if (!$property->hasType()) {
105
                throw new Exception\UntypedObjectPropertyException(sprintf(
106
                    'The <%s.%s> property is not typed.',
107
                    $class->getShortName(),
108
                    $property->getName(),
109
                ));
110
            }
111
112
            $this->hydrateProperty($object, $class, $property, $property->getType(), $data[$key]);
113
        }
114
    }
115
116
    /**
117
     * Hydrates the given property with the given value
118
     *
119
     * @param HydrableObjectInterface $object
120
     * @param ReflectionClass $class
121
     * @param ReflectionProperty $property
122
     * @param ReflectionType $type
123
     * @param mixed $value
124
     *
125
     * @return void
126
     *
127
     * @throws Exception\UnsupportedObjectPropertyTypeException
128
     *         If the given property contains an unsupported type.
129
     *
130
     * @throws Exception\InvalidValueException
131
     *         If the given value isn't valid.
132
     */
133
    private function hydrateProperty(
134
        HydrableObjectInterface $object,
135
        ReflectionClass $class,
136
        ReflectionProperty $property,
137
        ReflectionType $type,
138
        $value
139
    ) : void {
140
        if (null === $value) {
141
            $this->hydratePropertyWithNull($object, $class, $property, $type);
142
            return;
143
        }
144
145
        if (in_array($type->getName(), ['bool', 'int', 'float', 'string'])) {
0 ignored issues
show
Bug introduced by
The method getName() does not exist on ReflectionType. It seems like you code against a sub-type of ReflectionType such as ReflectionNamedType. ( Ignorable by Annotation )

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

145
        if (in_array($type->/** @scrutinizer ignore-call */ getName(), ['bool', 'int', 'float', 'string'])) {
Loading history...
146
            $this->hydratePropertyWithScalar($object, $class, $property, $type, $value);
147
            return;
148
        }
149
150
        if (is_subclass_of($type->getName(), ArrayAccess::class)) {
151
            $this->hydratePropertyWithArray($object, $class, $property, $type, $value);
152
            return;
153
        }
154
155
        if (is_subclass_of($type->getName(), DateTimeInterface::class)) {
156
            $this->hydratePropertyWithDateTime($object, $class, $property, $type, $value);
157
            return;
158
        }
159
160
        if (is_subclass_of($type->getName(), HydrableObjectInterface::class)) {
161
            $this->hydratePropertyWithOneToOneAssociation($object, $class, $property, $type, $value);
162
            return;
163
        }
164
165
        if (is_subclass_of($type->getName(), HydrableObjectCollectionInterface::class)) {
166
            $this->hydratePropertyWithOneToManyAssociation($object, $class, $property, $type, $value);
167
            return;
168
        }
169
170
        throw new Exception\UnsupportedObjectPropertyTypeException(sprintf(
171
            'The <%s.%s> property contains the <%s> unhydrable type.',
172
            $class->getShortName(),
173
            $property->getName(),
174
            $type->getName(),
175
        ));
176
    }
177
178
    /**
179
     * Hydrates the given property with null
180
     *
181
     * @param HydrableObjectInterface $object
182
     * @param ReflectionClass $class
183
     * @param ReflectionProperty $property
184
     * @param ReflectionType $type
185
     *
186
     * @return void
187
     *
188
     * @throws Exception\InvalidValueException
189
     *         If the given value isn't valid.
190
     */
191
    private function hydratePropertyWithNull(
192
        HydrableObjectInterface $object,
193
        ReflectionClass $class,
194
        ReflectionProperty $property,
195
        ReflectionType $type
196
    ) : void {
197
        if (!$type->allowsNull()) {
198
            throw new Exception\InvalidValueException(sprintf(
199
                'The <%s.%s> property does not support null.',
200
                $class->getShortName(),
201
                $property->getName(),
202
            ));
203
        }
204
205
        $property->setValue($object, null);
206
    }
207
208
    /**
209
     * Hydrates the given property with the given scalar value
210
     *
211
     * @param HydrableObjectInterface $object
212
     * @param ReflectionClass $class
213
     * @param ReflectionProperty $property
214
     * @param ReflectionType $type
215
     * @param mixed $value
216
     *
217
     * @return void
218
     *
219
     * @throws Exception\InvalidValueException
220
     *         If the given value isn't valid.
221
     */
222
    private function hydratePropertyWithScalar(
223
        HydrableObjectInterface $object,
224
        ReflectionClass $class,
225
        ReflectionProperty $property,
226
        ReflectionType $type,
227
        $value
228
    ) : void {
229
        if (!is_scalar($value)) {
230
            throw new Exception\InvalidValueException(sprintf(
231
                'The <%s.%s> property only accepts a scalar value.',
232
                $class->getShortName(),
233
                $property->getName(),
234
            ));
235
        }
236
237
        switch ($type->getName()) {
238
            case 'bool':
239
                $property->setValue($object, (bool) $value);
240
                break;
241
            case 'int':
242
                $property->setValue($object, (int) $value);
243
                break;
244
            case 'float':
245
                $property->setValue($object, (float) $value);
246
                break;
247
            case 'string':
248
                $property->setValue($object, (string) $value);
249
                break;
250
        }
251
    }
252
253
    /**
254
     * Hydrates the given property with the given array value
255
     *
256
     * @param HydrableObjectInterface $object
257
     * @param ReflectionClass $class
258
     * @param ReflectionProperty $property
259
     * @param ReflectionType $type
260
     * @param mixed $value
261
     *
262
     * @return void
263
     *
264
     * @throws Exception\InvalidValueException
265
     *         If the given value isn't valid.
266
     */
267
    private function hydratePropertyWithArray(
268
        HydrableObjectInterface $object,
269
        ReflectionClass $class,
270
        ReflectionProperty $property,
271
        ReflectionType $type,
272
        $value
273
    ) : void {
274
        if (!is_array($value)) {
275
            throw new Exception\InvalidValueException(sprintf(
276
                'The <%s.%s> property only accepts an array.',
277
                $class->getShortName(),
278
                $property->getName(),
279
            ));
280
        }
281
282
        $arrayClassName = $type->getName();
283
        $array = new $arrayClassName();
284
        foreach ($value as $offset => $element) {
285
            $array->offsetSet($offset, $element);
286
        }
287
288
        $property->setValue($object, $array);
289
    }
290
291
    /**
292
     * Hydrates the given property with the given date-time value
293
     *
294
     * @param HydrableObjectInterface $object
295
     * @param ReflectionClass $class
296
     * @param ReflectionProperty $property
297
     * @param ReflectionType $type
298
     * @param mixed $value
299
     *
300
     * @return void
301
     *
302
     * @throws Exception\InvalidValueException
303
     *         If the given value isn't valid.
304
     */
305
    private function hydratePropertyWithDateTime(
306
        HydrableObjectInterface $object,
307
        ReflectionClass $class,
308
        ReflectionProperty $property,
309
        ReflectionType $type,
310
        $value
311
    ) : void {
312
        if (!is_string($value) || false === strtotime($value)) {
313
            throw new Exception\InvalidValueException(sprintf(
314
                'The <%s.%s> property only accepts a valid date-time string.',
315
                $class->getShortName(),
316
                $property->getName(),
317
            ));
318
        }
319
320
        $dateTimeClassName = $type->getName();
321
        $dateTime = new $dateTimeClassName($value);
322
        $property->setValue($object, $dateTime);
323
    }
324
325
    /**
326
     * Hydrates the given property with the given one-to-one value
327
     *
328
     * @param HydrableObjectInterface $object
329
     * @param ReflectionClass $class
330
     * @param ReflectionProperty $property
331
     * @param ReflectionType $type
332
     * @param mixed $value
333
     *
334
     * @return void
335
     *
336
     * @throws Exception\InvalidValueException
337
     *         If the given value isn't valid.
338
     */
339
    private function hydratePropertyWithOneToOneAssociation(
340
        HydrableObjectInterface $object,
341
        ReflectionClass $class,
342
        ReflectionProperty $property,
343
        ReflectionType $type,
344
        $value
345
    ) : void {
346
        if (!is_array($value)) {
347
            throw new Exception\InvalidValueException(sprintf(
348
                'The <%s.%s> property only accepts an array.',
349
                $class->getShortName(),
350
                $property->getName(),
351
            ));
352
        }
353
354
        $childObjectClassName = $type->getName();
355
        $childObject = new $childObjectClassName();
356
        $this->hydrate($childObject, $value);
357
        $property->setValue($object, $childObject);
358
    }
359
360
    /**
361
     * Hydrates the given property with the given one-to-many value
362
     *
363
     * @param HydrableObjectInterface $object
364
     * @param ReflectionClass $class
365
     * @param ReflectionProperty $property
366
     * @param ReflectionType $type
367
     * @param mixed $value
368
     *
369
     * @return void
370
     *
371
     * @throws Exception\InvalidValueException
372
     *         If the given value isn't valid.
373
     */
374
    private function hydratePropertyWithOneToManyAssociation(
375
        HydrableObjectInterface $object,
376
        ReflectionClass $class,
377
        ReflectionProperty $property,
378
        ReflectionType $type,
379
        $value
380
    ) : void {
381
        if (!is_array($value)) {
382
            throw new Exception\InvalidValueException(sprintf(
383
                'The <%s.%s> property only accepts an array.',
384
                $class->getShortName(),
385
                $property->getName(),
386
            ));
387
        }
388
389
        $objectCollectionClassName = $type->getName();
390
        $objectCollection = new $objectCollectionClassName();
391
        $collectionObjectClassName = $objectCollectionClassName::T;
392
        foreach ($value as $item) {
393
            $collectionObject = new $collectionObjectClassName();
394
            $this->hydrate($collectionObject, (array) $item);
395
            $objectCollection->add($collectionObject);
396
        }
397
398
        $property->setValue($object, $objectCollection);
399
    }
400
}
401