Passed
Push — master ( 064bed...d815ea )
by Gerrit
04:18
created

ObjectMapping   B

Complexity

Total Complexity 46

Size/Duplication

Total Lines 341
Duplicated Lines 0 %

Test Coverage

Coverage 77.42%

Importance

Changes 5
Bugs 3 Features 1
Metric Value
eloc 132
c 5
b 3
f 1
dl 0
loc 341
ccs 96
cts 124
cp 0.7742
rs 8.72
wmc 46

14 Methods

Rating   Name   Duplication   Size   Complexity  
A getFactory() 0 3 1
A getClassName() 0 3 1
B revertValue() 0 87 11
A getId() 0 3 1
A getSerializer() 0 3 1
A describeOrigin() 0 3 1
A getReferencedId() 0 3 1
A getFieldMappings() 0 3 1
A collectDBALColumns() 0 19 3
A assertValue() 0 10 4
C resolveValue() 0 97 15
A wakeUpMapping() 0 8 3
A getDBALColumn() 0 3 1
A __construct() 0 23 2

How to fix   Complexity   

Complex Class

Complex classes like ObjectMapping often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ObjectMapping, and based on these observations, apply Extract Interface, too.

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
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...
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 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...
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
    }
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
                $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
                $context,
202
                $factoryData
203
            );
204
205
        } else {
206 1
            if ($reflectionClass->isInstantiable()) {
207 1
                $object = $reflectionClass->newInstanceWithoutConstructor();
208
            }
209
        }
210
211 3
        if (is_object($object)) {
212
            // Replace the class-name with the created object on top of the hydration stack
213 3
            $context->popFromObjectHydrationStack();
214 3
            $context->pushOnObjectHydrationStack($object);
215
216 3
            if (!empty($this->id) && !$context->hasRegisteredValue($this->id)) {
217 2
                $context->registerValue($this->id, $object);
218
            }
219
220 3
            foreach ($this->fieldMappings as $fieldName => $fieldMapping) {
221
                /** @var MappingInterface $fieldMapping */
222
223
                /** @var mixed $fieldValue */
224 3
                $fieldValue = $fieldMapping->resolveValue(
225
                    $context,
226
                    $dataFromAdditionalColumns
227
                );
228
229
                /** @var ReflectionClass $propertyReflectionClass */
230 3
                $propertyReflectionClass = $reflectionClass;
231
232 3
                while (is_object($propertyReflectionClass) && !$propertyReflectionClass->hasProperty($fieldName)) {
233
                    $propertyReflectionClass = $propertyReflectionClass->getParentClass();
234
                }
235
236 3
                if (!is_object($propertyReflectionClass)) {
237
                    throw new ReflectionException(sprintf(
238
                        "Property '%s' does not exist on class '%s' as defined at '%s'",
239
                        $fieldName,
240
                        $reflectionClass->getName(),
241
                        $reflectionClass->getFileName()
242
                    ));
243
                }
244
245
                /** @var ReflectionProperty $reflectionProperty */
246 3
                $reflectionProperty = $propertyReflectionClass->getProperty($fieldName);
247
248 3
                $reflectionProperty->setAccessible(true);
249 3
                $reflectionProperty->setValue($object, $fieldValue);
250
            }
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 2
            $reflectionProperty->setAccessible(true);
293
294
            /** @var mixed $valueFromField */
295 2
            $valueFromField = null;
296
297 2
            if (is_object($valueFromEntityField) && $reflectionProperty->isInitialized($valueFromEntityField)) {
298 1
                $valueFromField = $reflectionProperty->getValue($valueFromEntityField);
299
            }
300
301
            /** @var array<string, mixed> $fieldData */
302 2
            $fieldData = $fieldMapping->revertValue(
303
                $context,
304
                $valueFromField
305
            );
306
307 2
            if (array_key_exists("", $fieldData)) {
308
                $fieldData[$fieldName] = $fieldData[""];
309
                unset($fieldData[""]);
310
            }
311
312 2
            $data = array_merge($data, $fieldData);
313
        }
314
315 2
        if ($this->serializer instanceof CallDefinitionInterface) {
316
            /** @var string $columnName */
317
            $columnName = '';
318
319
            if ($this->column instanceof Column) {
320
                $columnName = $this->column->getName();
321
            }
322
323
            $data[$columnName] = $this->serializer->execute(
324
                $context,
325
                array_merge($data, ['' => $valueFromEntityField])
326
            );
327
328
            if ($this->column instanceof Column) {
329
                /** @var Type $type */
330
                $type = $this->column->getType();
331
332
                /** @var Connection $connection */
333
                $connection = $context->getEntityManager()->getConnection();
334
335
                $data[$columnName] = $type->convertToDatabaseValue(
336
                    $data[$columnName],
337
                    $connection->getDatabasePlatform()
338
                );
339
            }
340
        }
341
342 2
        $context->popFromObjectHydrationStack();
343
344 2
        return $data;
345
    }
346
347 3
    public function assertValue(
348
        HydrationContextInterface $context,
349
        array $dataFromAdditionalColumns,
350
        $actualValue
351
    ): void {
352 3
        if (!is_null($actualValue) && !$actualValue instanceof $this->className) {
353 1
            throw FailedRDMAssertionException::expectedInstanceOf(
354 1
                $this->className,
355 1
                is_object($actualValue) ?get_class($actualValue) :gettype($actualValue),
356 1
                $this->origin
357
            );
358
        }
359
    }
360
361 1
    public function wakeUpMapping(ContainerInterface $container): void
362
    {
363 1
        if ($this->factory instanceof CallDefinitionInterface) {
364 1
            $this->factory->wakeUpCall($container);
365
        }
366
367 1
        if ($this->serializer instanceof CallDefinitionInterface) {
368 1
            $this->serializer->wakeUpCall($container);
369
        }
370
    }
371
372
}
373