Test Failed
Push — master ( 86efb6...f66ff9 )
by Gerrit
15:32
created

SimpleSelectDataLoader::hasDataChanged()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5.025

Importance

Changes 0
Metric Value
dl 0
loc 23
ccs 9
cts 10
cp 0.9
rs 9.2408
c 0
b 0
f 0
cc 5
nc 6
nop 2
crap 5.025
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<scalar>>
50
     */
51
    private $originalData = array();
52
53
    /**
54
     * @var int
55
     */
56
    private $originalDataLimit;
57
58 7
    public function __construct(
59
        MappingDriverInterface $mappingDriver,
60
        int $originalDataLimit = 1000
61
    ) {
62 7
        $this->mappingDriver = $mappingDriver;
63 7
        $this->originalDataLimit = $originalDataLimit;
64 7
    }
65
66
    /**
67
     * @param object $entity
68
     */
69 3
    public function loadDBALDataForEntity($entity, EntityManagerInterface $entityManager): array
70
    {
71
        /** @var string $className */
72 3
        $className = get_class($entity);
73
74
        /** @var string $entityObjectHash */
75 3
        $entityObjectHash = spl_object_hash($entity);
76
77 3
        $this->originalData[$entityObjectHash] = [];
78
79
        /** @var array<string> $additionalData */
80 3
        $additionalData = array();
0 ignored issues
show
Unused Code introduced by
$additionalData 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...
81
82
        do {
83 3
            if (class_exists(ClassUtils::class)) {
84 3
                $className = ClassUtils::getRealClass($className);
85
            }
86
87 3
            if (!$entityManager->getMetadataFactory()->isTransient($className)) {
88
                /** @var ClassMetadata $classMetaData */
89
                $classMetaData = $entityManager->getClassMetadata($className);
90
91
                /** @var ?EntityMappingInterface $entityMapping */
0 ignored issues
show
Documentation introduced by
The doc-type ?EntityMappingInterface could not be parsed: Unknown type name "?EntityMappingInterface" 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...
92
                $entityMapping = $this->mappingDriver->loadRDMMetadataForClass($className);
93
94
                if ($entityMapping instanceof EntityMappingInterface) {
95
                    /** @var array<Column> $additionalColumns */
96
                    $additionalColumns = $entityMapping->collectDBALColumns();
97
98
                    if (!empty($additionalColumns)) {
99
                        /** @var Connection $connection */
100
                        $connection = $entityManager->getConnection();
101
102
                        /** @var QueryBuilder $queryBuilder */
103
                        $queryBuilder = $connection->createQueryBuilder();
104
105
                        /** @var Expr $expr */
106
                        $expr = $queryBuilder->expr();
107
108
                        foreach ($additionalColumns as $column) {
109
                            /** @var Column $column */
110
111
                            $queryBuilder->addSelect($column->getName());
112
                        }
113
114
                        $reflectionClass = new ReflectionClass($className);
115
116
                        /** @var bool $hasId */
117
                        $hasId = false;
118
119
                        foreach ($classMetaData->identifier as $idFieldName) {
120
                            /** @var string $idFieldName */
121
122
                            /** @var array $idColumn */
123
                            $idColumn = $classMetaData->fieldMappings[$idFieldName];
124
125
                            /** @var ReflectionProperty $reflectionProperty */
126
                            $reflectionProperty = $reflectionClass->getProperty($idFieldName);
127
128
                            $reflectionProperty->setAccessible(true);
129
130
                            $idValue = $reflectionProperty->getValue($entity);
131
132
                            if (!empty($idValue)) {
133
                                $hasId = true;
134
                                if (!is_numeric($idValue) || empty($idValue)) {
135
                                    $idValue = "'{$idValue}'";
136
                                }
137
                                $queryBuilder->andWhere($expr->eq($idColumn['columnName'], $idValue));
138
                            }
139
                        }
140
141
                        if ($hasId) {
142
                            $queryBuilder->from($classMetaData->getTableName());
143
                            $queryBuilder->setMaxResults(1);
144
145
                            /** @var Statement $statement */
146
                            $statement = $queryBuilder->execute();
147
148
                            $additionalData = $statement->fetch(PDO::FETCH_ASSOC);
149
150
                            if (!is_array($additionalData)) {
151
                                $additionalData = array();
152
                            }
153
154
                            $this->originalData[$entityObjectHash] = array_merge(
155
                                $this->originalData[$entityObjectHash],
156
                                $additionalData
157
                            );
158
159
                            if (count($this->originalData) > $this->originalDataLimit) {
160
                                array_shift($this->originalData);
161
                            }
162
                        }
163
                    }
164
                }
165
            }
166
167
            $className = current(class_parents($className));
168
        } while (class_exists($className));
169
170
        return $this->originalData[$entityObjectHash] ?? [];
171
    }
172
173
    /**
174
     * @param object $entity
175
     */
176 2
    public function storeDBALDataForEntity($entity, EntityManagerInterface $entityManager): void
