summaryrefslogtreecommitdiff
path: root/vendor/doctrine/orm/src/Persisters/Entity/JoinedSubclassPersister.php
diff options
context:
space:
mode:
authorpolo <ordipolo@gmx.fr>2024-08-13 23:45:21 +0200
committerpolo <ordipolo@gmx.fr>2024-08-13 23:45:21 +0200
commitbf6655a534a6775d30cafa67bd801276bda1d98d (patch)
treec6381e3f6c81c33eab72508f410b165ba05f7e9c /vendor/doctrine/orm/src/Persisters/Entity/JoinedSubclassPersister.php
parent94d67a4b51f8e62e7d518cce26a526ae1ec48278 (diff)
downloadAppliGestionPHP-bf6655a534a6775d30cafa67bd801276bda1d98d.zip
VERSION 0.2 doctrine ORM et entités
Diffstat (limited to 'vendor/doctrine/orm/src/Persisters/Entity/JoinedSubclassPersister.php')
-rw-r--r--vendor/doctrine/orm/src/Persisters/Entity/JoinedSubclassPersister.php601
1 files changed, 601 insertions, 0 deletions
diff --git a/vendor/doctrine/orm/src/Persisters/Entity/JoinedSubclassPersister.php b/vendor/doctrine/orm/src/Persisters/Entity/JoinedSubclassPersister.php
new file mode 100644
index 0000000..76719a2
--- /dev/null
+++ b/vendor/doctrine/orm/src/Persisters/Entity/JoinedSubclassPersister.php
@@ -0,0 +1,601 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Persisters\Entity;
6
7use Doctrine\Common\Collections\Criteria;
8use Doctrine\DBAL\LockMode;
9use Doctrine\DBAL\Types\Type;
10use Doctrine\DBAL\Types\Types;
11use Doctrine\ORM\Internal\SQLResultCasing;
12use Doctrine\ORM\Mapping\AssociationMapping;
13use Doctrine\ORM\Mapping\ClassMetadata;
14use Doctrine\ORM\Utility\LockSqlHelper;
15use Doctrine\ORM\Utility\PersisterHelper;
16use LengthException;
17
18use function array_combine;
19use function array_keys;
20use function array_values;
21use function implode;
22
23/**
24 * The joined subclass persister maps a single entity instance to several tables in the
25 * database as it is defined by the <tt>Class Table Inheritance</tt> strategy.
26 *
27 * @see https://martinfowler.com/eaaCatalog/classTableInheritance.html
28 */
29class JoinedSubclassPersister extends AbstractEntityInheritancePersister
30{
31 use LockSqlHelper;
32 use SQLResultCasing;
33
34 /**
35 * Map that maps column names to the table names that own them.
36 * This is mainly a temporary cache, used during a single request.
37 *
38 * @psalm-var array<string, string>
39 */
40 private array $owningTableMap = [];
41
42 /**
43 * Map of table to quoted table names.
44 *
45 * @psalm-var array<string, string>
46 */
47 private array $quotedTableMap = [];
48
49 protected function getDiscriminatorColumnTableName(): string
50 {
51 $class = $this->class->name !== $this->class->rootEntityName
52 ? $this->em->getClassMetadata($this->class->rootEntityName)
53 : $this->class;
54
55 return $class->getTableName();
56 }
57
58 /**
59 * This function finds the ClassMetadata instance in an inheritance hierarchy
60 * that is responsible for enabling versioning.
61 */
62 private function getVersionedClassMetadata(): ClassMetadata
63 {
64 if (isset($this->class->fieldMappings[$this->class->versionField]->inherited)) {
65 $definingClassName = $this->class->fieldMappings[$this->class->versionField]->inherited;
66
67 return $this->em->getClassMetadata($definingClassName);
68 }
69
70 return $this->class;
71 }
72
73 /**
74 * Gets the name of the table that owns the column the given field is mapped to.
75 */
76 public function getOwningTable(string $fieldName): string
77 {
78 if (isset($this->owningTableMap[$fieldName])) {
79 return $this->owningTableMap[$fieldName];
80 }
81
82 $cm = match (true) {
83 isset($this->class->associationMappings[$fieldName]->inherited)
84 => $this->em->getClassMetadata($this->class->associationMappings[$fieldName]->inherited),
85 isset($this->class->fieldMappings[$fieldName]->inherited)
86 => $this->em->getClassMetadata($this->class->fieldMappings[$fieldName]->inherited),
87 default => $this->class,
88 };
89
90 $tableName = $cm->getTableName();
91 $quotedTableName = $this->quoteStrategy->getTableName($cm, $this->platform);
92
93 $this->owningTableMap[$fieldName] = $tableName;
94 $this->quotedTableMap[$tableName] = $quotedTableName;
95
96 return $tableName;
97 }
98
99 public function executeInserts(): void
100 {
101 if (! $this->queuedInserts) {
102 return;
103 }
104
105 $uow = $this->em->getUnitOfWork();
106 $idGenerator = $this->class->idGenerator;
107 $isPostInsertId = $idGenerator->isPostInsertGenerator();
108 $rootClass = $this->class->name !== $this->class->rootEntityName
109 ? $this->em->getClassMetadata($this->class->rootEntityName)
110 : $this->class;
111
112 // Prepare statement for the root table
113 $rootPersister = $this->em->getUnitOfWork()->getEntityPersister($rootClass->name);
114 $rootTableName = $rootClass->getTableName();
115 $rootTableStmt = $this->conn->prepare($rootPersister->getInsertSQL());
116
117 // Prepare statements for sub tables.
118 $subTableStmts = [];
119
120 if ($rootClass !== $this->class) {
121 $subTableStmts[$this->class->getTableName()] = $this->conn->prepare($this->getInsertSQL());
122 }
123
124 foreach ($this->class->parentClasses as $parentClassName) {
125 $parentClass = $this->em->getClassMetadata($parentClassName);
126 $parentTableName = $parentClass->getTableName();
127
128 if ($parentClass !== $rootClass) {
129 $parentPersister = $this->em->getUnitOfWork()->getEntityPersister($parentClassName);
130 $subTableStmts[$parentTableName] = $this->conn->prepare($parentPersister->getInsertSQL());
131 }
132 }
133
134 // Execute all inserts. For each entity:
135 // 1) Insert on root table
136 // 2) Insert on sub tables
137 foreach ($this->queuedInserts as $entity) {
138 $insertData = $this->prepareInsertData($entity);
139
140 // Execute insert on root table
141 $paramIndex = 1;
142
143 foreach ($insertData[$rootTableName] as $columnName => $value) {
144 $rootTableStmt->bindValue($paramIndex++, $value, $this->columnTypes[$columnName]);
145 }
146
147 $rootTableStmt->executeStatement();
148
149 if ($isPostInsertId) {
150 $generatedId = $idGenerator->generateId($this->em, $entity);
151 $id = [$this->class->identifier[0] => $generatedId];
152
153 $uow->assignPostInsertId($entity, $generatedId);
154 } else {
155 $id = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
156 }
157
158 // Execute inserts on subtables.
159 // The order doesn't matter because all child tables link to the root table via FK.
160 foreach ($subTableStmts as $tableName => $stmt) {
161 $paramIndex = 1;
162 $data = $insertData[$tableName] ?? [];
163
164 foreach ($id as $idName => $idVal) {
165 $type = $this->columnTypes[$idName] ?? Types::STRING;
166
167 $stmt->bindValue($paramIndex++, $idVal, $type);
168 }
169
170 foreach ($data as $columnName => $value) {
171 if (! isset($id[$columnName])) {
172 $stmt->bindValue($paramIndex++, $value, $this->columnTypes[$columnName]);
173 }
174 }
175
176 $stmt->executeStatement();
177 }
178
179 if ($this->class->requiresFetchAfterChange) {
180 $this->assignDefaultVersionAndUpsertableValues($entity, $id);
181 }
182 }
183
184 $this->queuedInserts = [];
185 }
186
187 public function update(object $entity): void
188 {
189 $updateData = $this->prepareUpdateData($entity);
190
191 if (! $updateData) {
192 return;
193 }
194
195 $isVersioned = $this->class->isVersioned;
196
197 $versionedClass = $this->getVersionedClassMetadata();
198 $versionedTable = $versionedClass->getTableName();
199
200 foreach ($updateData as $tableName => $data) {
201 $tableName = $this->quotedTableMap[$tableName];
202 $versioned = $isVersioned && $versionedTable === $tableName;
203
204 $this->updateTable($entity, $tableName, $data, $versioned);
205 }
206
207 if ($this->class->requiresFetchAfterChange) {
208 // Make sure the table with the version column is updated even if no columns on that
209 // table were affected.
210 if ($isVersioned && ! isset($updateData[$versionedTable])) {
211 $tableName = $this->quoteStrategy->getTableName($versionedClass, $this->platform);
212
213 $this->updateTable($entity, $tableName, [], true);
214 }
215
216 $identifiers = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
217
218 $this->assignDefaultVersionAndUpsertableValues($entity, $identifiers);
219 }
220 }
221
222 public function delete(object $entity): bool
223 {
224 $identifier = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
225 $id = array_combine($this->class->getIdentifierColumnNames(), $identifier);
226 $types = $this->getClassIdentifiersTypes($this->class);
227
228 $this->deleteJoinTableRecords($identifier, $types);
229
230 // Delete the row from the root table. Cascades do the rest.
231 $rootClass = $this->em->getClassMetadata($this->class->rootEntityName);
232 $rootTable = $this->quoteStrategy->getTableName($rootClass, $this->platform);
233 $rootTypes = $this->getClassIdentifiersTypes($rootClass);
234
235 return (bool) $this->conn->delete($rootTable, $id, $rootTypes);
236 }
237
238 public function getSelectSQL(
239 array|Criteria $criteria,
240 AssociationMapping|null $assoc = null,
241 LockMode|int|null $lockMode = null,
242 int|null $limit = null,
243 int|null $offset = null,
244 array|null $orderBy = null,
245 ): string {
246 $this->switchPersisterContext($offset, $limit);
247
248 $baseTableAlias = $this->getSQLTableAlias($this->class->name);
249 $joinSql = $this->getJoinSql($baseTableAlias);
250
251 if ($assoc !== null && $assoc->isManyToMany()) {
252 $joinSql .= $this->getSelectManyToManyJoinSQL($assoc);
253 }
254
255 $conditionSql = $criteria instanceof Criteria
256 ? $this->getSelectConditionCriteriaSQL($criteria)
257 : $this->getSelectConditionSQL($criteria, $assoc);
258
259 $filterSql = $this->generateFilterConditionSQL(
260 $this->em->getClassMetadata($this->class->rootEntityName),
261 $this->getSQLTableAlias($this->class->rootEntityName),
262 );
263 // If the current class in the root entity, add the filters
264 if ($filterSql) {
265 $conditionSql .= $conditionSql
266 ? ' AND ' . $filterSql
267 : $filterSql;
268 }
269
270 $orderBySql = '';
271
272 if ($assoc !== null && $assoc->isOrdered()) {
273 $orderBy = $assoc->orderBy();
274 }
275
276 if ($orderBy) {
277 $orderBySql = $this->getOrderBySQL($orderBy, $baseTableAlias);
278 }
279
280 $lockSql = '';
281
282 switch ($lockMode) {
283 case LockMode::PESSIMISTIC_READ:
284 $lockSql = ' ' . $this->getReadLockSQL($this->platform);
285
286 break;
287
288 case LockMode::PESSIMISTIC_WRITE:
289 $lockSql = ' ' . $this->getWriteLockSQL($this->platform);
290
291 break;
292 }
293
294 $tableName = $this->quoteStrategy->getTableName($this->class, $this->platform);
295 $from = ' FROM ' . $tableName . ' ' . $baseTableAlias;
296 $where = $conditionSql !== '' ? ' WHERE ' . $conditionSql : '';
297 $lock = $this->platform->appendLockHint($from, $lockMode ?? LockMode::NONE);
298 $columnList = $this->getSelectColumnsSQL();
299 $query = 'SELECT ' . $columnList
300 . $lock
301 . $joinSql
302 . $where
303 . $orderBySql;
304
305 return $this->platform->modifyLimitQuery($query, $limit, $offset ?? 0) . $lockSql;
306 }
307
308 public function getCountSQL(array|Criteria $criteria = []): string
309 {
310 $tableName = $this->quoteStrategy->getTableName($this->class, $this->platform);
311 $baseTableAlias = $this->getSQLTableAlias($this->class->name);
312 $joinSql = $this->getJoinSql($baseTableAlias);
313
314 $conditionSql = $criteria instanceof Criteria
315 ? $this->getSelectConditionCriteriaSQL($criteria)
316 : $this->getSelectConditionSQL($criteria);
317
318 $filterSql = $this->generateFilterConditionSQL($this->em->getClassMetadata($this->class->rootEntityName), $this->getSQLTableAlias($this->class->rootEntityName));
319
320 if ($filterSql !== '') {
321 $conditionSql = $conditionSql
322 ? $conditionSql . ' AND ' . $filterSql
323 : $filterSql;
324 }
325
326 return 'SELECT COUNT(*) '
327 . 'FROM ' . $tableName . ' ' . $baseTableAlias
328 . $joinSql
329 . (empty($conditionSql) ? '' : ' WHERE ' . $conditionSql);
330 }
331
332 protected function getLockTablesSql(LockMode|int $lockMode): string
333 {
334 $joinSql = '';
335 $identifierColumns = $this->class->getIdentifierColumnNames();
336 $baseTableAlias = $this->getSQLTableAlias($this->class->name);
337
338 // INNER JOIN parent tables
339 foreach ($this->class->parentClasses as $parentClassName) {
340 $conditions = [];
341 $tableAlias = $this->getSQLTableAlias($parentClassName);
342 $parentClass = $this->em->getClassMetadata($parentClassName);
343 $joinSql .= ' INNER JOIN ' . $this->quoteStrategy->getTableName($parentClass, $this->platform) . ' ' . $tableAlias . ' ON ';
344
345 foreach ($identifierColumns as $idColumn) {
346 $conditions[] = $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn;
347 }
348
349 $joinSql .= implode(' AND ', $conditions);
350 }
351
352 return parent::getLockTablesSql($lockMode) . $joinSql;
353 }
354
355 /**
356 * Ensure this method is never called. This persister overrides getSelectEntitiesSQL directly.
357 */
358 protected function getSelectColumnsSQL(): string
359 {
360 // Create the column list fragment only once
361 if ($this->currentPersisterContext->selectColumnListSql !== null) {
362 return $this->currentPersisterContext->selectColumnListSql;
363 }
364
365 $columnList = [];
366 $discrColumn = $this->class->getDiscriminatorColumn();
367 $discrColumnName = $discrColumn->name;
368 $discrColumnType = $discrColumn->type;
369 $baseTableAlias = $this->getSQLTableAlias($this->class->name);
370 $resultColumnName = $this->getSQLResultCasing($this->platform, $discrColumnName);
371
372 $this->currentPersisterContext->rsm->addEntityResult($this->class->name, 'r');
373 $this->currentPersisterContext->rsm->setDiscriminatorColumn('r', $resultColumnName);
374 $this->currentPersisterContext->rsm->addMetaResult('r', $resultColumnName, $discrColumnName, false, $discrColumnType);
375
376 // Add regular columns
377 foreach ($this->class->fieldMappings as $fieldName => $mapping) {
378 $class = isset($mapping->inherited)
379 ? $this->em->getClassMetadata($mapping->inherited)
380 : $this->class;
381
382 $columnList[] = $this->getSelectColumnSQL($fieldName, $class);
383 }
384
385 // Add foreign key columns
386 foreach ($this->class->associationMappings as $mapping) {
387 if (! $mapping->isToOneOwningSide()) {
388 continue;
389 }
390
391 $tableAlias = isset($mapping->inherited)
392 ? $this->getSQLTableAlias($mapping->inherited)
393 : $baseTableAlias;
394
395 $targetClass = $this->em->getClassMetadata($mapping->targetEntity);
396
397 foreach ($mapping->joinColumns as $joinColumn) {
398 $columnList[] = $this->getSelectJoinColumnSQL(
399 $tableAlias,
400 $joinColumn->name,
401 $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform),
402 PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em),
403 );
404 }
405 }
406
407 // Add discriminator column (DO NOT ALIAS, see AbstractEntityInheritancePersister#processSQLResult).
408 $tableAlias = $this->class->rootEntityName === $this->class->name
409 ? $baseTableAlias
410 : $this->getSQLTableAlias($this->class->rootEntityName);
411
412 $columnList[] = $tableAlias . '.' . $discrColumnName;
413
414 // sub tables
415 foreach ($this->class->subClasses as $subClassName) {
416 $subClass = $this->em->getClassMetadata($subClassName);
417 $tableAlias = $this->getSQLTableAlias($subClassName);
418
419 // Add subclass columns
420 foreach ($subClass->fieldMappings as $fieldName => $mapping) {
421 if (isset($mapping->inherited)) {
422 continue;
423 }
424
425 $columnList[] = $this->getSelectColumnSQL($fieldName, $subClass);
426 }
427
428 // Add join columns (foreign keys)
429 foreach ($subClass->associationMappings as $mapping) {
430 if (! $mapping->isToOneOwningSide() || isset($mapping->inherited)) {
431 continue;
432 }
433
434 $targetClass = $this->em->getClassMetadata($mapping->targetEntity);
435
436 foreach ($mapping->joinColumns as $joinColumn) {
437 $columnList[] = $this->getSelectJoinColumnSQL(
438 $tableAlias,
439 $joinColumn->name,
440 $this->quoteStrategy->getJoinColumnName($joinColumn, $subClass, $this->platform),
441 PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em),
442 );
443 }
444 }
445 }
446
447 $this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList);
448
449 return $this->currentPersisterContext->selectColumnListSql;
450 }
451
452 /**
453 * {@inheritDoc}
454 */
455 protected function getInsertColumnList(): array
456 {
457 // Identifier columns must always come first in the column list of subclasses.
458 $columns = $this->class->parentClasses
459 ? $this->class->getIdentifierColumnNames()
460 : [];
461
462 foreach ($this->class->reflFields as $name => $field) {
463 if (
464 isset($this->class->fieldMappings[$name]->inherited)
465 && ! isset($this->class->fieldMappings[$name]->id)
466 || isset($this->class->associationMappings[$name]->inherited)
467 || ($this->class->isVersioned && $this->class->versionField === $name)
468 || isset($this->class->embeddedClasses[$name])
469 || isset($this->class->fieldMappings[$name]->notInsertable)
470 ) {
471 continue;
472 }
473
474 if (isset($this->class->associationMappings[$name])) {
475 $assoc = $this->class->associationMappings[$name];
476 if ($assoc->isToOneOwningSide()) {
477 foreach ($assoc->targetToSourceKeyColumns as $sourceCol) {
478 $columns[] = $sourceCol;
479 }
480 }
481 } elseif (
482 $this->class->name !== $this->class->rootEntityName ||
483 ! $this->class->isIdGeneratorIdentity() || $this->class->identifier[0] !== $name
484 ) {
485 $columns[] = $this->quoteStrategy->getColumnName($name, $this->class, $this->platform);
486 $this->columnTypes[$name] = $this->class->fieldMappings[$name]->type;
487 }
488 }
489
490 // Add discriminator column if it is the topmost class.
491 if ($this->class->name === $this->class->rootEntityName) {
492 $columns[] = $this->class->getDiscriminatorColumn()->name;
493 }
494
495 return $columns;
496 }
497
498 /**
499 * {@inheritDoc}
500 */
501 protected function assignDefaultVersionAndUpsertableValues(object $entity, array $id): void
502 {
503 $values = $this->fetchVersionAndNotUpsertableValues($this->getVersionedClassMetadata(), $id);
504
505 foreach ($values as $field => $value) {
506 $value = Type::getType($this->class->fieldMappings[$field]->type)->convertToPHPValue($value, $this->platform);
507
508 $this->class->setFieldValue($entity, $field, $value);
509 }
510 }
511
512 /**
513 * {@inheritDoc}
514 */
515 protected function fetchVersionAndNotUpsertableValues(ClassMetadata $versionedClass, array $id): mixed
516 {
517 $columnNames = [];
518 foreach ($this->class->fieldMappings as $key => $column) {
519 $class = null;
520 if ($this->class->isVersioned && $key === $versionedClass->versionField) {
521 $class = $versionedClass;
522 } elseif (isset($column->generated)) {
523 $class = isset($column->inherited)
524 ? $this->em->getClassMetadata($column->inherited)
525 : $this->class;
526 } else {
527 continue;
528 }
529
530 $columnNames[$key] = $this->getSelectColumnSQL($key, $class);
531 }
532
533 $tableName = $this->quoteStrategy->getTableName($versionedClass, $this->platform);
534 $baseTableAlias = $this->getSQLTableAlias($this->class->name);
535 $joinSql = $this->getJoinSql($baseTableAlias);
536 $identifier = $this->quoteStrategy->getIdentifierColumnNames($versionedClass, $this->platform);
537 foreach ($identifier as $i => $idValue) {
538 $identifier[$i] = $baseTableAlias . '.' . $idValue;
539 }
540
541 $sql = 'SELECT ' . implode(', ', $columnNames)
542 . ' FROM ' . $tableName . ' ' . $baseTableAlias
543 . $joinSql
544 . ' WHERE ' . implode(' = ? AND ', $identifier) . ' = ?';
545
546 $flatId = $this->identifierFlattener->flattenIdentifier($versionedClass, $id);
547 $values = $this->conn->fetchNumeric(
548 $sql,
549 array_values($flatId),
550 $this->extractIdentifierTypes($id, $versionedClass),
551 );
552
553 if ($values === false) {
554 throw new LengthException('Unexpected empty result for database query.');
555 }
556
557 $values = array_combine(array_keys($columnNames), $values);
558
559 if (! $values) {
560 throw new LengthException('Unexpected number of database columns.');
561 }
562
563 return $values;
564 }
565
566 private function getJoinSql(string $baseTableAlias): string
567 {
568 $joinSql = '';
569 $identifierColumn = $this->class->getIdentifierColumnNames();
570
571 // INNER JOIN parent tables
572 foreach ($this->class->parentClasses as $parentClassName) {
573 $conditions = [];
574 $parentClass = $this->em->getClassMetadata($parentClassName);
575 $tableAlias = $this->getSQLTableAlias($parentClassName);
576 $joinSql .= ' INNER JOIN ' . $this->quoteStrategy->getTableName($parentClass, $this->platform) . ' ' . $tableAlias . ' ON ';
577
578 foreach ($identifierColumn as $idColumn) {
579 $conditions[] = $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn;
580 }
581
582 $joinSql .= implode(' AND ', $conditions);
583 }
584
585 // OUTER JOIN sub tables
586 foreach ($this->class->subClasses as $subClassName) {
587 $conditions = [];
588 $subClass = $this->em->getClassMetadata($subClassName);
589 $tableAlias = $this->getSQLTableAlias($subClassName);
590 $joinSql .= ' LEFT JOIN ' . $this->quoteStrategy->getTableName($subClass, $this->platform) . ' ' . $tableAlias . ' ON ';
591
592 foreach ($identifierColumn as $idColumn) {
593 $conditions[] = $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn;
594 }
595
596 $joinSql .= implode(' AND ', $conditions);
597 }
598
599 return $joinSql;
600 }
601}