Passed
Push — master ( ada1ad...0bd463 )
by Gerrit
13:02
created

ObjectMapping::wakeUpMapping()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 14
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 4
eloc 6
c 1
b 1
f 0
nc 8
nop 1
dl 0
loc 14
ccs 0
cts 0
cp 0
crap 20
rs 10
1
<?php
2
/**
3
 * Copyright (C) 2018 Gerrit Addiks.
4
 * This package (including this file) was released under the terms of the GPL-3.0.
5
 * You should have received a copy of the GNU General Public License along with this program.
6
 * If not, see <http://www.gnu.org/licenses/> or send me a mail so i can send you a copy.
7
 *
8
 * @license GPL-3.0
9
 *
10
 * @author Gerrit Addiks <[email protected]>
11
 */
12
13
namespace Addiks\RDMBundle\Mapping;
14
15
use Doctrine\DBAL\Schema\Column;
16
use Addiks\RDMBundle\Mapping\MappingInterface;
17
use Webmozart\Assert\Assert;
18
use Addiks\RDMBundle\Hydration\HydrationContextInterface;
19
use Addiks\RDMBundle\Mapping\CallDefinitionInterface;
20
use Addiks\RDMBundle\Exception\FailedRDMAssertionException;
21
use ReflectionClass;
22
use ReflectionProperty;
23
use ReflectionException;
24
use Symfony\Component\DependencyInjection\ContainerInterface;
25
use Doctrine\DBAL\Types\Type;
26
use Doctrine\DBAL\Connection;
27
use BackedEnum;
0 ignored issues
show
Bug introduced by
The type BackedEnum 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...
28
use UnitEnum;
0 ignored issues
show
Bug introduced by
The type UnitEnum 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...
29
30
final class ObjectMapping implements MappingInterface
31
{
32
33
    /**
34
     * @var class-string
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...
35
     */
36
    private $className;
37
38
    /**
39
     * @var array<MappingInterface>
40
     */
41
    private $fieldMappings = array();
42
43
    /**
44
     * @var Column|null
45
     */
46
    private $column;
47
48
    /**
49
     * @var CallDefinitionInterface|null
50
     */
51
    private $factory;
52
53
    /**
54
     * @var CallDefinitionInterface|null
55
     */
56
    private $serializer;
57
58
    /**
59
     * @var string
60
     */
61
    private $origin;
62
63
    /**
64
     * @var string|null
65
     */
66
    private $id;
67
68
    /**
69
     * @var string|null
70
     */
71
    private $referencedId;
72 17
73
    /** @param class-string $className */
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...
74
    public function __construct(
75
        string $className,
76
        array $fieldMappings,
77
        Column $column = null,
78
        string $origin = "undefined",
79
        CallDefinitionInterface $factory = null,
80
        CallDefinitionInterface $serializer = null,
81
        string $id = null,
82 17
        string $referencedId = null
83 17
    ) {
84 17
        $this->className = $className;
85 17
        $this->factory = $factory;
86 17
        $this->column = $column;
87 17
        $this->serializer = $serializer;
88 17
        $this->origin = $origin;
89
        $this->id = $id;
90 17
        $this->referencedId = $referencedId;
91 17
92 17
        foreach ($fieldMappings as $fieldName => $fieldMapping) {
93
            Assert::isInstanceOf($fieldMapping, MappingInterface::class);
94 17
            Assert::false(is_numeric($fieldName));
95
96
            $this->fieldMappings[$fieldName] = $fieldMapping;
97
        }
98 1
    }
99
100 1
    public function getClassName(): string
101
    {
102
        return $this->className;
103 1
    }
104
105 1
    public function getDBALColumn(): ?Column
106
    {
107
        return $this->column;
108 1
    }
109
110 1
    public function getFieldMappings(): array
111
    {
112
        return $this->fieldMappings;
113 1
    }
114
115 1
    public function describeOrigin(): string
116
    {
117
        return $this->origin;
118 2
    }
119
120
    public function collectDBALColumns(): array
121 2
    {
122
        /** @var array<Column> $additionalColumns */
123 2
        $additionalColumns = array();
124 1
125
        if ($this->column instanceof Column) {
126
            $additionalColumns[] = $this->column;
127 2
        }
128
129
        foreach ($this->fieldMappings as $fieldMapping) {
130 2
            /** @var MappingInterface $fieldMapping */
131
132 2
            $additionalColumns = array_merge(
133
                $additionalColumns,
134
                $fieldMapping->collectDBALColumns()
135
            );
136 2
        }
137
138
        return $additionalColumns;
139 1
    }
140
141 1
    public function getFactory(): ?CallDefinitionInterface
142
    {
143
        return $this->factory;
144 1
    }
145
146 1
    public function getSerializer(): ?CallDefinitionInterface
147
    {
148
        return $this->serializer;
149 1
    }
150
151 1
    public function getId(): ?string
152
    {
153
        return $this->id;
154 1
    }
155
156 1
    public function getReferencedId(): ?string
157
    {
158
        return $this->referencedId;
159 3
    }
160
161
    public function resolveValue(
162
        HydrationContextInterface $context,
163
        array $dataFromAdditionalColumns
164 3
    ) {
165
        /** @var object|null $object */
166
        $object = null;
167 3
168
        /** @var string $columnName */
169 3
        $columnName = $this->column?->getName() ?? '';
170
171 3
        # During creation of an object, the class-name is on the top of that creation stack
172
        $context->pushOnObjectHydrationStack($this->className);
173
174 3
        $reflectionClass = new ReflectionClass($this->className);
175
176 2
        if (!empty($this->referencedId)) {
177
            $object = $context->getRegisteredValue($this->referencedId);
178 2
179
        } elseif ($this->factory instanceof CallDefinitionInterface) {
180 1
            /** @var array<string, string> $factoryData */
181
            $factoryData = $dataFromAdditionalColumns;
182
183 1
            if ($this->column instanceof Column && !array_key_exists("", $factoryData)) {
184
                /** @var Type $type */
185 1
                $type = $this->column->getType();
186
187
                if (array_key_exists($columnName, $dataFromAdditionalColumns)) {
188
                    /** @var Connection $connection */
189
                    $connection = $context->getEntityManager()->getConnection();
190
191
                    if (!is_null($dataFromAdditionalColumns[$columnName])) {
192
                        $dataFromAdditionalColumns[$columnName] = $type->convertToPHPValue(
193
                            $dataFromAdditionalColumns[$columnName],
194
                            $connection->getDatabasePlatform()
195
                        );
196
                    }
197
198
                    $factoryData[""] = $dataFromAdditionalColumns[$columnName];
199
                }
200 2
            }
201
202
            $object = $this->factory->execute(
203
                $context,
204
                $factoryData
205
            );
206 1
            
207 1
        } elseif (is_a($this->className, BackedEnum::class, true)) {
208
            $object = call_user_func($this->className . '::from', $dataFromAdditionalColumns[$columnName]);
209
#            $object = {$this->className}::from($dataFromAdditionalColumns[$columnName]);
210
211 3
        } elseif (is_a($this->className, UnitEnum::class, true)) {
212
            $object = constant(sprintf('%s::%s', $this->className, $dataFromAdditionalColumns[$columnName]));
213 3
214 3
        } else {
215
            if ($reflectionClass->isInstantiable()) {
216 3
                $object = $reflectionClass->newInstanceWithoutConstructor();
217 2
            }
218
        }
219
220 3
        if (is_object($object)) {
221
            // Replace the class-name with the created object on top of the hydration stack
222
            $context->popFromObjectHydrationStack();
223
            $context->pushOnObjectHydrationStack($object);
224 3
225
            if (!empty($this->id) && !$context->hasRegisteredValue($this->id)) {
226
                $context->registerValue($this->id, $object);
227
            }
228
229
            foreach ($this->fieldMappings as $fieldName => $fieldMapping) {
230 3
                /** @var MappingInterface $fieldMapping */
231
232 3
                /** @var mixed $fieldValue */
233
                $fieldValue = $fieldMapping->resolveValue(
234
                    $context,
235
                    $dataFromAdditionalColumns
236 3
                );
237
238
                /** @var ReflectionClass $propertyReflectionClass */
239
                $propertyReflectionClass = $reflectionClass;
240
241
                while (is_object($propertyReflectionClass) && !$propertyReflectionClass->hasProperty($fieldName)) {
242
                    $propertyReflectionClass = $propertyReflectionClass->getParentClass();
243
                }
244
245
                if (!is_object($propertyReflectionClass)) {
246 3
                    throw new ReflectionException(sprintf(
247
                        "Property '%s' does not exist on class '%s' as defined at '%s'",
248 3
                        $fieldName,
249 3
                        $reflectionClass->getName(),
250
                        $reflectionClass->getFileName()
251
                    ));
252
                }
253 3
254
                /** @var ReflectionProperty $reflectionProperty */
255 3
                $reflectionProperty = $propertyReflectionClass->getProperty($fieldName);
256
257
                $reflectionProperty->setAccessible(true);
258 2
                $reflectionProperty->setValue($object, $fieldValue);
259
            }
260
        }
261
262
        $context->popFromObjectHydrationStack();
263 2
264
        return $object;
265 2
    }
266
267 2
    public function revertValue(
268
        HydrationContextInterface $context,
269 2
        $valueFromEntityField
270
    ): array {
271 2
        /** @var array<scalar> $data */
272
        $data = array();
273
274
        $this->assertValue($context, [], $valueFromEntityField);
275 2
276
        $reflectionClass = new ReflectionClass($this->className);
277 2
278
        $context->pushOnObjectHydrationStack($valueFromEntityField);
279
280
        foreach ($this->fieldMappings as $fieldName => $fieldMapping) {
281 2
            /** @var MappingInterface $fieldMapping */
282
283
            /** @var ReflectionClass $propertyReflectionClass */
284
            $propertyReflectionClass = $reflectionClass;
285
286
            while (is_object($propertyReflectionClass) && !$propertyReflectionClass->hasProperty($fieldName)) {
287
                $propertyReflectionClass = $propertyReflectionClass->getParentClass();
288
            }
289
290
            if (!is_object($propertyReflectionClass)) {
291 2
                throw new ReflectionException(sprintf(
292 2
                    "Property %s does not exist in class %s. (Defined %s)",
293
                    $fieldName,
294
                    $this->className,
295 2
                    $this->origin
296
                ));
297 2
            }
298 1
299
            /** @var ReflectionProperty $reflectionProperty */
300
            $reflectionProperty = $propertyReflectionClass->getProperty($fieldName);
301
            $reflectionProperty->setAccessible(true);
302 2
303
            /** @var mixed $valueFromField */
304
            $valueFromField = null;
305
306
            if (is_object($valueFromEntityField) && $reflectionProperty->isInitialized($valueFromEntityField)) {
307 2
                $valueFromField = $reflectionProperty->getValue($valueFromEntityField);
308
            }
309
310
            /** @var array<string, mixed> $fieldData */
311
            $fieldData = $fieldMapping->revertValue(
312 2
                $context,
313
                $valueFromField
314
            );
315 2
316
            if (array_key_exists("", $fieldData)) {
317
                $fieldData[$fieldName] = $fieldData[""];
318
                unset($fieldData[""]);
319
            }
320
321
            $data = array_merge($data, $fieldData);
322
        }
323
324
        /** @var string $columnName */
325
        $columnName = '';
326
327
        if ($this->column instanceof Column) {
328
            $columnName = $this->column->getName();
329
        }
330
331
        if ($this->serializer instanceof CallDefinitionInterface) {
332
            $data[$columnName] = $this->serializer->execute(
333
                $context,
334
                array_merge($data, ['' => $valueFromEntityField])
335
            );
336
337
            if ($this->column instanceof Column) {
338
                /** @var Type $type */
339
                $type = $this->column->getType();
340
341
                /** @var Connection $connection */
342 2
                $connection = $context->getEntityManager()->getConnection();
343
344 2
                $data[$columnName] = $type->convertToDatabaseValue(
345
                    $data[$columnName],
346
                    $connection->getDatabasePlatform()
347 3
                );
348
            }
349
            
350
        } elseif (is_a($this->className, BackedEnum::class, true)) {
351
            $data[$columnName] = $valueFromEntityField->value;
352 3
353 1
        } elseif (is_a($this->className, UnitEnum::class, true)) {
354 1
            $data[$columnName] = $valueFromEntityField->name;
355 1
        }
356 1
357
        $context->popFromObjectHydrationStack();
358
359
        return $data;
360
    }
361 1
362
    public function assertValue(
363 1
        HydrationContextInterface $context,
364 1
        array $dataFromAdditionalColumns,
365
        $actualValue
366
    ): void {
367 1
        if (!is_null($actualValue) && !$actualValue instanceof $this->className) {
368 1
            throw FailedRDMAssertionException::expectedInstanceOf(
369
                $this->className,
370
                is_object($actualValue) ?get_class($actualValue) :gettype($actualValue),
371
                $this->origin
372
            );
373
        }
374
    }
375
376
    public function wakeUpMapping(ContainerInterface $container): void
377
    {
378
        if ($this->factory instanceof CallDefinitionInterface) {
379
            $this->factory->wakeUpCall($container);
380
        }
381
382
        if ($this->serializer instanceof CallDefinitionInterface) {
383
            $this->serializer->wakeUpCall($container);
384
        }
385
        
386
        foreach ($this->fieldMappings as $fieldName => $fieldMapping) {
387
            /** @var MappingInterface $fieldMapping */
388
389
            $fieldMapping->wakeUpMapping($container);
390
        }
391
    }
392
393
}
394