Passed
Push — master ( eec92d...87f8ce )
by Gerrit
02:46
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 (isset($originalEntityData[$fieldName])) {
110
                        $dbalData[$columnName] = $originalEntityData[$fieldName];
111
                    }
112
                }
113
            }
114
        }
115
116
        return $dbalData;
117
    }
118
119 1
    public function storeDBALDataForEntity($entity, EntityManagerInterface $entityManager)
120
    {
121
        # This happens after doctrine has already UPDATE'd the row itself, do nothing here.
122 1
    }
123
124
    public function removeDBALDataForEntity($entity, EntityManagerInterface $entityManager)
125
    {
126
        # Doctrine DELETE's the row for us, we dont need to do anything here.
127
    }
128
129 1
    public function prepareOnMetadataLoad(EntityManagerInterface $entityManager, ClassMetadata $classMetadata)
130
    {
131
        /** @var ClassMetadataFactory $metadataFactory */
132 1
        $metadataFactory = $entityManager->getMetadataFactory();
133
134 1
        if ($metadataFactory instanceof AbstractClassMetadataFactory) {
135
            /** @var ReflectionService|null $reflectionService */
136 1
            $reflectionService = $metadataFactory->getReflectionService();
137
138 1
            if (!$reflectionService instanceof BlackMagicReflectionServiceDecorator) {
139 1
                $reflectionService = new BlackMagicReflectionServiceDecorator(
140 1
                    $reflectionService ?? new RuntimeReflectionService(),
141 1
                    $this->mappingDriver,
142
                    $entityManager,
143
                    $this
144
                );
145
146 1
                $metadataFactory->setReflectionService($reflectionService);
147
            }
148
        }
149
150
        /** @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...
151 1
        $className = $classMetadata->getName();
152
153
        /** @var EntityMappingInterface|null $entityMapping */
154 1
        $entityMapping = $this->mappingDriver->loadRDMMetadataForClass($className);
155
156 1
        if ($entityMapping instanceof EntityMappingInterface) {
157
            /** @var array<Column> $dbalColumns */
158 1
            $dbalColumns = $entityMapping->collectDBALColumns();
159
160
            /** @var Column $column */
161 1
            foreach ($dbalColumns as $column) {
162
                /** @var string $columnName */
163 1
                $columnName = $column->getName();
164
165
                /** @var string $fieldName */
166 1
                $fieldName = $this->columnToFieldName($column);
167
168 1
                if (!isset($classMetadata->reflFields[$fieldName])) {
169 1
                    $classMetadata->reflFields[$fieldName] = new BlackMagicColumnReflectionPropertyMock(
170 1
                        $entityManager,
171
                        $classMetadata,
172
                        $column,
173
                        $fieldName,
174
                        $this
175
                    );
176
                }
177
178 1
                if (!isset($classMetadata->fieldMappings[$fieldName])) {
179
                    /** @var array<string, mixed> $mapping */
180 1
                    $mapping = array_merge(
181 1
                        $column->toArray(),
182
                        [
183 1
                            'columnName' => $columnName,
184 1
                            'fieldName' => $fieldName,
185 1
                            'nullable' => !$column->getNotnull(),
186
                        ]
187
                    );
188
189 1
                    if (isset($mapping['type']) && $mapping['type'] instanceof Type) {
190 1
                        $mapping['type'] = $mapping['type']->getName();
191
                    }
192
193
                    #$classMetadata->mapField($mapping);
194 1
                    $classMetadata->fieldMappings[$fieldName] = $mapping;
195
                }
196
197
                /** @psalm-suppress DeprecatedProperty */
198 1
                if (isset ($classMetadata->fieldNames) && !isset($classMetadata->fieldNames[$columnName])) {
199 1
                    $classMetadata->fieldNames[$columnName] = $fieldName;
200
                }
201
202
                /** @psalm-suppress DeprecatedProperty */
203 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...
204 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...
205
                }
206
            }
207
        }
208 1
    }
209
210
    public function onColumnValueSetOnEntity(
211
        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...
212
        ?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...
213
        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...
214
        $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...
215
    ): void {
216
        # Do nothing here, we first let doctrine collect all the data and the use that in "loadDBALDataForEntity" above.
217
    }
218
219
    public function onColumnValueRequestedFromEntity(
220
        EntityManagerInterface $entityManager,
221
        $entity,
222
        string $columnName
223
    ) {
224
        /** @var array<string, mixed> $entityData */
225
        $entityData = array();
226
227
        /** @var array<string, Column> $dbalColumns */
228
        $dbalColumns = array();
229
230
        if (is_object($entity)) {
231
            if ($entity === $this->entityDataCacheSource
232
            && is_array($this->entityDataCached)
233
            && array_key_exists($columnName, $this->entityDataCached)) {
234
                # This caching mechanism stores only the data of the current entity
235
                # and relies on doctrine only reading one entity at a time.
236
237
                $entityData = $this->entityDataCached;
238
                $dbalColumns = $this->dbalColumnsCached;
239
240
                unset($this->entityDataCached[$columnName]);
241
242
            } else {
243
                /** @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...
244
                $className = get_class($entity);
245
246
                if (class_exists(ClassUtils::class)) {
247
                    $className = ClassUtils::getRealClass($className);
248
                    Assert::classExists($className);
249
                }
250
251
                /** @var EntityMappingInterface|null $entityMapping */
252
                $entityMapping = $this->mappingDriver->loadRDMMetadataForClass($className);
253
254
                if ($entityMapping instanceof EntityMappingInterface) {
255
                    $context = new HydrationContext($entity, $entityManager);
256
257
                    $entityData = $entityMapping->revertValue($context, $entity);
258
259
                    /** @var Column $column */
260
                    foreach ($entityMapping->collectDBALColumns() as $column) {
261
                        $dbalColumns[$column->getName()] = $column;
262
                    }
263
                }
264
265
                $this->entityDataCached = $entityData;
266
                $this->entityDataCacheSource = $entity;
267
                $this->dbalColumnsCached = $dbalColumns;
268
            }
269
        }
270
271
        $value = $entityData[$columnName] ?? null;
272
273
        if (!is_null($value) && isset($dbalColumns[$columnName])) {
274
            /** @var AbstractPlatform $platform */
275
            $platform = $entityManager->getConnection()->getDatabasePlatform();
276
277
            /** @var Column $column */
278
            $column = $dbalColumns[$columnName];
279
280
            $value = $column->getType()->convertToPHPValue($value, $platform);
281
        }
282
283
        return $value;
284
    }
285
286 1
    public function columnToFieldName(Column $column): string
287
    {
288
        /** @var string $columnName */
289 1
        $columnName = $column->getName();
290
291
        /** @var string $fieldName */
292 1
        $fieldName = '__COLUMN__' . $columnName;
293
294 1
        return $fieldName;
295
    }
296
297
}
298