Passed
Push — master ( 7c1db9...7e6070 )
by Gerrit
02:33
created

SimpleSelectDataLoader::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

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