Issues (259)

DataLoader/SimpleSelectDataLoader.php (2 issues)

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 Closure;
17
use Doctrine\Common\Util\ClassUtils;
18
use Doctrine\DBAL\Schema\Column;
19
use Doctrine\DBAL\Connection;
20
use Doctrine\DBAL\Query\QueryBuilder;
21
use Doctrine\ORM\EntityManagerInterface;
22
use Doctrine\ORM\Mapping\ClassMetadata;
23
use ReflectionClass;
24
use ReflectionProperty;
25
use Doctrine\ORM\Query\Expr;
26
use Doctrine\DBAL\Driver\Statement;
27
use PDO;
28
use Addiks\RDMBundle\Mapping\MappingInterface;
29
use Addiks\RDMBundle\Hydration\HydrationContext;
30
use Webmozart\Assert\Assert;
31
use ErrorException;
32
33
/**
34
 * A very simple loader that just executes one simple select statement for every entity to load the data for.
35
 *
36
 * Because it executes one query for every entity to load data for, this could (and probably will) have an bad impact on
37
 * performance.
38
 *
39
 * TODO: This may be replaced in the future by integrating that data-loading into the select(s) executed by doctrine.
40
 */
41
final class SimpleSelectDataLoader implements DataLoaderInterface
42
{
43
44
    /**
45
     * @var MappingDriverInterface
46
     */
47
    private $mappingDriver;
48
49
    /**
50
     * @var array<array<string, string>>
51
     */
52
    private $originalData = array();
53 10
54
    public function __construct(
55
        MappingDriverInterface $mappingDriver
56 10
    ) {
57
        $this->mappingDriver = $mappingDriver;
58
    }
59
60
    public function boot(EntityManagerInterface|Closure $entityManager): void
61
    {
62
    }
63
64
    /**
65
     * @param object $entity
66
     *
67
     * @return array<string, string>
68 6
     */
69
    public function loadDBALDataForEntity($entity, EntityManagerInterface $entityManager): array
70
    {
71 6
        /** @var class-string $className */
72
        $className = get_class($entity);
73
74 6
        /** @var string $entityObjectHash */
75
        $entityObjectHash = spl_object_hash($entity);
76 6
77
        $this->originalData[$entityObjectHash] = [];
78
79 6
        /** @var array<string> $additionalData */
80
        $additionalData = array();
0 ignored issues
show
The assignment to $additionalData is dead and can be removed.
Loading history...
81
82 6
        do {
83 6
            if (class_exists(ClassUtils::class)) {
84 6
                $className = ClassUtils::getRealClass($className);
85
                Assert::classExists($className);
86
            }
87 6
88
            if (!$entityManager->getMetadataFactory()->isTransient($className)) {
89 6
                /** @var ClassMetadata $classMetaData */
90
                $classMetaData = $entityManager->getClassMetadata($className);
91
92 6
                /** @var ?EntityMappingInterface $entityMapping */
93
                $entityMapping = $this->mappingDriver->loadRDMMetadataForClass($className);
94 6
95
                if ($entityMapping instanceof EntityMappingInterface) {
96 6
                    /** @var array<Column> $additionalColumns */
97
                    $additionalColumns = $entityMapping->collectDBALColumns();
98 6
99
                    if (!empty($additionalColumns)) {
100 3
                        /** @var Connection $connection */
101
                        $connection = $entityManager->getConnection();
102
103 3
                        /** @var QueryBuilder $queryBuilder */
104
                        $queryBuilder = $connection->createQueryBuilder();
105
106 3
                        /** @var Expr $expr */
107
                        $expr = $queryBuilder->expr();
108 3
109
                        foreach ($additionalColumns as $column) {
110
                            /** @var Column $column */
111 3
112
                            $queryBuilder->addSelect($column->getName());
113
                        }
114 3
115
                        $reflectionClass = new ReflectionClass($className);
116
117 3
                        /** @var bool $hasId */
118
                        $hasId = false;
119 3
120
                        foreach ($classMetaData->identifier as $idFieldName) {
121
                            /** @var string $idFieldName */
122
123 3
                            /** @var array $idColumn */
124
                            $idColumn = $classMetaData->fieldMappings[$idFieldName];
125
126 3
                            /** @var ReflectionProperty $reflectionProperty */
127
                            $reflectionProperty = $reflectionClass->getProperty($idFieldName);
128 3
129
                            $reflectionProperty->setAccessible(true);
130 3
131
                            $idValue = $reflectionProperty->getValue($entity);
132 3
133 3
                            if (!empty($idValue)) {
134 3
                                $hasId = true;
135 2
                                if (!is_numeric($idValue) || empty($idValue)) {
136
                                    $idValue = "'{$idValue}'";
137 3
                                }
138
                                $queryBuilder->andWhere($expr->eq($idColumn['columnName'], $idValue));
139
                            }
140
                        }
141 3
142 3
                        if ($hasId) {
143 3
                            $queryBuilder->from($classMetaData->getTableName());
144
                            $queryBuilder->setMaxResults(1);
145
146 3
                            /** @var Statement $statement */
147
                            $statement = $queryBuilder->execute();
148 3
149
                            $additionalData = $statement->fetch(PDO::FETCH_ASSOC);
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\DBAL\ForwardCompatibility\Result::fetch() has been deprecated: Use fetchNumeric(), fetchAssociative() or fetchOne() instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

149
                            $additionalData = /** @scrutinizer ignore-deprecated */ $statement->fetch(PDO::FETCH_ASSOC);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
150 3
151
                            if (!is_array($additionalData)) {
152
                                $additionalData = array();
153
                            }
154 3
155 3
                            $this->originalData[$entityObjectHash] = array_merge(
156
                                $this->originalData[$entityObjectHash],
157
                                $additionalData
158
                            );
159
                        }
160
                    }
161
                }
162
            }
163 6
164 6
            $className = current(class_parents($className));
165
        } while (class_exists($className));
166 6
167
        return $this->originalData[$entityObjectHash] ?? [];
168
    }
169
170
    /**
171
     * @param object $entity
172 4
     */
173
    public function storeDBALDataForEntity($entity, EntityManagerInterface $entityManager): void
174
    {
175 4
        /** @var class-string $className */
176
        $className = get_class($entity);
177 4
178 4
        if (class_exists(ClassUtils::class)) {
179 4
            $className = ClassUtils::getRealClass($className);
180
            Assert::classExists($className);
181
        }
182
183
        do {
184 4
            /** @var null|EntityMappingInterface $entityMapping */
185
            $entityMapping = $this->mappingDriver->loadRDMMetadataForClass($className);
186 4
187 4
            if ($entityMapping instanceof EntityMappingInterface) {
188
                $context = new HydrationContext($entity, $entityManager);
189
190 4
                /** @var array<scalar> */
191
                $additionalData = $entityMapping->revertValue($context, $entity);
192 4
193
                if ($this->hasDataChanged($entity, $additionalData)) {
194 3
                    /** @var ClassMetadata $classMetaData */
195
                    $classMetaData = $entityManager->getClassMetadata($className);
196
197 3
                    /** @var array<scalar> $identifier */
198
                    $identifier = $this->collectIdentifierForEntity($entity, $entityMapping, $classMetaData);
199
200 3
                    /** @var string $tableName */
201
                    $tableName = $classMetaData->getTableName();
202
203 3
                    /** @var Connection $connection */
204
                    $connection = $entityManager->getConnection();
205 3
206
                    $connection->update($tableName, $additionalData, $identifier);
207
                }
208
            }
209 4
210 4
            $className = current(class_parents($className));
211
        } while (class_exists($className));
212
    }
213
214
    /**
215
     * @param object $entity
216
     */
217
    public function removeDBALDataForEntity($entity, EntityManagerInterface $entityManager): void
218
    {
219
        # This data-loader does not store data outside the entity-table.
220
        # No additional data need to be removed.
221
    }
222
223
    public function prepareOnMetadataLoad(EntityManagerInterface $entityManager, ClassMetadata $classMetadata): void
224
    {
225
        # This data-loader does not need any preparation
226
    }
227
228
    /**
229
     * @param object $entity
230 4
     */
231
    private function hasDataChanged($entity, array $additionalData): bool
232
    {
233 4
        /** @var array<scalar> */
234
        $originalData = array();
235
236 4
        /** @var string $entityObjectHash */
237
        $entityObjectHash = spl_object_hash($entity);
238 4
239 2
        if (isset($this->originalData[$entityObjectHash])) {
240
            $originalData = $this->originalData[$entityObjectHash];
241
        }
242
243 4
        /** @var bool $hasDataChanged */
244
        $hasDataChanged = false;
245 4
246 3
        foreach ($additionalData as $key => $value) {
247 3
            if (!array_key_exists($key, $originalData) || $originalData[$key] != $value) {
248 3
                $hasDataChanged = true;
249
                break;
250
            }
251
        }
252 4
253
        return $hasDataChanged;
254
    }
255
256
    /**
257
     * @param object $entity
258 3
     */
259
    private function collectIdentifierForEntity(
260
        $entity,
261
        EntityMappingInterface $entityMapping,
262
        ClassMetadata $classMetaData
263 3
    ): array {
264
        $reflectionClass = new ReflectionClass($entityMapping->getEntityClassName());
265
266 3
        /** @var array<scalar> $identifier */
267
        $identifier = array();
268 3
269
        foreach ($classMetaData->identifier as $idFieldName) {
270
            /** @var string $idFieldName */
271
272 3
            /** @var array $idColumn */
273
            $idColumn = $classMetaData->fieldMappings[$idFieldName];
274
275 3
            /** @var ReflectionProperty $reflectionProperty */
276
            $reflectionProperty = $reflectionClass->getProperty($idFieldName);
277 3
278
            $reflectionProperty->setAccessible(true);
279 3
280
            $idValue = $reflectionProperty->getValue($entity);
281 3
282
            $identifier[$idColumn['columnName']] = $idValue;
283
        }
284 3
285
        return $identifier;
286
    }
287
288
}
289