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
Unused Code
introduced
by
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
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
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 |