Passed
Push — master ( 87f8ce...93d3ce )
by Gerrit
02:43
created

onColumnValueRequestedFromEntity()   C

Complexity

Conditions 10
Paths 12

Size

Total Lines 66

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 110

Importance

Changes 0
Metric Value
dl 0
loc 66
ccs 0
cts 29
cp 0
rs 6.8751
c 0
b 0
f 0
cc 10
nc 12
nop 3
crap 110

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Copyright (C) 2019 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\DataLoader\BlackMagic;
14
15
use Addiks\RDMBundle\DataLoader\DataLoaderInterface;
16
use Doctrine\ORM\EntityManagerInterface;
17
use Doctrine\ORM\Mapping\ClassMetadata;
18
use Doctrine\ORM\UnitOfWork;
19
use Doctrine\DBAL\Schema\Column;
20
use Addiks\RDMBundle\Mapping\Drivers\MappingDriverInterface;
21
use ReflectionObject;
22
use Addiks\RDMBundle\Mapping\EntityMappingInterface;
23
use ReflectionProperty;
24
use Webmozart\Assert\Assert;
25
use ReflectionClass;
26
use Addiks\RDMBundle\DataLoader\BlackMagic\BlackMagicColumnReflectionPropertyMock;
27
use Addiks\RDMBundle\Hydration\HydrationContext;
28
use Doctrine\DBAL\Types\Type;
29
use Doctrine\Common\Persistence\Mapping\ClassMetadataFactory;
30
use Doctrine\Common\Persistence\Mapping\AbstractClassMetadataFactory;
31
use Addiks\RDMBundle\DataLoader\BlackMagic\BlackMagicReflectionServiceDecorator;
32
use Doctrine\Common\Util\ClassUtils;
33
use Doctrine\DBAL\Platforms\AbstractPlatform;
34
use Doctrine\Persistence\Mapping\RuntimeReflectionService;
35
use Doctrine\Persistence\Mapping\ReflectionService;
36
37
/**
38
 * This data-loader works by injecting fake doctrine columns into the doctrine class-metadata instance(s), where the injected
39
 * Reflection* objects are replaced by custom mock objects that give the raw DB data from doctrine to this data-loader.
40
 * From doctrine's point of view, every database-column looks like an actual property on the entity, even if that property does
41
 * not actually exist.
42
 *
43
 * ... In other words: BLACK MAGIC!!! *woooo*
44
 *
45
 *  #####################################################################################
46
 *  ### WARNING: Be aware that this data-loader is considered EXPERIMENTAL!           ###
47
 *  ###          If you use this data-loader and bad things happen, it is YOUR FAULT! ###
48
 *  #####################################################################################
49
 *
50
 */
