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

SimpleSelectDataLoader::loadDBALDataForEntity()   C

Complexity

Conditions 13
Paths 12

Size

Total Lines 99
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 42
CRAP Score 13.0021

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 13
eloc 44
c 1
b 0
f 1
nc 12
nop 2
dl 0
loc 99
ccs 42
cts 43
cp 0.9767
crap 13.0021
rs 6.6166

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) 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
 * @license GPL-3.0
8
 * @author Gerrit Addiks <[email protected]>
9
 */
10
11
namespace Addiks\RDMBundle\DataLoader;
12
13
use Addiks\RDMBundle\DataLoader\DataLoaderInterface;
14
use Addiks\RDMBundle\Mapping\Drivers\MappingDriverInterface;
15
use Addiks\RDMBundle\Mapping\EntityMappingInterface;
16
use Doctrine\Common\Util\ClassUtils;
17
use Doctrine\DBAL\Schema\Column;
18
use Doctrine\DBAL\Connection;
19
use Doctrine\DBAL\Query\QueryBuilder;
20
use Doctrine\ORM\EntityManagerInterface;
21
use Doctrine\ORM\Mapping\ClassMetadata;
22
use ReflectionClass;
23
use ReflectionProperty;
24
use Doctrine\ORM\Query\Expr;
25
use Doctrine\DBAL\Driver\Statement;
26
use PDO;
27
use Addiks\RDMBundle\Mapping\MappingInterface;
28
use Addiks\RDMBundle\Hydration\HydrationContext;
29
use Webmozart\Assert\Assert;
30
use ErrorException;
31
32
/**
33
 * A very simple loader that just executes one simple select statement for every entity to load the data for.
34
 *
35
 * Because it executes one query for every entity to load data for, this could (and probably will) have an bad impact on
36
 * performance.
37
 *
38
 * TODO: This may be replaced in the future by integrating that data-loading into the select(s) executed by doctrine.
39
 */