177
    {
178
        /** @var string $className */
179 2
        $className = get_class($entity);
180
181 2
        if (class_exists(ClassUtils::class)) {
182 2
            $className = ClassUtils::getRealClass($className);
183
        }
184
185
        do {
186
            /** @var null|EntityMappingInterface $entityMapping */
187 2
            $entityMapping = $this->mappingDriver->loadRDMMetadataForClass($className);
188
189 2
            if ($entityMapping instanceof EntityMappingInterface) {
190
                /** @var array<scalar> */
191 2
                $additionalData = $this->collectAdditionalDataForEntity($entity, $entityMapping, $entityManager);
192
193 2
                if ($this->hasDataChanged($entity, $additionalData)) {
194
                    /** @var ClassMetadata $classMetaData */
195 2
                    $classMetaData = $entityManager->getClassMetadata($className);
196
197
                    /** @var array<scalar> $identifier */
198 2
                    $identifier = $this->collectIdentifierForEntity($entity, $entityMapping, $classMetaData);
199
200
                    /** @var string $tableName */
201 2
                    $tableName = $classMetaData->getTableName();
202
203
                    /** @var Connection $connection */
204 2
                    $connection = $entityManager->getConnection();
205
206 2
                    $connection->update($tableName, $additionalData, $identifier);
207
                }
208
            }
209
210 2
            $className = current(class_parents($className));
211 2
        } while (class_exists($className));
212 2
    }
213
214
    /**
215
     * @param object $entity
216
     */
217 1
    public function removeDBALDataForEntity($entity, EntityManagerInterface $entityManager): void
218
    {
219
        # This data-loader does not store data outside the entity-table.
220
        # No additional data need to be removed.
221 1
    }
222
223 5
    public function prepareOnMetadataLoad(EntityManagerInterface $entityManager, ClassMetadata $classMetadata): void
224
    {
225
        # This data-loader does not need any preperation
226 5
    }
227
228
    /**
229
     * @param object $entity
230
     */
231 2
    private function hasDataChanged($entity, array $additionalData): bool
232
    {
233
        /** @var array<scalar> */
234 2
        $originalData = array();
235
236
        /** @var string $entityObjectHash */
237 2
        $entityObjectHash = spl_object_hash($entity);
238
239 2
        if (isset($this->originalData[$entityObjectHash])) {
240
            $originalData = $this->originalData[$entityObjectHash];
241
        }
242
243
        /** @var bool $hasDataChanged */
244 2
        $hasDataChanged = false;
245
246 2
        foreach ($additionalData as $key => $value) {
247 2
            if (!array_key_exists($key, $originalData) || $originalData[$key] != $value) {
248 2
                $hasDataChanged = true;
249
            }
250
        }
251
252 2
        return $hasDataChanged;
253
    }
254
255
    /**
256
     * @param object $entity
257
     */
258 2
    private function collectAdditionalDataForEntity(
259
        $entity,
260
        EntityMappingInterface $entityMapping,
261
        EntityManagerInterface $entityManager#,
262
        #string $className = null
263
    ): array {
264
        /** @var array<scalar> */
265 2
        $additionalData = array();
266
267 2
        $reflectionClass = new ReflectionClass($entityMapping->getEntityClassName());
268
269 2
        $context = new HydrationContext($entity, $entityManager);
270
271 2
        foreach ($entityMapping->getFieldMappings() as $fieldName => $entityFieldMapping) {
272
            /** @var MappingInterface $entityFieldMapping */
273
274
            /** @var ReflectionClass $concreteReflectionClass */
275 2
            $concreteReflectionClass = $reflectionClass;
276
277 2
            while (is_object($concreteReflectionClass) && !$concreteReflectionClass->hasProperty($fieldName)) {
278
                $concreteReflectionClass = $concreteReflectionClass->getParentClass();
279
            }
280
281 2
            if (!is_object($concreteReflectionClass)) {
282
                throw new ErrorException(sprintf(
283
                    "Property '%s' does not exist on object of class '%s'!",
284
                    $fieldName,
285
                    $entityMapping->getEntityClassName()
286
                ));
287
            }
288
289
            /** @var ReflectionProperty $reflectionProperty */
290 2
            $reflectionProperty = $concreteReflectionClass->getProperty($fieldName);
291
292 2
            $reflectionProperty->setAccessible(true);
293
294
            /** @var mixed $valueFromEntityField */
295 2
            $valueFromEntityField = $reflectionProperty->getValue($entity);
296
297
            /** @var array<scalar> $fieldAdditionalData */
298 2
            $fieldAdditionalData = $entityFieldMapping->revertValue(
299 2
                $context,
300 2
                $valueFromEntityField
301
            );
302
303 2
            $additionalData = array_merge($additionalData, $fieldAdditionalData);
304
        }
305
306 2
        return $additionalData;
307
    }
308
309
    /**
310
     * @param object $entity
311
     */
312 2
    private function collectIdentifierForEntity(
313
        $entity,
314
        EntityMappingInterface $entityMapping,
315
        ClassMetadata $classMetaData
316
    ): array {
317 2
        $reflectionClass = new ReflectionClass($entityMapping->getEntityClassName());
318
319
        /** @var array<scalar> $identifier */
320 2
        $identifier = array();
321
322 2
        foreach ($classMetaData->identifier as $idFieldName) {
323
            /** @var string $idFieldName */
324
325
            /** @var array $idColumn */
326 2
            $idColumn = $classMetaData->fieldMappings[$idFieldName];
327
328
            /** @var ReflectionProperty $reflectionProperty */
329 2
            $reflectionProperty = $reflectionClass->getProperty($idFieldName);
330
331 2
            $reflectionProperty->setAccessible(true);
332
333 2
            $idValue = $reflectionProperty->getValue($entity);
334
335 2
            $identifier[$idColumn['columnName']] = $idValue;
336
        }
337
338 2
        return $identifier;
339
    }
340
341
}
342