Completed
Push — master ( 77cc29...840912 )
by Gerrit
10:19
created

SimpleSelectDataLoader   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 270
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Test Coverage

Coverage 97.85%

Importance

Changes 0
Metric Value
wmc 27
lcom 1
cbo 11
dl 0
loc 270
ccs 91
cts 93
cp 0.9785
rs 10
c 0
b 0
f 0

8 Methods

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