Passed
Push — master ( 7b5f02...eec92d )
by Gerrit
02:49
created

ObjectMapping::describeOrigin()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 1
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
28
final class ObjectMapping implements MappingInterface
29
{
30
31
    /**
32
     * @var class-string
33
     */
34
    private $className;
35
36
    /**
37
     * @var array<MappingInterface>
38
     */
39
    private $fieldMappings = array();
40
41
    /**
42
     * @var Column|null
43
     */
44
    private $column;
45
46
    /**
47
     * @var CallDefinitionInterface|null
48
     */
49
    private $factory;
50
51
    /**
52
     * @var CallDefinitionInterface|null
53
     */
54
    private $serializer;
55
56
    /**
57
     * @var string
58
     */
59
    private $origin;
60
61
    /**
62
     * @var string|null
63
     */
64
    private $id;
65
66
    /**
67
     * @var string|null
68
     */
69
    private $referencedId;
70
71
    /** @param class-string $className */
0 ignored issues
show
Documentation introduced by
The doc-type class-string could not be parsed: Unknown type name "class-string" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
72 17
    public function __construct(
73
        string $className,
74
        array $fieldMappings,
75
        Column $column = null,
76
        string $origin = "undefined",
77
        CallDefinitionInterface $factory = null,
78
        CallDefinitionInterface $serializer = null,
79
        string $id = null,
80
        string $referencedId = null
81
    ) {
82 17
        $this->className = $className;
83 17
        $this->factory = $factory;
84 17
        $this->column = $column;
85 17
        $this->serializer = $serializer;
86 17
        $this->origin = $origin;
87 17
        $this->id = $id;
88 17
        $this->referencedId = $referencedId;
89
90 17
        foreach ($fieldMappings as $fieldName => $fieldMapping) {
91 17
            Assert::isInstanceOf($fieldMapping, MappingInterface::class);
92 17
            Assert::false(is_numeric($fieldName));
93
94 17
            $this->fieldMappings[$fieldName] = $fieldMapping;
95
        }
96 17
    }
97
98 1
    public function getClassName(): string
99
    {
100 1
        return $this->className;
101
    }
102
103 1
    public function getDBALColumn(): ?Column
104
    {
105 1
        return $this->column;
106
    }
107
108 1
    public function getFieldMappings(): array
109
    {
110 1
        return $this->fieldMappings;
111
    }
112
113 1
    public function describeOrigin(): string
114
    {
115 1
        return $this->origin;
116
    }
117
118 2
    public function collectDBALColumns(): array
119
    {
120
        /** @var array<Column> $additionalColumns */
121 2
        $additionalColumns = array();
122
123 2
        if ($this->column instanceof Column) {
124 1
            $additionalColumns[] = $this->column;
125
        }
126
127 2
        foreach ($this->fieldMappings as $fieldMapping) {
128
            /** @var MappingInterface $fieldMapping */
129
130 2
            $additionalColumns = array_merge(
131 2
                $additionalColumns,
132 2
                $fieldMapping->collectDBALColumns()
133
            );
134
        }
135
136 2
        return $additionalColumns;
137
    }
138
139 1
    public function getFactory(): ?CallDefinitionInterface
140
    {
141 1
        return $this->factory;
142
    }
143
144 1
    public function getSerializer(): ?CallDefinitionInterface
145
    {
146 1
        return $this->serializer;
147
    }
148
149 1
    public function getId(): ?string
150
    {
151 1
        return $this->id;
152
    }
153
154 1
    public function getReferencedId(): ?string
155
    {
156 1
        return $this->referencedId;
157
    }
158
159 3
    public function resolveValue(
160
        HydrationContextInterface $context,
161
        array $dataFromAdditionalColumns
162
    ) {
163
        /** @var object|null $object */
164 3
        $object = null;
165
166
        # During creation of an object, the class-name is on the top of that creation stack
167 3
        $context->pushOnObjectHydrationStack($this->className);
168
169 3
        $reflectionClass = new ReflectionClass($this->className);
170
171 3
        if (!empty($this->referencedId)) {
172
            $object = $context->getRegisteredValue($this->referencedId);
173
174 3
        } elseif ($this->factory instanceof CallDefinitionInterface) {
175
            /** @var array<string, string> $factoryData */
176 2
            $factoryData = $dataFromAdditionalColumns;
177
178 2
            if ($this->column instanceof Column && !array_key_exists("", $factoryData)) {
179
                /** @var Type $type */
180 1
                $type = $this->column->getType();
181
182
                /** @var string $columnName */
183 1
                $columnName = $this->column->getName();
184
185 1
                if (array_key_exists($columnName, $dataFromAdditionalColumns)) {
186
                    /** @var Connection $connection */
187
                    $connection = $context->getEntityManager()->getConnection();
188
189
                    if (!is_null($dataFromAdditionalColumns[$columnName])) {
190
                        $dataFromAdditionalColumns[$columnName] = $type->convertToPHPValue(
191
                            $dataFromAdditionalColumns[$columnName],
192
                            $connection->getDatabasePlatform()
193
                        );
194
                    }
195
196
                    $factoryData[""] = $dataFromAdditionalColumns[$columnName];
197
                }
198
            }
199
200 2
            $object = $this->factory->execute(
201 2
                $context,
202
                $factoryData
203
            );
204
205
        } else {
206 1
            if ($reflectionClass->isInstantiable()) {
207 1
                $object = $reflectionClass->newInstanceWithoutConstructor();
208
            }
209
        }
210
211 3
        Assert::object($object);
212
213
        // Replace the class-name with the created object on top of the hydration stack
214 3
        $context->popFromObjectHydrationStack();
215 3
        $context->pushOnObjectHydrationStack($object);
216
217 3
        if (!empty($this->id) && !$context->hasRegisteredValue($this->id)) {
218 2
            $context->registerValue($this->id, $object);
219
        }
220
221 3
        foreach ($this->fieldMappings as $fieldName => $fieldMapping) {
222
            /** @var MappingInterface $fieldMapping */
223
224
            /** @var mixed $fieldValue */
225 3
            $fieldValue = $fieldMapping->resolveValue(
226 3
                $context,
227
                $dataFromAdditionalColumns
228
            );
229
230
            /** @var ReflectionClass $propertyReflectionClass */
231 3
            $propertyReflectionClass = $reflectionClass;
232
233 3
            while (is_object($propertyReflectionClass) && !$propertyReflectionClass->hasProperty($fieldName)) {
234
                $propertyReflectionClass = $propertyReflectionClass->getParentClass();
235
            }
236
237 3
            if (!is_object($propertyReflectionClass)) {
238
                throw new ReflectionException(sprintf(
239
                    "Property '%s' does not exist on class '%s' as defined at '%s'",
240
                    $fieldName,
241
                    $reflectionClass->getName(),
0 ignored issues
show
Bug introduced by
Consider using $reflectionClass->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
242
                    $reflectionClass->getFileName()
243
                ));
244
            }
245
246
            /** @var ReflectionProperty $reflectionProperty */
247 3
            $reflectionProperty = $propertyReflectionClass->getProperty($fieldName);
248
249 3
            $reflectionProperty->setAccessible(true);
250 3
            $reflectionProperty->setValue($object, $fieldValue);
251
        }
252
253 3
        $context->popFromObjectHydrationStack();
254
255 3
        return $object;
256
    }
257
258 2
    public function revertValue(
259
        HydrationContextInterface $context,
260
        $valueFromEntityField
261
    ): array {
262
        /** @var array<scalar> $data */
263 2
        $data = array();
264
265 2
        $this->assertValue($context, [], $valueFromEntityField);
266
267 2
        $reflectionClass = new ReflectionClass($this->className);
268
269 2
        $context->pushOnObjectHydrationStack($valueFromEntityField);
270
271 2
        foreach ($this->fieldMappings as $fieldName => $fieldMapping) {
272
            /** @var MappingInterface $fieldMapping */
273
274
            /** @var ReflectionClass $propertyReflectionClass */
275 2
            $propertyReflectionClass = $reflectionClass;
276
277 2
            while (is_object($propertyReflectionClass) && !$propertyReflectionClass->hasProperty($fieldName)) {
278
                $propertyReflectionClass = $propertyReflectionClass->getParentClass();
279
            }
280
281 2
            if (!is_object($propertyReflectionClass)) {
282
                throw new ReflectionException(sprintf(
283
                    "Property %s does not exist in class %s. (Defined %s)",
284
                    $fieldName,
285
                    $this->className,
286
                    $this->origin
287
                ));
288
            }
289
290
            /** @var ReflectionProperty $reflectionProperty */
291 2
            $reflectionProperty = $propertyReflectionClass->getProperty($fieldName);
292
293 2
            $reflectionProperty->setAccessible(true);
294 2
            $valueFromField = null;
295
296 2
            if (is_object($valueFromEntityField)) {
297 1
                $valueFromField = $reflectionProperty->getValue($valueFromEntityField);
298
            }
299
300
            /** @var array<string, mixed> $fieldData */
301 2
            $fieldData = $fieldMapping->revertValue(
302 2
                $context,
303
                $valueFromField
304
            );
305
306 2
            if (array_key_exists("", $fieldData)) {
307
                $fieldData[$fieldName] = $fieldData[""];
308
                unset($fieldData[""]);
309
            }
310
311 2
            $data = array_merge($data, $fieldData);
312
        }
313
314 2
        if ($this->serializer instanceof CallDefinitionInterface) {
315
            /** @var string $columnName */
316
            $columnName = '';
317
318
            if ($this->column instanceof Column) {
319
                $columnName = $this->column->getName();
320
            }
321
322
            $data[$columnName] = $this->serializer->execute(
323
                $context,
324
                array_merge($data, ['' => $valueFromEntityField])
325
            );
326
327
            if ($this->column instanceof Column) {
328
                /** @var Type $type */
329
                $type = $this->column->getType();
330
331
                /** @var Connection $connection */
332
                $connection = $context->getEntityManager()->getConnection();
333
334
                $data[$columnName] = $type->convertToDatabaseValue(
335
                    $data[$columnName],
336
                    $connection->getDatabasePlatform()
337
                );
338
            }
339
        }
340
341 2
        $context->popFromObjectHydrationStack();
342
343 2
        return $data;
344
    }
345
346 3
    public function assertValue(
347
        HydrationContextInterface $context,
348
        array $dataFromAdditionalColumns,
349
        $actualValue
350
    ): void {
351 3
        if (!is_null($actualValue) && !$actualValue instanceof $this->className) {
352 1
            throw FailedRDMAssertionException::expectedInstanceOf(
353 1
                $this->className,
354 1
                is_object($actualValue) ?get_class($actualValue) :gettype($actualValue),
355 1
                $this->origin
356
            );
357
        }
358 2
    }
359
360 1
    public function wakeUpMapping(ContainerInterface $container): void
361
    {
362 1
        if ($this->factory instanceof CallDefinitionInterface) {
363 1
            $this->factory->wakeUpCall($container);
364
        }
365
366 1
        if ($this->serializer instanceof CallDefinitionInterface) {
367 1
            $this->serializer->wakeUpCall($container);
368
        }
369 1
    }
370
371
}
372