Completed
Push — master ( db2a6e...13df6e )
by Gerrit
09:40
created

ObjectMapping   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 342
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Test Coverage

Coverage 76.81%

Importance

Changes 0
Metric Value
wmc 43
lcom 1
cbo 9
dl 0
loc 342
ccs 106
cts 138
cp 0.7681
rs 8.96
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 27 2
A getClassName() 0 4 1
A getDBALColumn() 0 4 1
A getFieldMappings() 0 4 1
A describeOrigin() 0 4 1
A collectDBALColumns() 0 20 3
A getFactory() 0 4 1
A getSerializer() 0 4 1
A getId() 0 4 1
A getReferencedId() 0 4 1
C resolveValue() 0 95 13
C revertValue() 0 87 10
A assertValue() 0 13 4
A wakeUpMapping() 0 10 3

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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 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 17
    public function __construct(
72
        string $className,
73
        array $fieldMappings,
74
        Column $column = null,
75
        string $origin = "undefined",
76
        CallDefinitionInterface $factory = null,
77
        CallDefinitionInterface $serializer = null,
78
        string $id = null,
79
        string $referencedId = null
80
    ) {
81 17
        $this->className = $className;
82 17
        $this->factory = $factory;
83 17
        $this->column = $column;
84 17
        $this->serializer = $serializer;
85 17
        $this->origin = $origin;
86 17
        $this->id = $id;
87 17
        $this->referencedId = $referencedId;
88
89 17
        foreach ($fieldMappings as $fieldName => $fieldMapping) {
90
            /** @var MappingInterface $fieldMapping */
91
92 17
            Assert::isInstanceOf($fieldMapping, MappingInterface::class);
93 17
            Assert::false(is_numeric($fieldName));
94
95 17
            $this->fieldMappings[$fieldName] = $fieldMapping;
96
        }
97 17
    }
98
99 1
    public function getClassName(): string
100
    {
101 1
        return $this->className;
102
    }
103
104 1
    public function getDBALColumn(): ?Column
105
    {
106 1
        return $this->column;
107
    }
108
109 1
    public function getFieldMappings(): array
110
    {
111 1
        return $this->fieldMappings;
112
    }
113
114 1
    public function describeOrigin(): string
115
    {
116 1
        return $this->origin;
117
    }
118
119 2
    public function collectDBALColumns(): array
120
    {
121
        /** @var array<Column> $additionalColumns */
122 2
        $additionalColumns = array();
123
124 2
        if ($this->column instanceof Column) {
125 1
            $additionalColumns[] = $this->column;
126
        }
127
128 2
        foreach ($this->fieldMappings as $fieldMapping) {
129
            /** @var MappingInterface $fieldMapping */
130
131 2
            $additionalColumns = array_merge(
132 2
                $additionalColumns,
133 2
                $fieldMapping->collectDBALColumns()
134
            );
135
        }
136
137 2
        return $additionalColumns;
138
    }
139
140 1
    public function getFactory(): ?CallDefinitionInterface
141
    {
142 1
        return $this->factory;
143
    }
144
145 1
    public function getSerializer(): ?CallDefinitionInterface
146
    {
147 1
        return $this->serializer;
148
    }
149
150 1
    public function getId(): ?string
151
    {
152 1
        return $this->id;
153
    }
154
155 1
    public function getReferencedId(): ?string
156
    {
157 1
        return $this->referencedId;
158
    }
159
160 3
    public function resolveValue(
161
        HydrationContextInterface $context,
162
        array $dataFromAdditionalColumns
163
    ) {
164
        /** @var mixed $object */
165 3
        $object = null;
0 ignored issues
show
Unused Code introduced by
$object is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
166
167 3
        $reflectionClass = new ReflectionClass($this->className);
168
169
        /** @var object|string $object */
170 3
        $object = $this->className;
171
172 3
        if ($reflectionClass->isInstantiable()) {
173 3
            $object = $reflectionClass->newInstanceWithoutConstructor();
174
        }
175
176 3
        $context->pushOnObjectHydrationStack($object);
177
178 3
        if (!empty($this->referencedId)) {
179
            $object = $context->getRegisteredValue($this->referencedId);
180
181 3
        } elseif ($this->factory instanceof CallDefinitionInterface) {
182
            /** @var array<string, string> $factoryData */
183 2
            $factoryData = $dataFromAdditionalColumns;
184
185 2
            if ($this->column instanceof Column && !array_key_exists("", $factoryData)) {
186
                /** @var Type $type */
187 1
                $type = $this->column->getType();
188
189
                /** @var string $columnName */
190 1
                $columnName = $this->column->getName();
191
192 1
                if (isset($dataFromAdditionalColumns[$columnName])) {
193
                    /** @var Connection $connection */
194
                    $connection = $context->getEntityManager()->getConnection();
195
196
                    $dataFromAdditionalColumns[$columnName] = $type->convertToPHPValue(
197
                        $dataFromAdditionalColumns[$columnName],
198
                        $connection->getDatabasePlatform()
199
                    );
200
201
                    $factoryData[""] = $dataFromAdditionalColumns[$columnName];
202
                }
203
            }
204
205 2
            $object = $this->factory->execute(
206 2
                $context,
207 2
                $factoryData
208
            );
209
        }
210
211
        // $object may have been replaced during creation, re-assign on top of stack.
212 3
        $context->popFromObjectHydrationStack();
213 3
        $context->pushOnObjectHydrationStack($object);
214
215 3
        if (!empty($this->id) && !$context->hasRegisteredValue($this->id)) {
216 2
            $context->registerValue($this->id, $object);
217
        }
218
219 3
        foreach ($this->fieldMappings as $fieldName => $fieldMapping) {
220
            /** @var MappingInterface $fieldMapping */
221
222
            /** @var mixed $fieldValue */
223 3
            $fieldValue = $fieldMapping->resolveValue(
224 3
                $context,
225 3
                $dataFromAdditionalColumns
226
            );
227
228
            /** @var ReflectionClass $propertyReflectionClass */
229 3
            $propertyReflectionClass = $reflectionClass;
230
231 3
            while (is_object($propertyReflectionClass) && !$propertyReflectionClass->hasProperty($fieldName)) {
232
                $propertyReflectionClass = $propertyReflectionClass->getParentClass();
233
            }
234
235 3
            if (!is_object($propertyReflectionClass)) {
236
                throw new ReflectionException(sprintf(
237
                    "Property '%s' does not exist on class '%s' as defined at '%s'",
238
                    $fieldName,
239
                    $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...
240
                    $reflectionClass->getFileName()
241
                ));
242
            }
243
244
            /** @var ReflectionProperty $reflectionProperty */
245 3
            $reflectionProperty = $propertyReflectionClass->getProperty($fieldName);
246
247 3
            $reflectionProperty->setAccessible(true);
248 3
            $reflectionProperty->setValue($object, $fieldValue);
249
        }
250
251 3
        $context->popFromObjectHydrationStack();
252
253 3
        return $object;
254
    }
