Completed
Push — master ( 7e6070...203e38 )
by Gerrit
02:38
created

SimpleSelectDataLoader   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 270
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Test Coverage

Coverage 97.83%

Importance

Changes 0
Metric Value
wmc 25
lcom 1
cbo 11
dl 0
loc 270
ccs 90
cts 92
cp 0.9783
rs 10
c 0
b 0
f 0

8 Methods

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