51
class BlackMagicDataLoader implements DataLoaderInterface
52
{
53
54
    /** @var MappingDriverInterface */
55
    private $mappingDriver;
56
57
    /** @var array<string, mixed>|null */
58
    private $entityDataCached;
59
60
    /** @var array<string, Column>|null */
61
    private $dbalColumnsCached;
62
63
    /** @var object|null */
64
    private $entityDataCacheSource;
65
66 1
    public function __construct(MappingDriverInterface $mappingDriver)
67
    {
68 1
        $this->mappingDriver = $mappingDriver;
69 1
    }
70
71
    public function loadDBALDataForEntity($entity, EntityManagerInterface $entityManager): array
72
    {
73
        /** @var array<string, string> $dbalData */
74
        $dbalData = array();
75
76
        /** @var 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...
77
        $className = get_class($entity);
78
79
        if (class_exists(ClassUtils::class)) {
80
            $className = ClassUtils::getRealClass($className);
81
            Assert::classExists($className);
82
        }
83
84
        /** @var ClassMetadata $classMetaData */
85
        $classMetaData = $entityManager->getClassMetadata($className);
0 ignored issues
show
Unused Code introduced by
$classMetaData 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...
86
87
        /** @var EntityMappingInterface|null $entityMapping */
88
        $entityMapping = $this->mappingDriver->loadRDMMetadataForClass($className);
89
90
        if ($entityMapping instanceof EntityMappingInterface) {
91
            /** @var array<Column> $columns */
92
            $columns = $entityMapping->collectDBALColumns();
93
94
            if (!empty($columns)) {
95
                /** @var UnitOfWork $unitOfWork */
96
                $unitOfWork = $entityManager->getUnitOfWork();
97
98
                /** @var array<string, mixed> $originalEntityData */
99
                $originalEntityData = $unitOfWork->getOriginalEntityData($entity);
100
101
                /** @var Column $column */
102
                foreach ($columns as $column) {
103
                    /** @var string $columnName */
104
                    $columnName = $column->getName();
105
106
                    /** @var string $fieldName */
107
                    $fieldName = $this->columnToFieldName($column);
108
109
                    if (array_key_exists($fieldName, $originalEntityData)) {
110
                        $dbalData[$columnName] = $originalEntityData[$fieldName];
111
112
                    } elseif (array_key_exists($columnName, $originalEntityData)) {
113
                        $dbalData[$columnName] = $originalEntityData[$columnName];
114
                    }
115
                }
116
            }
117
        }
118
119
        return $dbalData;
120
    }
121
122 1
    public function storeDBALDataForEntity($entity, EntityManagerInterface $entityManager)
123
    {
124
        # This happens after doctrine has already UPDATE'd the row itself, do nothing here.
125 1
    }
126
127
    public function removeDBALDataForEntity($entity, EntityManagerInterface $entityManager)
128
    {
129
        # Doctrine DELETE's the row for us, we dont need to do anything here.
130
    }
131
132 1
    public function prepareOnMetadataLoad(EntityManagerInterface $entityManager, ClassMetadata $classMetadata)
133
    {
134
        /** @var ClassMetadataFactory $metadataFactory */
135 1
        $metadataFactory = $entityManager->getMetadataFactory();
136
137 1
        if ($metadataFactory instanceof AbstractClassMetadataFactory) {
138
            /** @var ReflectionService|null $reflectionService */
139 1
            $reflectionService = $metadataFactory->getReflectionService();
140
141 1
            if (!$reflectionService instanceof BlackMagicReflectionServiceDecorator) {
142 1
                $reflectionService = new BlackMagicReflectionServiceDecorator(
143 1
                    $reflectionService ?? new RuntimeReflectionService(),
144 1
                    $this->mappingDriver,
145
                    $entityManager,
146
                    $this
147
                );
148
149 1
                $metadataFactory->setReflectionService($reflectionService);
150
            }
151
        }
152
153
        /** @var 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...
154 1
        $className = $classMetadata->getName();
155
156
        /** @var EntityMappingInterface|null $entityMapping */
157 1
        $entityMapping = $this->mappingDriver->loadRDMMetadataForClass($className);
158
159 1
        if ($entityMapping instanceof EntityMappingInterface) {
160
            /** @var array<Column> $dbalColumns */
161 1
            $dbalColumns = $entityMapping->collectDBALColumns();
162
163
            /** @var Column $column */
164 1
            foreach ($dbalColumns as $column) {
165
                /** @var string $columnName */
166 1
                $columnName = $column->getName();
167
168
                /** @var string $fieldName */
169 1
                $fieldName = $this->columnToFieldName($column);
170
171 1
                if (!isset($classMetadata->reflFields[$fieldName])) {
172 1
                    $classMetadata->reflFields[$fieldName] = new BlackMagicColumnReflectionPropertyMock(
173 1
                        $entityManager,
174
                        $classMetadata,
175
                        $column,
176
                        $fieldName,
177
                        $this
178
                    );
179
                }
180
181 1
                if (!isset($classMetadata->fieldMappings[$fieldName])) {
182
                    /** @var array<string, mixed> $mapping */
183 1
                    $mapping = array_merge(
184 1
                        $column->toArray(),
185
                        [
186 1
                            'columnName' => $columnName,
187 1
                            'fieldName' => $fieldName,
188 1
                            'nullable' => !$column->getNotnull(),
189
                        ]
190
                    );
191
192 1
                    if (isset($mapping['type']) && $mapping['type'] instanceof Type) {
193 1
                        $mapping['type'] = $mapping['type']->getName();
194
                    }
195
196
                    #$classMetadata->mapField($mapping);
197 1
                    $classMetadata->fieldMappings[$fieldName] = $mapping;
198
                }
199
200
                /** @psalm-suppress DeprecatedProperty */
201 1
                if (isset ($classMetadata->fieldNames) && !isset($classMetadata->fieldNames[$columnName])) {
202 1
                    $classMetadata->fieldNames[$columnName] = $fieldName;
203
                }
204
205
                /** @psalm-suppress DeprecatedProperty */
206 1
                if (isset ($classMetadata->columnNames) && !isset($classMetadata->columnNames[$fieldName])) {
0 ignored issues
show
Deprecated Code introduced by
The property Doctrine\ORM\Mapping\Cla...adataInfo::$columnNames has been deprecated with message: 3.0 Remove this.

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
207 1
                    $classMetadata->columnNames[$fieldName] = $columnName;
0 ignored issues
show
Deprecated Code introduced by
The property Doctrine\ORM\Mapping\Cla...adataInfo::$columnNames has been deprecated with message: 3.0 Remove this.

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
208
                }
209
            }
210
        }
211 1
    }
212
213
    public function onColumnValueSetOnEntity(
214
        EntityManagerInterface $entityManager,
0 ignored issues
show
Unused Code introduced by
The parameter $entityManager is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
215
        ?object $entity,
0 ignored issues
show
Unused Code introduced by
The parameter $entity is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
216
        string $columnName,
0 ignored issues
show
Unused Code introduced by
The parameter $columnName is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
217
        $value = null
0 ignored issues
show
Unused Code introduced by
The parameter $value is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
218
    ): void {
219
        # Do nothing here, we first let doctrine collect all the data and the use that in "loadDBALDataForEntity" above.
220
    }
221
222
    public function onColumnValueRequestedFromEntity(
223
        EntityManagerInterface $entityManager,
224
        $entity,
225
        string $columnName
226
    ) {
227
        /** @var array<string, mixed> $entityData */
228
        $entityData = array();
229
230
        /** @var array<string, Column> $dbalColumns */
231
        $dbalColumns = array();
232
233
        if (is_object($entity)) {
234
            if ($entity === $this->entityDataCacheSource
235
            && is_array($this->entityDataCached)
236
            && array_key_exists($columnName, $this->entityDataCached)) {
237
                # This caching mechanism stores only the data of the current entity
238
                # and relies on doctrine only reading one entity at a time.
239
240
                $entityData = $this->entityDataCached;
241
                $dbalColumns = $this->dbalColumnsCached;
242
243
                unset($this->entityDataCached[$columnName]);
244
245
            } else {
246
                /** @var 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...
247
                $className = get_class($entity);
248
249
                if (class_exists(ClassUtils::class)) {
250
                    $className = ClassUtils::getRealClass($className);
251
                    Assert::classExists($className);
252
                }
253
254
                /** @var EntityMappingInterface|null $entityMapping */
255
                $entityMapping = $this->mappingDriver->loadRDMMetadataForClass($className);
256
257
                if ($entityMapping instanceof EntityMappingInterface) {
258
                    $context = new HydrationContext($entity, $entityManager);
259
260
                    $entityData = $entityMapping->revertValue($context, $entity);
261
262
                    /** @var Column $column */
263
                    foreach ($entityMapping->collectDBALColumns() as $column) {
264
                        $dbalColumns[$column->getName()] = $column;
265
                    }
266
                }
267
268
                $this->entityDataCached = $entityData;
269
                $this->entityDataCacheSource = $entity;
270
                $this->dbalColumnsCached = $dbalColumns;
271
            }
272
        }
273
274
        $value = $entityData[$columnName] ?? null;
275
276
        if (!is_null($value) && isset($dbalColumns[$columnName])) {
277
            /** @var AbstractPlatform $platform */
278
            $platform = $entityManager->getConnection()->getDatabasePlatform();
279
280
            /** @var Column $column */
281
            $column = $dbalColumns[$columnName];
282
283
            $value = $column->getType()->convertToPHPValue($value, $platform);
284
        }
285
286
        return $value;
287
    }
288
289 1
    public function columnToFieldName(Column $column): string
290
    {
291
        /** @var string $columnName */
292 1
        $columnName = $column->getName();
293
294
        /** @var string $fieldName */
295 1
        $fieldName = '__COLUMN__' . $columnName;
296
297 1
        return $fieldName;
298
    }
299
300
}
301