Passed
Pull Request — master (#33)
by Vincent
06:53
created

MapperHydrator::readFromProperty()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3.1406

Importance

Changes 0
Metric Value
eloc 8
c 0
b 0
f 0
dl 0
loc 13
ccs 6
cts 8
cp 0.75
rs 10
cc 3
nc 3
nop 4
crap 3.1406
1
<?php
2
3
namespace Bdf\Prime\Entity\Hydrator;
4
5
use Bdf\Prime\Entity\Hydrator\Exception\FieldNotDeclaredException;
6
use Bdf\Prime\Entity\Hydrator\Exception\InvalidTypeException;
7
use Bdf\Prime\Entity\Hydrator\Exception\UninitializedPropertyException;
8
use Bdf\Prime\Entity\Instantiator\InstantiatorInterface;
9
use Bdf\Prime\Mapper\Metadata;
10
use Bdf\Prime\Platform\PlatformTypesInterface;
11
use Error;
12
use ReflectionException;
13
use ReflectionProperty;
14
use stdClass;
15
use TypeError;
16
17
/**
18
 * Base implementation for @see MapperHydratorInterface
19
 *
20
 * Prefer use generated hydrators on production
0 ignored issues
show
introduced by
Doc comment long description must end with a full stop
Loading history...
21
 */
22
class MapperHydrator implements MapperHydratorInterface
23
{
24
    /**
25
     * @var InstantiatorInterface
26
     */
27
    protected $instantiator;
28
29
    /**
30
     * @var Metadata
31
     */
32
    protected $metadata;
33
34
    /**
35
     * Property accessors, indexed by attribute name
36
     *
37
     * @var ReflectionProperty[][]
38
     */
39
    private $reflectionProperties = [];
40
41
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $instantiator should have a doc-comment as per coding-style.
Loading history...
42
     * {@inheritdoc}
43
     */
44 240
    public function setPrimeInstantiator(InstantiatorInterface $instantiator)
45
    {
46 240
        $this->instantiator = $instantiator;
47 240
    }
48
49
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $metadata should have a doc-comment as per coding-style.
Loading history...
50
     * {@inheritdoc}
51
     */
52 240
    public function setPrimeMetadata(Metadata $metadata)
53
    {
54 240
        $this->metadata = $metadata;
55 240
    }
56
57
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $object should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $attributes should have a doc-comment as per coding-style.
Loading history...
58
     * {@inheritdoc}
59
     */
60 431
    public function flatExtract($object, array $attributes = null)
61
    {
62 431
        $values = [];
63 431
        $cache  = [];
64
65 431
        $attributes = $attributes === null
0 ignored issues
show
Coding Style introduced by
The value of a comparison must not be assigned to a variable
Loading history...
66 425
            ? $this->metadata->attributes
0 ignored issues
show
Coding Style introduced by
Inline shorthand IF statement must be declared on a single line
Loading history...
67 431
            : array_intersect_key($this->metadata->attributes, $attributes);
68
69 431
        foreach ($attributes as $attribute => $metadata) {
70 431
            if (isset($metadata['embedded'])) {
71 349
                $path = $metadata['embedded'];
72
73 349
                if (empty($cache[$path])) {
74 349
                    $cache[$path] = $this->getOwnerObject($object, $metadata);
75
                }
76
77 349
                $value = $cache[$path] ? $this->readFromAttribute($cache[$path], $metadata) : null;
0 ignored issues
show
Coding Style introduced by
The value of a comparison must not be assigned to a variable
Loading history...
Coding Style introduced by
Inline shorthand IF statement requires brackets around comparison
Loading history...
78
            } else {
79 431
                $value = $this->readFromAttribute($object, $metadata);
80
            }
81
82 431
            $values[$attribute] = $value;
83
        }
84
85 431
        return $values;
86
    }
87
88
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $object should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $data should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $types should have a doc-comment as per coding-style.
Loading history...
89
     * {@inheritdoc}
90
     */
91 288
    public function flatHydrate($object, array $data, PlatformTypesInterface $types)
0 ignored issues
show
Coding Style introduced by
Function's nesting level (6) exceeds 5; consider refactoring the function
Loading history...
92
    {
93 288
        $metadata  = $this->metadata->fields;
0 ignored issues
show
introduced by
Equals sign not aligned with surrounding assignments; expected 1 space but found 2 spaces
Loading history...
94 288
        $embeddeds = $this->metadata->embeddeds;
95
        $cacheEmbedded = [
96 288
            'root' => $object
0 ignored issues
show
introduced by
A comma should follow the last multiline array item. Found: $object
Loading history...
97
        ];
98
99 288
        foreach ($data as $field => $value) {
100 288
            if (!isset($metadata[$field])) {
101 1
                continue;
102
            }
103
104 288
            $value = $types->get($metadata[$field]['type'])->fromDatabase($value, $metadata[$field]['phpOptions']);
105
106 288
            if (isset($metadata[$field]['embedded'])) {
107 205
                $path = $metadata[$field]['embedded'];
108
109
                //creation du cache d'objet. Le but est de parcourir les paths de l'embedded
0 ignored issues
show
Coding Style introduced by
No space found before comment text; expected "// creation du cache d'objet. Le but est de parcourir les paths de l'embedded" but found "//creation du cache d'objet. Le but est de parcourir les paths de l'embedded"
Loading history...
Coding Style Documentation introduced by
Inline comments must start with a capital letter
Loading history...
110
                //de creer les objets et de les associés entre eux
0 ignored issues
show
Coding Style introduced by
No space found before comment text; expected "// de creer les objets et de les associés entre eux" but found "//de creer les objets et de les associés entre eux"
Loading history...
111
                //ex:
0 ignored issues
show
Coding Style introduced by
No space found before comment text; expected "// ex:" but found "//ex:"
Loading history...
112
                //  'root.wrapper.offer.id'  id est embedded dans offer qui est embedded dans wrapper, etc...
0 ignored issues
show
Coding Style introduced by
Expected 1 space before comment text but found 2; use block comment if you need indentation
Loading history...
113
                //   l'attribute id peut etre parsé sans que wrapper ne soit déjà construit (parsqu'il n'a pas d'attribut,
0 ignored issues
show
Coding Style introduced by
Expected 1 space before comment text but found 3; use block comment if you need indentation
Loading history...
introduced by
Comment indentation error, expected only 2 spaces
Loading history...
114
                //   ou parce qu'il est définit avant dans le select)
0 ignored issues
show
Coding Style introduced by
Expected 1 space before comment text but found 3; use block comment if you need indentation
Loading history...
115 205
                if (empty($cacheEmbedded[$path])) {
116 205
                    for ($i = 0, $l = count($embeddeds[$path]['paths']); $i < $l; $i++) {
117 205
                        $pathCursor = $embeddeds[$path]['paths'][$i];
118
119 205
                        if (empty($cacheEmbedded[$pathCursor])) {
120 205
                            $parentMeta = $embeddeds[$pathCursor];
121
122 205
                            if (empty($parentMeta['polymorph'])) {
123 205
                                $cacheEmbedded[$pathCursor] = $this->instantiator->instantiate($parentMeta['class'], $parentMeta['hint']);
124
                            } else {
125 1
                                $cacheEmbedded[$pathCursor] = $this->instantiator->instantiate(
126 1
                                    $className = $parentMeta['class_map'][$data[$parentMeta['discriminator_field']]],
0 ignored issues
show
Coding Style introduced by
Assignments must be the first block of code on a line
Loading history...
127 1
                                    $parentMeta['hints'][$className]
128
                                );
129
                            }
130
131 205
                            $this->writeToEmbedded(
132 205
                                $cacheEmbedded[$parentMeta['parentPath']],
133 205
                                $parentMeta,
134 205
                                $cacheEmbedded[$parentMeta['path']],
135 205
                                true
136
                            );
137
                        }
138
                    }
0 ignored issues
show
Coding Style introduced by
End comment for long condition not found; expected "//end for"
Loading history...
139
                }
0 ignored issues
show
Coding Style introduced by
End comment for long condition not found; expected "//end if"
Loading history...
140
141 205
                $this->writeToAttribute($cacheEmbedded[$path], $metadata[$field], $value, true);
142
            } else {
143 288
                $this->writeToAttribute($object, $metadata[$field], $value, true);
144
            }
0 ignored issues
show
Coding Style introduced by
End comment for long condition not found; expected "//end if"
Loading history...
145
        }
0 ignored issues
show
Coding Style introduced by
End comment for long condition not found; expected "//end foreach"
Loading history...
146 288
    }
147
148
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $attribute should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $object should have a doc-comment as per coding-style.
Loading history...
149
     * {@inheritdoc}
150
     */
0 ignored issues
show
Coding Style Documentation introduced by
Missing @throws tag in function comment
Loading history...
151 260
    public function extractOne($object, $attribute)
152
    {
153 260
        if (!isset($this->metadata->attributes[$attribute])) {
154 38
            if (!isset($this->metadata->embeddeds[$attribute])) {
155 2
                throw new FieldNotDeclaredException($this->metadata->entityClass, $attribute);
156
            }
157
158 36
            return $this->readFromEmbedded($object, $this->metadata->embeddeds[$attribute]);
159
        } else {
160 255
            $ownerObject = $this->getOwnerObject($object, $this->metadata->attributes[$attribute]);
161
162
            // Polymorphic embedded not instantiated
163 255
            if ($ownerObject === null) {
164 1
                return null;
165
            }
166
167 255
            return $this->readFromAttribute($ownerObject, $this->metadata->attributes[$attribute]);
168
        }
169
    }
170
171
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $attribute should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $object should have a doc-comment as per coding-style.
Loading history...
Coding Style introduced by
Parameter $value should have a doc-comment as per coding-style.
Loading history...
172
     * {@inheritdoc}
173
     */
0 ignored issues
show
Coding Style Documentation introduced by
Missing @throws tag in function comment
Loading history...
174 223
    public function hydrateOne($object, $attribute, $value)
175
    {
176 223
        if (!isset($this->metadata->attributes[$attribute])) {
177 112
            if (!isset($this->metadata->embeddeds[$attribute])) {
178 2
                throw new FieldNotDeclaredException($this->metadata->entityClass, $attribute);
179
            }
180
181 110
            $this->writeToEmbedded($object, $this->metadata->embeddeds[$attribute], $value, false);
182
        } else {
183 117
            $ownerObject = $this->getOwnerObject($object, $this->metadata->attributes[$attribute]);
184
185
            // Polymorphic embedded not instantiated
186 117
            if ($ownerObject === null) {
187 1
                throw new \InvalidArgumentException('Cannot write to attribute '.$attribute.' : the embedded entity cannot be resolved');
188
            }
189
190 116
            $this->writeToAttribute($ownerObject, $this->metadata->attributes[$attribute], $value, false);
191
        }
192 220
    }
193
194
    /**
195
     * Get owner object attribute
196
     *
197
     * @param object $entity
198
     * @param array $metadata Metadata d'un attribut a retrouver
199
     *
200
     * @return object|null The object, or null if cannot be instantiated (polymorph)
201
     */
202 442
    protected function getOwnerObject($entity, array $metadata)
203
    {
204 442
        if (!isset($metadata['embedded'])) {
205 310
            return $entity;
206
        }
207
208 362
        $embeddeds = $this->metadata->embeddeds;
209 362
        $current = $entity;
210 362
        $embeddedMeta = $embeddeds[$metadata['embedded']];
211 362
        $embedded = null;
212
213
        //parcourt des paths pour descendre juqu'à l'objet propriétaire des meta données
0 ignored issues
show
Coding Style Documentation introduced by
Inline comments must start with a capital letter
Loading history...
Coding Style introduced by
No space found before comment text; expected "// parcourt des paths pour descendre juqu'à l'objet propriétaire des meta données" but found "//parcourt des paths pour descendre juqu'à l'objet propriétaire des meta données"
Loading history...
214
        //si l'objet n'existe pas, on le créé avant de l'associer à son object parent
0 ignored issues
show
Coding Style introduced by
No space found before comment text; expected "// si l'objet n'existe pas, on le créé avant de l'associer à son object parent" but found "//si l'objet n'existe pas, on le créé avant de l'associer à son object parent"
Loading history...
215 362
        for ($i = 0, $l = count($embeddedMeta['paths']); $i < $l; $i++) {
216 362
            $parentMeta = $embeddeds[$embeddedMeta['paths'][$i]];
217
218
            try {
219 362
                $embedded = $this->readFromEmbedded($current, $parentMeta);
220
            } catch (UninitializedPropertyException $e) { // The property is not initialized
0 ignored issues
show
Coding Style introduced by
Comments may not appear after statements
Loading history...
introduced by
There should be no white space after an opening "{"
Loading history...
221
                $embedded = null;
222
            }
223
224 362
            if ($embedded === null) {
225 213
                if (!isset($parentMeta['class'])) {
226 3
                    return null;
227
                }
228
229 211
                $embedded = $this->instantiator->instantiate($parentMeta['class'], $parentMeta['hint']);
230
231
                // This writes should never fail : $embedded is not null
232 211
                $this->writeToEmbedded($current, $parentMeta, $embedded, false);
233
            }
234
235 361
            $current = $embedded;
236
        }
0 ignored issues
show
Coding Style introduced by
End comment for long condition not found; expected "//end for"
Loading history...
237
238 361
        return $embedded;
239
    }
240
241
    /**
242
     * Read a value from an entity, with attribute metadata
243
     *
244
     * @param object $entity
245
     * @param array $metadata
246
     *
247
     * @return mixed
248
     *
249
     * @throws ReflectionException When property do not exist on the object
0 ignored issues
show
introduced by
@throws comment must be on the next line
Loading history...
introduced by
@throws tag comment must end with a full stop
Loading history...
250
     * @throws UninitializedPropertyException When the property is not initialized
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
251
     */
0 ignored issues
show
Coding Style introduced by
Expected 1 @throws tag(s) in function comment; 2 found
Loading history...
252 451
    protected function readFromAttribute($entity, array $metadata)
253
    {
254 451
        $attribute = $metadata['attribute'];
255 451
        $class = get_class($entity);
256
257 451
        if (isset($this->reflectionProperties[$class][$attribute])) {
258
            try {
259 428
                return $this->reflectionProperties[$class][$attribute]->getValue($entity);
260
            } catch (Error $e) {
261
                throw new UninitializedPropertyException($class, $this->reflectionProperties[$class][$attribute]->name, $e);
262
            }
263
        }
264
265 106
        if (!isset($metadata['embedded'])) {
266 103
            $property = $attribute;
267
        } else {
268 72
            $property = substr($attribute, strlen($metadata['embedded']) + 1);
0 ignored issues
show
Coding Style introduced by
Operation must be bracketed
Loading history...
269
        }
270
271 106
        return $this->readFromProperty($class, $property, $attribute, $entity);
272
    }
273
274
    /**
275
     * Read a value from an entity, with embedded metadata
276
     *
277
     * @param object $entity
278
     * @param array $metadata
279
     *
280
     * @return mixed
281
     *
282
     * @throws ReflectionException When property do not exist on the object
0 ignored issues
show
introduced by
@throws comment must be on the next line
Loading history...
introduced by
@throws tag comment must end with a full stop
Loading history...
283
     * @throws UninitializedPropertyException When the property is not initialized
0 ignored issues
show
introduced by
@throws tag comment must end with a full stop
Loading history...
284
     */
0 ignored issues
show
Coding Style introduced by
Expected 1 @throws tag(s) in function comment; 2 found
Loading history...
285 372
    protected function readFromEmbedded($entity, array $metadata)
286
    {
287 372
        $attribute = $metadata['path'];
288 372
        $class = get_class($entity);
289
290 372
        if (isset($this->reflectionProperties[$class][$attribute])) {
291
            try {
292 344
                return $this->reflectionProperties[$class][$attribute]->getValue($entity);
293
            } catch (Error $e) {
294
                throw new UninitializedPropertyException($class, $this->reflectionProperties[$class][$attribute]->name, $e);
295
            }
296
        }
297
298 76
        if ($metadata['parentPath'] === 'root') {
299 76
            $property = $attribute;
300
        } else {
301 9
            $property = substr($attribute, strlen($metadata['parentPath']) + 1);
0 ignored issues
show
Coding Style introduced by
Operation must be bracketed
Loading history...
302
        }
303
304 76
        return $this->readFromProperty($class, $property, $attribute, $entity);
305
    }
306
307
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $skipInvalid should have a doc-comment as per coding-style.
Loading history...
introduced by
Parameter $skipInvalid is not described in comment
Loading history...
308
     * @param object $entity
309
     * @param array $metadata
310
     * @param mixed $value
311
     */
312 337
    protected function writeToAttribute($entity, array $metadata, $value, bool $skipInvalid)
313
    {
314 337
        $attribute = $metadata['attribute'];
315 337
        $class = get_class($entity);
316
317 337
        if (isset($this->reflectionProperties[$class][$attribute])) {
318 316
            $this->writeToReflection($this->reflectionProperties[$class][$attribute], $entity, $value, $skipInvalid, $metadata);
319 316
            return;
320
        }
321
322 40
        if (!isset($metadata['embedded'])) {
323 34
            $property = $attribute;
324
        } else {
325 15
            $property = substr($attribute, strlen($metadata['embedded']) + 1);
0 ignored issues
show
Coding Style introduced by
Operation must be bracketed
Loading history...
326
        }
327
328 40
        if ($class === stdClass::class) {
329 5
            $entity->{$property} = $value;
330 5
            return;
331
        }
332
333 35
        $this->reflectionProperties[$class][$attribute] = $reflectionProperty = new ReflectionProperty($class, $property);
0 ignored issues
show
Coding Style introduced by
Assignments must be the first block of code on a line
Loading history...
334 35
        $reflectionProperty->setAccessible(true);
335
336 35
        $this->writeToReflection($reflectionProperty, $entity, $value, $skipInvalid, $metadata);
337 35
    }
338
339
    /**
0 ignored issues
show
Coding Style introduced by
Parameter $skipInvalid should have a doc-comment as per coding-style.
Loading history...
introduced by
Parameter $skipInvalid is not described in comment
Loading history...
340
     * @param object $entity
341
     * @param array $metadata
342
     * @param mixed $value
343
     */
344 321
    protected function writeToEmbedded($entity, array $metadata, $value, bool $skipInvalid)
345
    {
346 321
        $attribute = $metadata['path'];
347 321
        $class = get_class($entity);
348
349 321
        if (isset($this->reflectionProperties[$class][$attribute])) {
350 310
            $this->writeToReflection($this->reflectionProperties[$class][$attribute], $entity, $value, $skipInvalid, $metadata);
351 310
            return;
352
        }
353
354 32
        if ($metadata['parentPath'] === 'root') {
355 28
            $property = $attribute;
356
        } else {
357 5
            $property = substr($attribute, strlen($metadata['parentPath']) + 1);
0 ignored issues
show
Coding Style introduced by
Operation must be bracketed
Loading history...
358
        }
359
360 32
        if ($class === stdClass::class) {
361 3
            $entity->{$property} = $value;
362 3
            return;
363
        }
364
365 29
        $this->reflectionProperties[$class][$attribute] = $reflectionProperty = new ReflectionProperty($class, $property);
0 ignored issues
show
Coding Style introduced by
Assignments must be the first block of code on a line
Loading history...
366 29
        $reflectionProperty->setAccessible(true);
367
368 29
        $this->writeToReflection($reflectionProperty, $entity, $value, $skipInvalid, $metadata);
369 29
    }
370
371
    /**
372
     * Simple property read
373
     *
374
     * @param class-string $class
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...
introduced by
Expected "classstring" but found "class-string" for parameter type
Loading history...
375
     * @param string $property
376
     * @param string $attribute
377
     * @param object $entity
378
     * @return mixed|null
0 ignored issues
show
Coding Style introduced by
Tag @return cannot be grouped with parameter tags in a doc comment
Loading history...
379
     *
380
     * @throws ReflectionException
381
     * @throws UninitializedPropertyException
382
     */
0 ignored issues
show
Coding Style introduced by
Expected 1 @throws tag(s) in function comment; 2 found
Loading history...
383 110
    private function readFromProperty(string $class, string $property, string $attribute, $entity)
0 ignored issues
show
Coding Style introduced by
Private method name "MapperHydrator::readFromProperty" must be prefixed with an underscore
Loading history...
384
    {
385 110
        if ($class === stdClass::class) {
386 9
            return $entity->$property ?? null;
0 ignored issues
show
Coding Style introduced by
Operation must be bracketed
Loading history...
387
        }
388
389 105
        $this->reflectionProperties[$class][$attribute] = $propertyReflection = new ReflectionProperty($class, $property);
0 ignored issues
show
Coding Style introduced by
Assignments must be the first block of code on a line
Loading history...
390 105
        $propertyReflection->setAccessible(true);
391
392
        try {
393 105
            return $propertyReflection->getValue($entity);
394
        } catch (Error $e) {
395
            throw new UninitializedPropertyException($class, $property, $e);
396
        }
397
    }
398
399
    /**
400
     * Check if the value should not be hydrated, in case of null value on not nullable property
401
     *
402
     * @param ReflectionProperty $property
403
     * @param mixed $value
404
     *
405
     * @return bool
0 ignored issues
show
Coding Style introduced by
Expected "boolean" but found "bool" for function return type
Loading history...
406
     */
407 284
    private function shouldSkipValue(ReflectionProperty $property, $value): bool
0 ignored issues
show
Coding Style introduced by
Private method name "MapperHydrator::shouldSkipValue" must be prefixed with an underscore
Loading history...
408
    {
409 284
        if (PHP_VERSION_ID < 70400 || $value !== null) {
410 284
            return false;
411
        }
412
413
        return $property->hasType() && !$property->getType()->allowsNull();
0 ignored issues
show
Coding Style introduced by
Boolean operators are not allowed outside of control structure conditions
Loading history...
414
    }
415
416
    /**
417
     * @param ReflectionProperty $reflectionProperty
418
     * @param object $entity
419
     * @param mixed $value
420
     * @param bool $skipInvalid
0 ignored issues
show
Coding Style introduced by
Expected "boolean" but found "bool" for parameter type
Loading history...
421
     * @param array $metadata
422
     *
423
     * @throws InvalidTypeException
424
     */
425 383
    private function writeToReflection(ReflectionProperty $reflectionProperty, $entity, $value, bool $skipInvalid, array $metadata)
0 ignored issues
show
Coding Style introduced by
Private method name "MapperHydrator::writeToReflection" must be prefixed with an underscore
Loading history...
426
    {
427 383
        if (!$skipInvalid || !$this->shouldSkipValue($reflectionProperty, $value)) {
428
            try {
429 383
                $reflectionProperty->setValue($entity, $value);
430
            } catch (TypeError $e) {
431
                throw new InvalidTypeException($e, $metadata['type'] ?? null);
0 ignored issues
show
Coding Style introduced by
Operation must be bracketed
Loading history...
432
            }
433
        }
434 383
    }
435
}
436