Completed
Push — master ( db2a6e...13df6e )
by Gerrit
09:40
created

collectAdditionalDataForEntity()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 49

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 5.3374

Importance

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