Passed
Push — master ( 356755...14835f )
by Gerrit
04:25
created

SimpleSelectDataLoader   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 278
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 12

Test Coverage

Coverage 97.89%

Importance

Changes 0
Metric Value
wmc 27
lcom 1
cbo 12
dl 0
loc 278
ccs 93
cts 95
cp 0.9789
rs 10
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
C loadDBALDataForEntity() 0 90 11
B storeDBALDataForEntity() 0 33 4
A removeDBALDataForEntity() 0 5 1
A prepareOnMetadataLoad() 0 4 1
B hasDataChanged() 0 23 5
B collectAdditionalDataForEntity() 0 36 2
B collectIdentifierForEntity() 0 28 2
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\ValueResolver\ValueResolverInterface;
29
use Addiks\RDMBundle\Hydration\HydrationContext;
30
31
/**
32
 * A very simple loader that just executes one simple select statement for every entity to load the data for.
33
 *
34
 * Because it executes one query for every entity to load data for, this could (and probably will) have an bad impact on
35
 * performance.
36
 *
37
 * TODO: This may be replaced in the future by integrating that data-loading into the select(s) executed by doctrine.
38
 */
39
final class SimpleSelectDataLoader implements DataLoaderInterface
40
{
41
42
    /**
43
     * @var MappingDriverInterface
44
     */
45
    private $mappingDriver;
46
47
    /**
48
     * @var ValueResolverInterface
49
     */
50
    private $valueResolver;
51
52
    /**
53
     * @var array<array<scalar>>
54
     */
55
    private $originalData = array();
56
57
    /**
58
     * @var int
59
     */
60
    private $originalDataLimit;
61
62 10
    public function __construct(
63
        MappingDriverInterface $mappingDriver,
64
        ValueResolverInterface $valueResolver,
65
        int $originalDataLimit = 1000
66
    ) {
67 10
        $this->mappingDriver = $mappingDriver;
68 10
        $this->valueResolver = $valueResolver;
69 10
        $this->originalDataLimit = $originalDataLimit;
70 10
    }
71
72
    /**
73
     * @param object $entity
74
     */
75 4
    public function loadDBALDataForEntity($entity, EntityManagerInterface $entityManager): array
76
    {
77
        /** @var string $className */
78 4
        $className = get_class($entity);
79
80 4
        if (class_exists(ClassUtils::class)) {
81 4
            $className = ClassUtils::getRealClass($className);
82
        }
83
84
        /** @var array<string> $additionalData */
85 4
        $additionalData = array();
86
87
        /** @var ClassMetadata $classMetaData */
88 4
        $classMetaData = $entityManager->getClassMetadata($className);
89
90
        /** @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...
91 4
        $entityMapping = $this->mappingDriver->loadRDMMetadataForClass($className);
92
93 4
        if ($entityMapping instanceof EntityMappingInterface) {
94
            /** @var array<Column> $additionalColumns */
95 4
            $additionalColumns = $entityMapping->collectDBALColumns();
96
97
            /** @var Connection $connection */
98 4
            $connection = $entityManager->getConnection();
99
100
            /** @var QueryBuilder $queryBuilder */
101 4
            $queryBuilder = $connection->createQueryBuilder();
102
103
            /** @var Expr $expr */
104 4
            $expr = $queryBuilder->expr();
105
106 4
            foreach ($additionalColumns as $column) {
107
                /** @var Column $column */
108
109 4
                $queryBuilder->addSelect($column->getName());
110
            }
111
112 4
            $reflectionClass = new ReflectionClass($className);
113
114
            /** @var bool $hasId */
115 4
            $hasId = false;
116
117 4
            foreach ($classMetaData->identifier as $idFieldName) {
118
                /** @var string $idFieldName */
119
120
                /** @var array $idColumn */
121 3
                $idColumn = $classMetaData->fieldMappings[$idFieldName];
122
123
                /** @var ReflectionProperty $reflectionProperty */
124 3
                $reflectionProperty = $reflectionClass->getProperty($idFieldName);
125
126 3
                $reflectionProperty->setAccessible(true);
127
128 3
                $idValue = $reflectionProperty->getValue($entity);
129
130 3
                if (!empty($idValue)) {
131 3
                    $hasId = true;
132 3
                    if (!is_numeric($idValue) || empty($idValue)) {
133 2
                        $idValue = "'{$idValue}'";
134
                    }
135 3
                    $queryBuilder->andWhere($expr->eq($idColumn['columnName'], $idValue));
136
                }
137
            }
138
139 4
            if ($hasId) {
140 3
                $queryBuilder->from($classMetaData->getTableName());
141 3
                $queryBuilder->setMaxResults(1);
142
143
                /** @var Statement $statement */
144 3
                $statement = $queryBuilder->execute();
145
146 3
                $additionalData = $statement->fetch(PDO::FETCH_ASSOC);
147
148 3
                if (!is_array($additionalData)) {
149
                    $additionalData = array();
150
                }
151
152
                /** @var string $entityObjectHash */
153 3
                $entityObjectHash = spl_object_hash($entity);
154
155 3
                $this->originalData[$entityObjectHash] = $additionalData;
156
157 3
                if (count($this->originalData) > $this->originalDataLimit) {
158
                    array_shift($this->originalData);
159
                }
160
            }
161
        }
162
163 4
        return $additionalData;
164
    }
