Passed
Push — master ( f66ff9...71552f )
by Gerrit
07:43 queued 04:25
created

SimpleSelectDataLoader::hasDataChanged()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 24
c 0
b 0
f 0
ccs 11
cts 11
cp 1
rs 9.2248
cc 5
nc 6
nop 2
crap 5
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 10
    public function __construct(
59
        MappingDriverInterface $mappingDriver,
60
        int $originalDataLimit = 1000
61
    ) {
62 10
        $this->mappingDriver = $mappingDriver;
63 10
        $this->originalDataLimit = $originalDataLimit;
64 10
    }
65
66
    /**
67
     * @param object $entity
68
     */
69 6
    public function loadDBALDataForEntity($entity, EntityManagerInterface $entityManager): array
70
    {
71
        /** @var string $className */
72 6
        $className = get_class($entity);
73
74
        /** @var string $entityObjectHash */
75 6
        $entityObjectHash = spl_object_hash($entity);
76
77 6
        $this->originalData[$entityObjectHash] = [];
78
79
        /** @var array<string> $additionalData */
80 6
        $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 6
            if (class_exists(ClassUtils::class)) {
84 6
                $className = ClassUtils::getRealClass($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 */
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 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);
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 3
                                $additionalData
157
                            );
158
159 3
                            if (count($this->originalData) > $this->originalDataLimit) {
160
                                array_shift($this->originalData);
161
                            }
162
                        }
163
                    }
164
                }
165
            }
166
167 6
            $className = current(class_parents($className));
168 6
        } while (class_exists($className));
169
170 6
        return $this->originalData[$entityObjectHash] ?? [];
171
    }
172
173
    /**
174
     * @param object $entity
175
     */
176 4
    public function storeDBALDataForEntity($entity, EntityManagerInterface $entityManager): void
177
    {
178
        /** @var string $className */
179 4
        $className = get_class($entity);
180
181 4
        if (class_exists(ClassUtils::class)) {
182 4
            $className = ClassUtils::getRealClass($className);
183
        }
184
185
        do {
186
            /** @var null|EntityMappingInterface $entityMapping */
187 4
            $entityMapping = $this->mappingDriver->loadRDMMetadataForClass($className);
188
189 4
            if ($entityMapping instanceof EntityMappingInterface) {
190
                /** @var array<scalar> */
191 4
                $additionalData = $this->collectAdditionalDataForEntity($entity, $entityMapping, $entityManager);
192
193 4
                if ($this->hasDataChanged($entity, $additionalData)) {
194
                    /** @var ClassMetadata $classMetaData */
195 3
                    $classMetaData = $entityManager->getClassMetadata($className);
196
197
                    /** @var array<scalar> $identifier */
198 3
                    $identifier = $this->collectIdentifierForEntity($entity, $entityMapping, $classMetaData);
199
200
                    /** @var string $tableName */
201 3
                    $tableName = $classMetaData->getTableName();
202
203
                    /** @var Connection $connection */
204 3
                    $connection = $entityManager->getConnection();
205
206 3
                    $connection->update($tableName, $additionalData, $identifier);
207
                }
208
            }
209
210 4
            $className = current(class_parents($className));
211 4
        } while (class_exists($className));
212 4
    }
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 4
    private function hasDataChanged($entity, array $additionalData): bool
232
    {
233
        /** @var array<scalar> */
234 4
        $originalData = array();
235
236
        /** @var string $entityObjectHash */
237 4
        $entityObjectHash = spl_object_hash($entity);
238
239 4
        if (isset($this->originalData[$entityObjectHash])) {
240 2
            $originalData = $this->originalData[$entityObjectHash];
241
        }
242
243
        /** @var bool $hasDataChanged */
244 4
        $hasDataChanged = false;
245
246 4
        foreach ($additionalData as $key => $value) {
247 4
            if (!array_key_exists($key, $originalData) || $originalData[$key] != $value) {
248 3
                $hasDataChanged = true;
249 4
                break;
250
            }
251
        }
252
253 4
        return $hasDataChanged;
254
    }
255
256
    /**
257
     * @param object $entity
258
     */
259 4
    private function collectAdditionalDataForEntity(
260
        $entity,
261
        EntityMappingInterface $entityMapping,
262
        EntityManagerInterface $entityManager#,
263
        #string $className = null
264
    ): array {
265
        /** @var array<scalar> */
266 4
        $additionalData = array();
267
268 4
        $reflectionClass = new ReflectionClass($entityMapping->getEntityClassName());
269
270 4
        $context = new HydrationContext($entity, $entityManager);
271
272 4
        foreach ($entityMapping->getFieldMappings() as $fieldName => $entityFieldMapping) {
273
            /** @var MappingInterface $entityFieldMapping */
274
275
            /** @var ReflectionClass $concreteReflectionClass */
276 4
            $concreteReflectionClass = $reflectionClass;
277
278 4
            while (is_object($concreteReflectionClass) && !$concreteReflectionClass->hasProperty($fieldName)) {
279
                $concreteReflectionClass = $concreteReflectionClass->getParentClass();
280
            }
281
282 4
            if (!is_object($concreteReflectionClass)) {
283
                throw new ErrorException(sprintf(
284
                    "Property '%s' does not exist on object of class '%s'!",
285
                    $fieldName,
286
                    $entityMapping->getEntityClassName()
287
                ));
288
            }
289
290
            /** @var ReflectionProperty $reflectionProperty */
291 4
            $reflectionProperty = $concreteReflectionClass->getProperty($fieldName);
292
293 4
            $reflectionProperty->setAccessible(true);
294
295
            /** @var mixed $valueFromEntityField */
296 4
            $valueFromEntityField = $reflectionProperty->getValue($entity);
297
298
            /** @var array<scalar> $fieldAdditionalData */
299 4
            $fieldAdditionalData = $entityFieldMapping->revertValue(
300 4
                $context,
301 4
                $valueFromEntityField
302
            );
303
304 4
            $additionalData = array_merge($additionalData, $fieldAdditionalData);
305
        }
306
307 4
        return $additionalData;
308
    }
309
310
    /**
311
     * @param object $entity
312
     */
313 3
    private function collectIdentifierForEntity(
314
        $entity,
315
        EntityMappingInterface $entityMapping,
316
        ClassMetadata $classMetaData
317
    ): array {
318 3
        $reflectionClass = new ReflectionClass($entityMapping->getEntityClassName());
319
320
        /** @var array<scalar> $identifier */
321 3
        $identifier = array();
322
323 3
        foreach ($classMetaData->identifier as $idFieldName) {
324
            /** @var string $idFieldName */
325
326
            /** @var array $idColumn */
327 3
            $idColumn = $classMetaData->fieldMappings[$idFieldName];
328
329
            /** @var ReflectionProperty $reflectionProperty */
330 3
            $reflectionProperty = $reflectionClass->getProperty($idFieldName);
331
332 3
            $reflectionProperty->setAccessible(true);
333
334 3
            $idValue = $reflectionProperty->getValue($entity);
335
336 3
            $identifier[$idColumn['columnName']] = $idValue;
337
        }
338
339 3
        return $identifier;
340
    }
341
342
}
343