255
256 2
    public function revertValue(
257
        HydrationContextInterface $context,
258
        $valueFromEntityField
259
    ): array {
260
        /** @var array<scalar> $data */
261 2
        $data = array();
262
263 2
        $this->assertValue($context, [], $valueFromEntityField);
264
265 2
        $reflectionClass = new ReflectionClass($this->className);
266
267 2
        $context->pushOnObjectHydrationStack($valueFromEntityField);
268
269 2
        foreach ($this->fieldMappings as $fieldName => $fieldMapping) {
270
            /** @var MappingInterface $fieldMapping */
271
272
            /** @var ReflectionClass $propertyReflectionClass */
273 2
            $propertyReflectionClass = $reflectionClass;
274
275 2
            while (is_object($propertyReflectionClass) && !$propertyReflectionClass->hasProperty($fieldName)) {
276
                $propertyReflectionClass = $propertyReflectionClass->getParentClass();
277
            }
278
279 2
            if (!is_object($propertyReflectionClass)) {
280
                throw new ReflectionException(sprintf(
281
                    "Property %s does not exist in class %s. (Defined %s)",
282
                    $fieldName,
283
                    get_class($valueFromEntityField),
284
                    $this->origin
285
                ));
286
            }
287
288
            /** @var ReflectionProperty $reflectionProperty */
289 2
            $reflectionProperty = $propertyReflectionClass->getProperty($fieldName);
290
291 2
            $reflectionProperty->setAccessible(true);
292 2
            $valueFromField = null;
293
294 2
            if (is_object($valueFromEntityField)) {
295 1
                $valueFromField = $reflectionProperty->getValue($valueFromEntityField);
296
            }
297
298
            /** @var array<string, mixed> $fieldData */
299 2
            $fieldData = $fieldMapping->revertValue(
300 2
                $context,
301 2
                $valueFromField
302
            );
303
304 2
            if (array_key_exists("", $fieldData)) {
305
                $fieldData[$fieldName] = $fieldData[""];
306
                unset($fieldData[""]);
307
            }
308
309 2
            $data = array_merge($data, $fieldData);
310
        }
311
312 2
        if ($this->serializer instanceof CallDefinitionInterface) {
313
            /** @var string $columnName */
314
            $columnName = '';
315
316
            if ($this->column instanceof Column) {
317
                $columnName = $this->column->getName();
318
            }
319
320
            $data[$columnName] = $this->serializer->execute(
321
                $context,
322
                array_merge($data, ['' => $valueFromEntityField])
323
            );
324
325
            if ($this->column instanceof Column) {
326
                /** @var Type $type */
327
                $type = $this->column->getType();
328
329
                /** @var Connection $connection */
330
                $connection = $context->getEntityManager()->getConnection();
331
332
                $data[$columnName] = $type->convertToDatabaseValue(
333
                    $data[$columnName],
334
                    $connection->getDatabasePlatform()
335
                );
336
            }
337
        }
338
339 2
        $context->popFromObjectHydrationStack();
340
341 2
        return $data;
342
    }
343
344 3
    public function assertValue(
345
        HydrationContextInterface $context,
346
        array $dataFromAdditionalColumns,
347
        $actualValue
348
    ): void {
349 3
        if (!is_null($actualValue) && !$actualValue instanceof $this->className) {
350 1
            throw FailedRDMAssertionException::expectedInstanceOf(
351 1
                $this->className,
352 1
                is_object($actualValue) ?get_class($actualValue) :gettype($actualValue),
353 1
                $this->origin
354
            );
355
        }
356 2
    }
357
358 1
    public function wakeUpMapping(ContainerInterface $container): void
359
    {
360 1
        if ($this->factory instanceof CallDefinitionInterface) {
361 1
            $this->factory->wakeUpCall($container);
362
        }
363
364 1
        if ($this->serializer instanceof CallDefinitionInterface) {
365 1
            $this->serializer->wakeUpCall($container);
366
        }
367 1
    }
368
369
}
370