165
166
    /**
167
     * @param object $entity
168
     */
169 4
    public function storeDBALDataForEntity($entity, EntityManagerInterface $entityManager): void
170
    {
171
        /** @var string $className */
172 4
        $className = get_class($entity);
173
174 4
        if (class_exists(ClassUtils::class)) {
175 4
            $className = ClassUtils::getRealClass($className);
176
        }
177
178
        /** @var null|EntityMappingInterface $entityMapping */
179 4
        $entityMapping = $this->mappingDriver->loadRDMMetadataForClass($className);
180
181 4
        if ($entityMapping instanceof EntityMappingInterface) {
182
            /** @var array<scalar> */
183 4
            $additionalData = $this->collectAdditionalDataForEntity($entity, $entityMapping, $entityManager);
184
185 4
            if ($this->hasDataChanged($entity, $additionalData)) {
186
                /** @var ClassMetadata $classMetaData */
187 3
                $classMetaData = $entityManager->getClassMetadata($className);
188
189
                /** @var array<scalar> $identifier */
190 3
                $identifier = $this->collectIdentifierForEntity($entity, $entityMapping, $classMetaData);
191
192
                /** @var string $tableName */
193 3
                $tableName = $classMetaData->getTableName();
194
195
                /** @var Connection $connection */
196 3
                $connection = $entityManager->getConnection();
197
198 3
                $connection->update($tableName, $additionalData, $identifier);
199
            }
200
        }
201 4
    }
202
203
    /**
204
     * @param object $entity
205
     */
206 1
    public function removeDBALDataForEntity($entity, EntityManagerInterface $entityManager): void
207
    {
208
        # This data-loader does not store data outside the entity-table.
209
        # No additional data need to be removed.
210 1
    }
211
212 5
    public function prepareOnMetadataLoad(EntityManagerInterface $entityManager, ClassMetadata $classMetadata): void
213
    {
214
        # This data-loader does not need any preperation
215 5
    }
216
217
    /**
218
     * @param object $entity
219
     */
220 4
    private function hasDataChanged($entity, array $additionalData): bool
221
    {
222
        /** @var array<scalar> */
223 4
        $originalData = array();
224
225
        /** @var string $entityObjectHash */
226 4
        $entityObjectHash = spl_object_hash($entity);
227
228 4
        if (isset($this->originalData[$entityObjectHash])) {
229 2
            $originalData = $this->originalData[$entityObjectHash];
230
        }
231
232
        /** @var bool $hasDataChanged */
233 4
        $hasDataChanged = false;
234
235 4
        foreach ($additionalData as $key => $value) {
236 4
            if (!array_key_exists($key, $originalData) || $originalData[$key] !== $value) {
237 4
                $hasDataChanged = true;
238
            }
239
        }
240
241 4
        return $hasDataChanged;
242
    }
243
244
    /**
245
     * @param object $entity
246
     */
247 4
    private function collectAdditionalDataForEntity(
248
        $entity,
249
        EntityMappingInterface $entityMapping,
250
        EntityManagerInterface $entityManager
251
    ): array {
252
        /** @var array<scalar> */
253 4
        $additionalData = array();
254
255
        /** @var mixed $reflectionClass */
256 4
        $reflectionClass = new ReflectionClass($entityMapping->getEntityClassName());
257
258 4
        $context = new HydrationContext($entity, $entityManager);
259
260 4
        foreach ($entityMapping->getFieldMappings() as $fieldName => $entityFieldMapping) {
261
            /** @var MappingInterface $entityFieldMapping */
262
263
            /** @var ReflectionProperty $reflectionProperty */
264 4
            $reflectionProperty = $reflectionClass->getProperty($fieldName);
265
266 4
            $reflectionProperty->setAccessible(true);
267
268
            /** @var mixed $valueFromEntityField */
269 4
            $valueFromEntityField = $reflectionProperty->getValue($entity);
270
271
            /** @var array<scalar> $fieldAdditionalData */
272 4
            $fieldAdditionalData = $this->valueResolver->revertValue(
273 4
                $entityFieldMapping,
274 4
                $context,
275 4
                $valueFromEntityField
276
            );
277
278 4
            $additionalData = array_merge($additionalData, $fieldAdditionalData);
279
        }
280
281 4
        return $additionalData;
282
    }
283
284
    /**
285
     * @param object $entity
286
     */
287 3
    private function collectIdentifierForEntity(
288
        $entity,
289
        EntityMappingInterface $entityMapping,
290
        ClassMetadata $classMetaData
291
    ): array {
292 3
        $reflectionClass = new ReflectionClass($entityMapping->getEntityClassName());
293
294
        /** @var array<scalar> $identifier */
295 3
        $identifier = array();
296
297 3
        foreach ($classMetaData->identifier as $idFieldName) {
298
            /** @var string $idFieldName */
299
300
            /** @var array $idColumn */
301 3
            $idColumn = $classMetaData->fieldMappings[$idFieldName];
302
303
            /** @var ReflectionProperty $reflectionProperty */
304 3
            $reflectionProperty = $reflectionClass->getProperty($idFieldName);
305
306 3
            $reflectionProperty->setAccessible(true);
307
308 3
            $idValue = $reflectionProperty->getValue($entity);
309
310 3
            $identifier[$idColumn['columnName']] = $idValue;
311
        }
312
313 3
        return $identifier;
314
    }
315
316
}
317