40
final class SimpleSelectDataLoader implements DataLoaderInterface
41
{
42
43
    /**
44
     * @var MappingDriverInterface
45
     */
46
    private $mappingDriver;
47
48
    /**
49
     * @var array<array<string, string>>
50
     */
51
    private $originalData = array();
52
53 10
    public function __construct(
54
        MappingDriverInterface $mappingDriver
55
    ) {
56 10
        $this->mappingDriver = $mappingDriver;
57
    }
58
59
    public function boot(EntityManagerInterface $entityManager): void
60
    {
61
    }
62
63
    /**
64
     * @param object $entity
65
     *
66
     * @return array<string, string>
67
     */
68 6
    public function loadDBALDataForEntity($entity, EntityManagerInterface $entityManager): array
69
    {
70
        /** @var class-string $className */
71 6
        $className = get_class($entity);
72
73
        /** @var string $entityObjectHash */
74 6
        $entityObjectHash = spl_object_hash($entity);
75
76 6
        $this->originalData[$entityObjectHash] = [];
77
78
        /** @var array<string> $additionalData */
79 6
        $additionalData = array();
0 ignored issues
show
Unused Code introduced by
The assignment to $additionalData is dead and can be removed.
Loading history...
80
81
        do {
82 6
            if (class_exists(ClassUtils::class)) {
83 6
                $className = ClassUtils::getRealClass($className);
84 6
                Assert::classExists($className);
85
            }
86
87 6
            if (!$entityManager->getMetadataFactory()->isTransient($className)) {
88
                /** @var ClassMetadata $classMetaData */
89 6
                $classMetaData = $entityManager->getClassMetadata($className);
90
91
                /** @var ?EntityMappingInterface $entityMapping */
92 6
                $entityMapping = $this->mappingDriver->loadRDMMetadataForClass($className);
93
94 6
                if ($entityMapping instanceof EntityMappingInterface) {
95
                    /** @var array<Column> $additionalColumns */
96 6
                    $additionalColumns = $entityMapping->collectDBALColumns();
97
98 6
                    if (!empty($additionalColumns)) {
99
                        /** @var Connection $connection */
100 3
                        $connection = $entityManager->getConnection();
101
102
                        /** @var QueryBuilder $queryBuilder */
103 3
                        $queryBuilder = $connection->createQueryBuilder();
104
105
                        /** @var Expr $expr */
106 3
                        $expr = $queryBuilder->expr();
107
108 3
                        foreach ($additionalColumns as $column) {
109
                            /** @var Column $column */
110
111 3
                            $queryBuilder->addSelect($column->getName());
112
                        }
113
114 3
                        $reflectionClass = new ReflectionClass($className);
115
116
                        /** @var bool $hasId */
117 3
                        $hasId = false;
118
119 3
                        foreach ($classMetaData->identifier as $idFieldName) {
120
                            /** @var string $idFieldName */
121
122
                            /** @var array $idColumn */
123 3
                            $idColumn = $classMetaData->fieldMappings[$idFieldName];
124
125
                            /** @var ReflectionProperty $reflectionProperty */
126 3
                            $reflectionProperty = $reflectionClass->getProperty($idFieldName);
127
128 3
                            $reflectionProperty->setAccessible(true);
129
130 3
                            $idValue = $reflectionProperty->getValue($entity);
131
132 3
                            if (!empty($idValue)) {
133 3
                                $hasId = true;
134 3
                                if (!is_numeric($idValue) || empty($idValue)) {
135 2
                                    $idValue = "'{$idValue}'";
136
                                }
137 3
                                $queryBuilder->andWhere($expr->eq($idColumn['columnName'], $idValue));
138
                            }
139
                        }
140
141 3
                        if ($hasId) {
142 3
                            $queryBuilder->from($classMetaData->getTableName());
143 3
                            $queryBuilder->setMaxResults(1);
144
145
                            /** @var Statement $statement */
146 3
                            $statement = $queryBuilder->execute();
147
148 3
                            $additionalData = $statement->fetch(PDO::FETCH_ASSOC);
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\DBAL\ForwardCompatibility\Result::fetch() has been deprecated: Use fetchNumeric(), fetchAssociative() or fetchOne() instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

148
                            $additionalData = /** @scrutinizer ignore-deprecated */ $statement->fetch(PDO::FETCH_ASSOC);

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

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

Loading history...
149
150 3
                            if (!is_array($additionalData)) {
151
                                $additionalData = array();
152
                            }
153
154 3
                            $this->originalData[$entityObjectHash] = array_merge(
155 3
                                $this->originalData[$entityObjectHash],
156
                                $additionalData
157
                            );
158
                        }
159
                    }
160
                }
161
            }
162
163 6
            $className = current(class_parents($className));
164 6
        } while (class_exists($className));
165
166 6
        return $this->originalData[$entityObjectHash] ?? [];
167
    }
168
169
    /**
170
     * @param object $entity
171
     */
172 4
    public function storeDBALDataForEntity($entity, EntityManagerInterface $entityManager): void
173
    {
174
        /** @var class-string $className */
175 4
        $className = get_class($entity);
176
177 4
        if (class_exists(ClassUtils::class)) {
178 4
            $className = ClassUtils::getRealClass($className);
179 4
            Assert::classExists($className);
180
        }
181
182
        do {
183
            /** @var null|EntityMappingInterface $entityMapping */
184 4
            $entityMapping = $this->mappingDriver->loadRDMMetadataForClass($className);
185
186 4
            if ($entityMapping instanceof EntityMappingInterface) {
187 4
                $context = new HydrationContext($entity, $entityManager);
188
189
                /** @var array<scalar> */
190 4
                $additionalData = $entityMapping->revertValue($context, $entity);
191
192 4
                if ($this->hasDataChanged($entity, $additionalData)) {
193
                    /** @var ClassMetadata $classMetaData */
194 3
                    $classMetaData = $entityManager->getClassMetadata($className);
195
196
                    /** @var array<scalar> $identifier */
197 3
                    $identifier = $this->collectIdentifierForEntity($entity, $entityMapping, $classMetaData);
198
199
                    /** @var string $tableName */
200 3
                    $tableName = $classMetaData->getTableName();
201
202
                    /** @var Connection $connection */
203 3
                    $connection = $entityManager->getConnection();
204
205 3
                    $connection->update($tableName, $additionalData, $identifier);
206
                }
207
            }
208
209 4
            $className = current(class_parents($className));
210 4
        } while (class_exists($className));
211
    }
212
213
    /**
214
     * @param object $entity
215
     */
216
    public function removeDBALDataForEntity($entity, EntityManagerInterface $entityManager): void
217
    {
218
        # This data-loader does not store data outside the entity-table.
219
        # No additional data need to be removed.
220
    }
221
222
    public function prepareOnMetadataLoad(EntityManagerInterface $entityManager, ClassMetadata $classMetadata): void
223
    {
224
        # This data-loader does not need any preparation
225
    }
226
227
    /**
228
     * @param object $entity
229
     */
230 4
    private function hasDataChanged($entity, array $additionalData): bool
231
    {
232
        /** @var array<scalar> */
233 4
        $originalData = array();
234
235
        /** @var string $entityObjectHash */
236 4
        $entityObjectHash = spl_object_hash($entity);
237
238 4
        if (isset($this->originalData[$entityObjectHash])) {
239 2
            $originalData = $this->originalData[$entityObjectHash];
240
        }
241
242
        /** @var bool $hasDataChanged */
243 4
        $hasDataChanged = false;
244
245 4
        foreach ($additionalData as $key => $value) {
246 3
            if (!array_key_exists($key, $originalData) || $originalData[$key] != $value) {
247 3
                $hasDataChanged = true;
248 3
                break;
249
            }
250
        }
251
252 4
        return $hasDataChanged;
253
    }
254
255
    /**
256
     * @param object $entity
257
     */
258 3
    private function collectIdentifierForEntity(
259
        $entity,
260
        EntityMappingInterface $entityMapping,
261
        ClassMetadata $classMetaData
262
    ): array {
263 3
        $reflectionClass = new ReflectionClass($entityMapping->getEntityClassName());
264
265
        /** @var array<scalar> $identifier */
266 3
        $identifier = array();
267
268 3
        foreach ($classMetaData->identifier as $idFieldName) {
269
            /** @var string $idFieldName */
270
271
            /** @var array $idColumn */
272 3
            $idColumn = $classMetaData->fieldMappings[$idFieldName];
273
274
            /** @var ReflectionProperty $reflectionProperty */
275 3
            $reflectionProperty = $reflectionClass->getProperty($idFieldName);
276
277 3
            $reflectionProperty->setAccessible(true);
278
279 3
            $idValue = $reflectionProperty->getValue($entity);
280
281 3
            $identifier[$idColumn['columnName']] = $idValue;
282
        }
283
284 3
        return $identifier;
285
    }
286
287
}
288