summaryrefslogtreecommitdiff
path: root/vendor/doctrine/orm/src/Persisters/Collection
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/doctrine/orm/src/Persisters/Collection')
-rw-r--r--vendor/doctrine/orm/src/Persisters/Collection/AbstractCollectionPersister.php50
-rw-r--r--vendor/doctrine/orm/src/Persisters/Collection/CollectionPersister.php59
-rw-r--r--vendor/doctrine/orm/src/Persisters/Collection/ManyToManyPersister.php770
-rw-r--r--vendor/doctrine/orm/src/Persisters/Collection/OneToManyPersister.php264
4 files changed, 1143 insertions, 0 deletions
diff --git a/vendor/doctrine/orm/src/Persisters/Collection/AbstractCollectionPersister.php b/vendor/doctrine/orm/src/Persisters/Collection/AbstractCollectionPersister.php
new file mode 100644
index 0000000..26f0b9e
--- /dev/null
+++ b/vendor/doctrine/orm/src/Persisters/Collection/AbstractCollectionPersister.php
@@ -0,0 +1,50 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Persisters\Collection;
6
7use Doctrine\DBAL\Connection;
8use Doctrine\DBAL\Platforms\AbstractPlatform;
9use Doctrine\ORM\EntityManagerInterface;
10use Doctrine\ORM\Mapping\QuoteStrategy;
11use Doctrine\ORM\UnitOfWork;
12
13/**
14 * Base class for all collection persisters.
15 */
16abstract class AbstractCollectionPersister implements CollectionPersister
17{
18 protected Connection $conn;
19 protected UnitOfWork $uow;
20 protected AbstractPlatform $platform;
21 protected QuoteStrategy $quoteStrategy;
22
23 /**
24 * Initializes a new instance of a class derived from AbstractCollectionPersister.
25 */
26 public function __construct(
27 protected EntityManagerInterface $em,
28 ) {
29 $this->uow = $em->getUnitOfWork();
30 $this->conn = $em->getConnection();
31 $this->platform = $this->conn->getDatabasePlatform();
32 $this->quoteStrategy = $em->getConfiguration()->getQuoteStrategy();
33 }
34
35 /**
36 * Check if entity is in a valid state for operations.
37 */
38 protected function isValidEntityState(object $entity): bool
39 {
40 $entityState = $this->uow->getEntityState($entity, UnitOfWork::STATE_NEW);
41
42 if ($entityState === UnitOfWork::STATE_NEW) {
43 return false;
44 }
45
46 // If Entity is scheduled for inclusion, it is not in this collection.
47 // We can assure that because it would have return true before on array check
48 return ! ($entityState === UnitOfWork::STATE_MANAGED && $this->uow->isScheduledForInsert($entity));
49 }
50}
diff --git a/vendor/doctrine/orm/src/Persisters/Collection/CollectionPersister.php b/vendor/doctrine/orm/src/Persisters/Collection/CollectionPersister.php
new file mode 100644
index 0000000..07c4eaf
--- /dev/null
+++ b/vendor/doctrine/orm/src/Persisters/Collection/CollectionPersister.php
@@ -0,0 +1,59 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Persisters\Collection;
6
7use Doctrine\Common\Collections\Criteria;
8use Doctrine\ORM\PersistentCollection;
9
10/**
11 * Define the behavior that should be implemented by all collection persisters.
12 */
13interface CollectionPersister
14{
15 /**
16 * Deletes the persistent state represented by the given collection.
17 */
18 public function delete(PersistentCollection $collection): void;
19
20 /**
21 * Updates the given collection, synchronizing its state with the database
22 * by inserting, updating and deleting individual elements.
23 */
24 public function update(PersistentCollection $collection): void;
25
26 /**
27 * Counts the size of this persistent collection.
28 */
29 public function count(PersistentCollection $collection): int;
30
31 /**
32 * Slices elements.
33 *
34 * @return mixed[]
35 */
36 public function slice(PersistentCollection $collection, int $offset, int|null $length = null): array;
37
38 /**
39 * Checks for existence of an element.
40 */
41 public function contains(PersistentCollection $collection, object $element): bool;
42
43 /**
44 * Checks for existence of a key.
45 */
46 public function containsKey(PersistentCollection $collection, mixed $key): bool;
47
48 /**
49 * Gets an element by key.
50 */
51 public function get(PersistentCollection $collection, mixed $index): mixed;
52
53 /**
54 * Loads association entities matching the given Criteria object.
55 *
56 * @return mixed[]
57 */
58 public function loadCriteria(PersistentCollection $collection, Criteria $criteria): array;
59}
diff --git a/vendor/doctrine/orm/src/Persisters/Collection/ManyToManyPersister.php b/vendor/doctrine/orm/src/Persisters/Collection/ManyToManyPersister.php
new file mode 100644
index 0000000..7cf993d
--- /dev/null
+++ b/vendor/doctrine/orm/src/Persisters/Collection/ManyToManyPersister.php
@@ -0,0 +1,770 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Persisters\Collection;
6
7use BadMethodCallException;
8use Doctrine\Common\Collections\Criteria;
9use Doctrine\Common\Collections\Expr\Comparison;
10use Doctrine\DBAL\Exception as DBALException;
11use Doctrine\DBAL\LockMode;
12use Doctrine\ORM\Mapping\AssociationMapping;
13use Doctrine\ORM\Mapping\ClassMetadata;
14use Doctrine\ORM\Mapping\InverseSideMapping;
15use Doctrine\ORM\Mapping\ManyToManyAssociationMapping;
16use Doctrine\ORM\PersistentCollection;
17use Doctrine\ORM\Persisters\SqlValueVisitor;
18use Doctrine\ORM\Query;
19use Doctrine\ORM\Utility\PersisterHelper;
20
21use function array_fill;
22use function array_pop;
23use function assert;
24use function count;
25use function implode;
26use function in_array;
27use function reset;
28use function sprintf;
29
30/**
31 * Persister for many-to-many collections.
32 */
33class ManyToManyPersister extends AbstractCollectionPersister
34{
35 public function delete(PersistentCollection $collection): void
36 {
37 $mapping = $this->getMapping($collection);
38
39 if (! $mapping->isOwningSide()) {
40 return; // ignore inverse side
41 }
42
43 assert($mapping->isManyToManyOwningSide());
44
45 $types = [];
46 $class = $this->em->getClassMetadata($mapping->sourceEntity);
47
48 foreach ($mapping->joinTable->joinColumns as $joinColumn) {
49 $types[] = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $class, $this->em);
50 }
51
52 $this->conn->executeStatement($this->getDeleteSQL($collection), $this->getDeleteSQLParameters($collection), $types);
53 }
54
55 public function update(PersistentCollection $collection): void
56 {
57 $mapping = $this->getMapping($collection);
58
59 if (! $mapping->isOwningSide()) {
60 return; // ignore inverse side
61 }
62
63 [$deleteSql, $deleteTypes] = $this->getDeleteRowSQL($collection);
64 [$insertSql, $insertTypes] = $this->getInsertRowSQL($collection);
65
66 foreach ($collection->getDeleteDiff() as $element) {
67 $this->conn->executeStatement(
68 $deleteSql,
69 $this->getDeleteRowSQLParameters($collection, $element),
70 $deleteTypes,
71 );
72 }
73
74 foreach ($collection->getInsertDiff() as $element) {
75 $this->conn->executeStatement(
76 $insertSql,
77 $this->getInsertRowSQLParameters($collection, $element),
78 $insertTypes,
79 );
80 }
81 }
82
83 public function get(PersistentCollection $collection, mixed $index): object|null
84 {
85 $mapping = $this->getMapping($collection);
86
87 if (! $mapping->isIndexed()) {
88 throw new BadMethodCallException('Selecting a collection by index is only supported on indexed collections.');
89 }
90
91 $persister = $this->uow->getEntityPersister($mapping->targetEntity);
92 $mappedKey = $mapping->isOwningSide()
93 ? $mapping->inversedBy
94 : $mapping->mappedBy;
95
96 assert($mappedKey !== null);
97
98 return $persister->load(
99 [$mappedKey => $collection->getOwner(), $mapping->indexBy() => $index],
100 null,
101 $mapping,
102 [],
103 LockMode::NONE,
104 1,
105 );
106 }
107
108 public function count(PersistentCollection $collection): int
109 {
110 $conditions = [];
111 $params = [];
112 $types = [];
113 $mapping = $this->getMapping($collection);
114 $id = $this->uow->getEntityIdentifier($collection->getOwner());
115 $sourceClass = $this->em->getClassMetadata($mapping->sourceEntity);
116 $association = $this->em->getMetadataFactory()->getOwningSide($mapping);
117
118 $joinTableName = $this->quoteStrategy->getJoinTableName($association, $sourceClass, $this->platform);
119 $joinColumns = ! $mapping->isOwningSide()
120 ? $association->joinTable->inverseJoinColumns
121 : $association->joinTable->joinColumns;
122
123 foreach ($joinColumns as $joinColumn) {
124 $columnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $sourceClass, $this->platform);
125 $referencedName = $joinColumn->referencedColumnName;
126 $conditions[] = 't.' . $columnName . ' = ?';
127 $params[] = $id[$sourceClass->getFieldForColumn($referencedName)];
128 $types[] = PersisterHelper::getTypeOfColumn($referencedName, $sourceClass, $this->em);
129 }
130
131 [$joinTargetEntitySQL, $filterSql] = $this->getFilterSql($mapping);
132
133 if ($filterSql) {
134 $conditions[] = $filterSql;
135 }
136
137 // If there is a provided criteria, make part of conditions
138 // @todo Fix this. Current SQL returns something like:
139 /*if ($criteria && ($expression = $criteria->getWhereExpression()) !== null) {
140 // A join is needed on the target entity
141 $targetTableName = $this->quoteStrategy->getTableName($targetClass, $this->platform);
142 $targetJoinSql = ' JOIN ' . $targetTableName . ' te'
143 . ' ON' . implode(' AND ', $this->getOnConditionSQL($association));
144
145 // And criteria conditions needs to be added
146 $persister = $this->uow->getEntityPersister($targetClass->name);
147 $visitor = new SqlExpressionVisitor($persister, $targetClass);
148 $conditions[] = $visitor->dispatch($expression);
149
150 $joinTargetEntitySQL = $targetJoinSql . $joinTargetEntitySQL;
151 }*/
152
153 $sql = 'SELECT COUNT(*)'
154 . ' FROM ' . $joinTableName . ' t'
155 . $joinTargetEntitySQL
156 . ' WHERE ' . implode(' AND ', $conditions);
157
158 return (int) $this->conn->fetchOne($sql, $params, $types);
159 }
160
161 /**
162 * {@inheritDoc}
163 */
164 public function slice(PersistentCollection $collection, int $offset, int|null $length = null): array
165 {
166 $mapping = $this->getMapping($collection);
167 $persister = $this->uow->getEntityPersister($mapping->targetEntity);
168
169 return $persister->getManyToManyCollection($mapping, $collection->getOwner(), $offset, $length);
170 }
171
172 public function containsKey(PersistentCollection $collection, mixed $key): bool
173 {
174 $mapping = $this->getMapping($collection);
175
176 if (! $mapping->isIndexed()) {
177 throw new BadMethodCallException('Selecting a collection by index is only supported on indexed collections.');
178 }
179
180 [$quotedJoinTable, $whereClauses, $params, $types] = $this->getJoinTableRestrictionsWithKey(
181 $collection,
182 (string) $key,
183 true,
184 );
185
186 $sql = 'SELECT 1 FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses);
187
188 return (bool) $this->conn->fetchOne($sql, $params, $types);
189 }
190
191 public function contains(PersistentCollection $collection, object $element): bool
192 {
193 if (! $this->isValidEntityState($element)) {
194 return false;
195 }
196
197 [$quotedJoinTable, $whereClauses, $params, $types] = $this->getJoinTableRestrictions(
198 $collection,
199 $element,
200 true,
201 );
202
203 $sql = 'SELECT 1 FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses);
204
205 return (bool) $this->conn->fetchOne($sql, $params, $types);
206 }
207
208 /**
209 * {@inheritDoc}
210 */
211 public function loadCriteria(PersistentCollection $collection, Criteria $criteria): array
212 {
213 $mapping = $this->getMapping($collection);
214 $owner = $collection->getOwner();
215 $ownerMetadata = $this->em->getClassMetadata($owner::class);
216 $id = $this->uow->getEntityIdentifier($owner);
217 $targetClass = $this->em->getClassMetadata($mapping->targetEntity);
218 $onConditions = $this->getOnConditionSQL($mapping);
219 $whereClauses = $params = [];
220 $paramTypes = [];
221
222 if (! $mapping->isOwningSide()) {
223 assert($mapping instanceof InverseSideMapping);
224 $associationSourceClass = $targetClass;
225 $sourceRelationMode = 'relationToTargetKeyColumns';
226 } else {
227 $associationSourceClass = $ownerMetadata;
228 $sourceRelationMode = 'relationToSourceKeyColumns';
229 }
230
231 $mapping = $this->em->getMetadataFactory()->getOwningSide($mapping);
232
233 foreach ($mapping->$sourceRelationMode as $key => $value) {
234 $whereClauses[] = sprintf('t.%s = ?', $key);
235 $params[] = $ownerMetadata->containsForeignIdentifier
236 ? $id[$ownerMetadata->getFieldForColumn($value)]
237 : $id[$ownerMetadata->fieldNames[$value]];
238 $paramTypes[] = PersisterHelper::getTypeOfColumn($value, $ownerMetadata, $this->em);
239 }
240
241 $parameters = $this->expandCriteriaParameters($criteria);
242
243 foreach ($parameters as $parameter) {
244 [$name, $value, $operator] = $parameter;
245
246 $field = $this->quoteStrategy->getColumnName($name, $targetClass, $this->platform);
247
248 if ($value === null && ($operator === Comparison::EQ || $operator === Comparison::NEQ)) {
249 $whereClauses[] = sprintf('te.%s %s NULL', $field, $operator === Comparison::EQ ? 'IS' : 'IS NOT');
250 } else {
251 $whereClauses[] = sprintf('te.%s %s ?', $field, $operator);
252 $params[] = $value;
253 $paramTypes[] = PersisterHelper::getTypeOfField($name, $targetClass, $this->em)[0];
254 }
255 }
256
257 $tableName = $this->quoteStrategy->getTableName($targetClass, $this->platform);
258 $joinTable = $this->quoteStrategy->getJoinTableName($mapping, $associationSourceClass, $this->platform);
259
260 $rsm = new Query\ResultSetMappingBuilder($this->em);
261 $rsm->addRootEntityFromClassMetadata($targetClass->name, 'te');
262
263 $sql = 'SELECT ' . $rsm->generateSelectClause()
264 . ' FROM ' . $tableName . ' te'
265 . ' JOIN ' . $joinTable . ' t ON'
266 . implode(' AND ', $onConditions)
267 . ' WHERE ' . implode(' AND ', $whereClauses);
268
269 $sql .= $this->getOrderingSql($criteria, $targetClass);
270
271 $sql .= $this->getLimitSql($criteria);
272
273 $stmt = $this->conn->executeQuery($sql, $params, $paramTypes);
274
275 return $this
276 ->em
277 ->newHydrator(Query::HYDRATE_OBJECT)
278 ->hydrateAll($stmt, $rsm);
279 }
280
281 /**
282 * Generates the filter SQL for a given mapping.
283 *
284 * This method is not used for actually grabbing the related entities
285 * but when the extra-lazy collection methods are called on a filtered
286 * association. This is why besides the many to many table we also
287 * have to join in the actual entities table leading to additional
288 * JOIN.
289 *
290 * @param AssociationMapping $mapping Array containing mapping information.
291 *
292 * @return string[] ordered tuple:
293 * - JOIN condition to add to the SQL
294 * - WHERE condition to add to the SQL
295 * @psalm-return array{0: string, 1: string}
296 */
297 public function getFilterSql(AssociationMapping $mapping): array
298 {
299 $targetClass = $this->em->getClassMetadata($mapping->targetEntity);
300 $rootClass = $this->em->getClassMetadata($targetClass->rootEntityName);
301 $filterSql = $this->generateFilterConditionSQL($rootClass, 'te');
302
303 if ($filterSql === '') {
304 return ['', ''];
305 }
306
307 // A join is needed if there is filtering on the target entity
308 $tableName = $this->quoteStrategy->getTableName($rootClass, $this->platform);
309 $joinSql = ' JOIN ' . $tableName . ' te'
310 . ' ON' . implode(' AND ', $this->getOnConditionSQL($mapping));
311
312 return [$joinSql, $filterSql];
313 }
314
315 /**
316 * Generates the filter SQL for a given entity and table alias.
317 *
318 * @param ClassMetadata $targetEntity Metadata of the target entity.
319 * @param string $targetTableAlias The table alias of the joined/selected table.
320 *
321 * @return string The SQL query part to add to a query.
322 */
323 protected function generateFilterConditionSQL(ClassMetadata $targetEntity, string $targetTableAlias): string
324 {
325 $filterClauses = [];
326
327 foreach ($this->em->getFilters()->getEnabledFilters() as $filter) {
328 $filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias);
329 if ($filterExpr) {
330 $filterClauses[] = '(' . $filterExpr . ')';
331 }
332 }
333
334 return $filterClauses
335 ? '(' . implode(' AND ', $filterClauses) . ')'
336 : '';
337 }
338
339 /**
340 * Generate ON condition
341 *
342 * @return string[]
343 * @psalm-return list<string>
344 */
345 protected function getOnConditionSQL(AssociationMapping $mapping): array
346 {
347 $association = $this->em->getMetadataFactory()->getOwningSide($mapping);
348 $joinColumns = $mapping->isOwningSide()
349 ? $association->joinTable->inverseJoinColumns
350 : $association->joinTable->joinColumns;
351
352 $conditions = [];
353
354 $targetClass = $this->em->getClassMetadata($mapping->targetEntity);
355 foreach ($joinColumns as $joinColumn) {
356 $joinColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform);
357 $refColumnName = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform);
358
359 $conditions[] = ' t.' . $joinColumnName . ' = te.' . $refColumnName;
360 }
361
362 return $conditions;
363 }
364
365 protected function getDeleteSQL(PersistentCollection $collection): string
366 {
367 $columns = [];
368 $mapping = $this->getMapping($collection);
369 assert($mapping->isManyToManyOwningSide());
370 $class = $this->em->getClassMetadata($collection->getOwner()::class);
371 $joinTable = $this->quoteStrategy->getJoinTableName($mapping, $class, $this->platform);
372
373 foreach ($mapping->joinTable->joinColumns as $joinColumn) {
374 $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
375 }
376
377 return 'DELETE FROM ' . $joinTable
378 . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?';
379 }
380
381 /**
382 * Internal note: Order of the parameters must be the same as the order of the columns in getDeleteSql.
383 *
384 * @return list<mixed>
385 */
386 protected function getDeleteSQLParameters(PersistentCollection $collection): array
387 {
388 $mapping = $this->getMapping($collection);
389 assert($mapping->isManyToManyOwningSide());
390 $identifier = $this->uow->getEntityIdentifier($collection->getOwner());
391
392 // Optimization for single column identifier
393 if (count($mapping->relationToSourceKeyColumns) === 1) {
394 return [reset($identifier)];
395 }
396
397 // Composite identifier
398 $sourceClass = $this->em->getClassMetadata($mapping->sourceEntity);
399 $params = [];
400
401 foreach ($mapping->relationToSourceKeyColumns as $columnName => $refColumnName) {
402 $params[] = isset($sourceClass->fieldNames[$refColumnName])
403 ? $identifier[$sourceClass->fieldNames[$refColumnName]]
404 : $identifier[$sourceClass->getFieldForColumn($refColumnName)];
405 }
406
407 return $params;
408 }
409
410 /**
411 * Gets the SQL statement used for deleting a row from the collection.
412 *
413 * @return string[]|string[][] ordered tuple containing the SQL to be executed and an array
414 * of types for bound parameters
415 * @psalm-return array{0: string, 1: list<string>}
416 */
417 protected function getDeleteRowSQL(PersistentCollection $collection): array
418 {
419 $mapping = $this->getMapping($collection);
420 assert($mapping->isManyToManyOwningSide());
421 $class = $this->em->getClassMetadata($mapping->sourceEntity);
422 $targetClass = $this->em->getClassMetadata($mapping->targetEntity);
423 $columns = [];
424 $types = [];
425
426 foreach ($mapping->joinTable->joinColumns as $joinColumn) {
427 $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
428 $types[] = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $class, $this->em);
429 }
430
431 foreach ($mapping->joinTable->inverseJoinColumns as $joinColumn) {
432 $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform);
433 $types[] = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em);
434 }
435
436 return [
437 'DELETE FROM ' . $this->quoteStrategy->getJoinTableName($mapping, $class, $this->platform)
438 . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?',
439 $types,
440 ];
441 }
442
443 /**
444 * Gets the SQL parameters for the corresponding SQL statement to delete the given
445 * element from the given collection.
446 *
447 * Internal note: Order of the parameters must be the same as the order of the columns in getDeleteRowSql.
448 *
449 * @return mixed[]
450 * @psalm-return list<mixed>
451 */
452 protected function getDeleteRowSQLParameters(PersistentCollection $collection, object $element): array
453 {
454 return $this->collectJoinTableColumnParameters($collection, $element);
455 }
456
457 /**
458 * Gets the SQL statement used for inserting a row in the collection.
459 *
460 * @return string[]|string[][] ordered tuple containing the SQL to be executed and an array
461 * of types for bound parameters
462 * @psalm-return array{0: string, 1: list<string>}
463 */
464 protected function getInsertRowSQL(PersistentCollection $collection): array
465 {
466 $columns = [];
467 $types = [];
468 $mapping = $this->getMapping($collection);
469 assert($mapping->isManyToManyOwningSide());
470 $class = $this->em->getClassMetadata($mapping->sourceEntity);
471 $targetClass = $this->em->getClassMetadata($mapping->targetEntity);
472
473 foreach ($mapping->joinTable->joinColumns as $joinColumn) {
474 $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform);
475 $types[] = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $class, $this->em);
476 }
477
478 foreach ($mapping->joinTable->inverseJoinColumns as $joinColumn) {
479 $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform);
480 $types[] = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em);
481 }
482
483 return [
484 'INSERT INTO ' . $this->quoteStrategy->getJoinTableName($mapping, $class, $this->platform)
485 . ' (' . implode(', ', $columns) . ')'
486 . ' VALUES'
487 . ' (' . implode(', ', array_fill(0, count($columns), '?')) . ')',
488 $types,
489 ];
490 }
491
492 /**
493 * Gets the SQL parameters for the corresponding SQL statement to insert the given
494 * element of the given collection into the database.
495 *
496 * Internal note: Order of the parameters must be the same as the order of the columns in getInsertRowSql.
497 *
498 * @return mixed[]
499 * @psalm-return list<mixed>
500 */
501 protected function getInsertRowSQLParameters(PersistentCollection $collection, object $element): array
502 {
503 return $this->collectJoinTableColumnParameters($collection, $element);
504 }
505
506 /**
507 * Collects the parameters for inserting/deleting on the join table in the order
508 * of the join table columns as specified in ManyToManyMapping#joinTableColumns.
509 *
510 * @return mixed[]
511 * @psalm-return list<mixed>
512 */
513 private function collectJoinTableColumnParameters(
514 PersistentCollection $collection,
515 object $element,
516 ): array {
517 $params = [];
518 $mapping = $this->getMapping($collection);
519 assert($mapping->isManyToManyOwningSide());
520 $isComposite = count($mapping->joinTableColumns) > 2;
521
522 $identifier1 = $this->uow->getEntityIdentifier($collection->getOwner());
523 $identifier2 = $this->uow->getEntityIdentifier($element);
524
525 $class1 = $class2 = null;
526 if ($isComposite) {
527 $class1 = $this->em->getClassMetadata($collection->getOwner()::class);
528 $class2 = $collection->getTypeClass();
529 }
530
531 foreach ($mapping->joinTableColumns as $joinTableColumn) {
532 $isRelationToSource = isset($mapping->relationToSourceKeyColumns[$joinTableColumn]);
533
534 if (! $isComposite) {
535 $params[] = $isRelationToSource ? array_pop($identifier1) : array_pop($identifier2);
536
537 continue;
538 }
539
540 if ($isRelationToSource) {
541 $params[] = $identifier1[$class1->getFieldForColumn($mapping->relationToSourceKeyColumns[$joinTableColumn])];
542
543 continue;
544 }
545
546 $params[] = $identifier2[$class2->getFieldForColumn($mapping->relationToTargetKeyColumns[$joinTableColumn])];
547 }
548
549 return $params;
550 }
551
552 /**
553 * @param bool $addFilters Whether the filter SQL should be included or not.
554 *
555 * @return mixed[] ordered vector:
556 * - quoted join table name
557 * - where clauses to be added for filtering
558 * - parameters to be bound for filtering
559 * - types of the parameters to be bound for filtering
560 * @psalm-return array{0: string, 1: list<string>, 2: list<mixed>, 3: list<string>}
561 */
562 private function getJoinTableRestrictionsWithKey(
563 PersistentCollection $collection,
564 string $key,
565 bool $addFilters,
566 ): array {
567 $filterMapping = $this->getMapping($collection);
568 $mapping = $filterMapping;
569 $indexBy = $mapping->indexBy();
570 $id = $this->uow->getEntityIdentifier($collection->getOwner());
571 $sourceClass = $this->em->getClassMetadata($mapping->sourceEntity);
572 $targetClass = $this->em->getClassMetadata($mapping->targetEntity);
573
574 if (! $mapping->isOwningSide()) {
575 assert($mapping instanceof InverseSideMapping);
576 $associationSourceClass = $this->em->getClassMetadata($mapping->targetEntity);
577 $mapping = $associationSourceClass->associationMappings[$mapping->mappedBy];
578 assert($mapping->isManyToManyOwningSide());
579 $joinColumns = $mapping->joinTable->joinColumns;
580 $sourceRelationMode = 'relationToTargetKeyColumns';
581 $targetRelationMode = 'relationToSourceKeyColumns';
582 } else {
583 assert($mapping->isManyToManyOwningSide());
584 $associationSourceClass = $this->em->getClassMetadata($mapping->sourceEntity);
585 $joinColumns = $mapping->joinTable->inverseJoinColumns;
586 $sourceRelationMode = 'relationToSourceKeyColumns';
587 $targetRelationMode = 'relationToTargetKeyColumns';
588 }
589
590 $quotedJoinTable = $this->quoteStrategy->getJoinTableName($mapping, $associationSourceClass, $this->platform) . ' t';
591 $whereClauses = [];
592 $params = [];
593 $types = [];
594
595 $joinNeeded = ! in_array($indexBy, $targetClass->identifier, true);
596
597 if ($joinNeeded) { // extra join needed if indexBy is not a @id
598 $joinConditions = [];
599
600 foreach ($joinColumns as $joinTableColumn) {
601 $joinConditions[] = 't.' . $joinTableColumn->name . ' = tr.' . $joinTableColumn->referencedColumnName;
602 }
603
604 $tableName = $this->quoteStrategy->getTableName($targetClass, $this->platform);
605 $quotedJoinTable .= ' JOIN ' . $tableName . ' tr ON ' . implode(' AND ', $joinConditions);
606 $columnName = $targetClass->getColumnName($indexBy);
607
608 $whereClauses[] = 'tr.' . $columnName . ' = ?';
609 $params[] = $key;
610 $types[] = PersisterHelper::getTypeOfColumn($columnName, $targetClass, $this->em);
611 }
612
613 foreach ($mapping->joinTableColumns as $joinTableColumn) {
614 if (isset($mapping->{$sourceRelationMode}[$joinTableColumn])) {
615 $column = $mapping->{$sourceRelationMode}[$joinTableColumn];
616 $whereClauses[] = 't.' . $joinTableColumn . ' = ?';
617 $params[] = $sourceClass->containsForeignIdentifier
618 ? $id[$sourceClass->getFieldForColumn($column)]
619 : $id[$sourceClass->fieldNames[$column]];
620 $types[] = PersisterHelper::getTypeOfColumn($column, $sourceClass, $this->em);
621 } elseif (! $joinNeeded) {
622 $column = $mapping->{$targetRelationMode}[$joinTableColumn];
623
624 $whereClauses[] = 't.' . $joinTableColumn . ' = ?';
625 $params[] = $key;
626 $types[] = PersisterHelper::getTypeOfColumn($column, $targetClass, $this->em);
627 }
628 }
629
630 if ($addFilters) {
631 [$joinTargetEntitySQL, $filterSql] = $this->getFilterSql($filterMapping);
632
633 if ($filterSql) {
634 $quotedJoinTable .= ' ' . $joinTargetEntitySQL;
635 $whereClauses[] = $filterSql;
636 }
637 }
638
639 return [$quotedJoinTable, $whereClauses, $params, $types];
640 }
641
642 /**
643 * @param bool $addFilters Whether the filter SQL should be included or not.
644 *
645 * @return mixed[] ordered vector:
646 * - quoted join table name
647 * - where clauses to be added for filtering
648 * - parameters to be bound for filtering
649 * - types of the parameters to be bound for filtering
650 * @psalm-return array{0: string, 1: list<string>, 2: list<mixed>, 3: list<string>}
651 */
652 private function getJoinTableRestrictions(
653 PersistentCollection $collection,
654 object $element,
655 bool $addFilters,
656 ): array {
657 $filterMapping = $this->getMapping($collection);
658 $mapping = $filterMapping;
659
660 if (! $mapping->isOwningSide()) {
661 $sourceClass = $this->em->getClassMetadata($mapping->targetEntity);
662 $targetClass = $this->em->getClassMetadata($mapping->sourceEntity);
663 $sourceId = $this->uow->getEntityIdentifier($element);
664 $targetId = $this->uow->getEntityIdentifier($collection->getOwner());
665 } else {
666 $sourceClass = $this->em->getClassMetadata($mapping->sourceEntity);
667 $targetClass = $this->em->getClassMetadata($mapping->targetEntity);
668 $sourceId = $this->uow->getEntityIdentifier($collection->getOwner());
669 $targetId = $this->uow->getEntityIdentifier($element);
670 }
671
672 $mapping = $this->em->getMetadataFactory()->getOwningSide($mapping);
673
674 $quotedJoinTable = $this->quoteStrategy->getJoinTableName($mapping, $sourceClass, $this->platform);
675 $whereClauses = [];
676 $params = [];
677 $types = [];
678
679 foreach ($mapping->joinTableColumns as $joinTableColumn) {
680 $whereClauses[] = ($addFilters ? 't.' : '') . $joinTableColumn . ' = ?';
681
682 if (isset($mapping->relationToTargetKeyColumns[$joinTableColumn])) {
683 $targetColumn = $mapping->relationToTargetKeyColumns[$joinTableColumn];
684 $params[] = $targetId[$targetClass->getFieldForColumn($targetColumn)];
685 $types[] = PersisterHelper::getTypeOfColumn($targetColumn, $targetClass, $this->em);
686
687 continue;
688 }
689
690 // relationToSourceKeyColumns
691 $targetColumn = $mapping->relationToSourceKeyColumns[$joinTableColumn];
692 $params[] = $sourceId[$sourceClass->getFieldForColumn($targetColumn)];
693 $types[] = PersisterHelper::getTypeOfColumn($targetColumn, $sourceClass, $this->em);
694 }
695
696 if ($addFilters) {
697 $quotedJoinTable .= ' t';
698
699 [$joinTargetEntitySQL, $filterSql] = $this->getFilterSql($filterMapping);
700
701 if ($filterSql) {
702 $quotedJoinTable .= ' ' . $joinTargetEntitySQL;
703 $whereClauses[] = $filterSql;
704 }
705 }
706
707 return [$quotedJoinTable, $whereClauses, $params, $types];
708 }
709
710 /**
711 * Expands Criteria Parameters by walking the expressions and grabbing all
712 * parameters and types from it.
713 *
714 * @return mixed[][]
715 */
716 private function expandCriteriaParameters(Criteria $criteria): array
717 {
718 $expression = $criteria->getWhereExpression();
719
720 if ($expression === null) {
721 return [];
722 }
723
724 $valueVisitor = new SqlValueVisitor();
725
726 $valueVisitor->dispatch($expression);
727
728 [, $types] = $valueVisitor->getParamsAndTypes();
729
730 return $types;
731 }
732
733 private function getOrderingSql(Criteria $criteria, ClassMetadata $targetClass): string
734 {
735 $orderings = $criteria->orderings();
736 if ($orderings) {
737 $orderBy = [];
738 foreach ($orderings as $name => $direction) {
739 $field = $this->quoteStrategy->getColumnName(
740 $name,
741 $targetClass,
742 $this->platform,
743 );
744 $orderBy[] = $field . ' ' . $direction->value;
745 }
746
747 return ' ORDER BY ' . implode(', ', $orderBy);
748 }
749
750 return '';
751 }
752
753 /** @throws DBALException */
754 private function getLimitSql(Criteria $criteria): string
755 {
756 $limit = $criteria->getMaxResults();
757 $offset = $criteria->getFirstResult();
758
759 return $this->platform->modifyLimitQuery('', $limit, $offset ?? 0);
760 }
761
762 private function getMapping(PersistentCollection $collection): AssociationMapping&ManyToManyAssociationMapping
763 {
764 $mapping = $collection->getMapping();
765
766 assert($mapping instanceof ManyToManyAssociationMapping);
767
768 return $mapping;
769 }
770}
diff --git a/vendor/doctrine/orm/src/Persisters/Collection/OneToManyPersister.php b/vendor/doctrine/orm/src/Persisters/Collection/OneToManyPersister.php
new file mode 100644
index 0000000..0727b1f
--- /dev/null
+++ b/vendor/doctrine/orm/src/Persisters/Collection/OneToManyPersister.php
@@ -0,0 +1,264 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Persisters\Collection;
6
7use BadMethodCallException;
8use Doctrine\Common\Collections\Criteria;
9use Doctrine\DBAL\Exception as DBALException;
10use Doctrine\DBAL\Types\Type;
11use Doctrine\ORM\EntityNotFoundException;
12use Doctrine\ORM\Mapping\MappingException;
13use Doctrine\ORM\Mapping\OneToManyAssociationMapping;
14use Doctrine\ORM\PersistentCollection;
15use Doctrine\ORM\Utility\PersisterHelper;
16
17use function array_reverse;
18use function array_values;
19use function assert;
20use function implode;
21use function is_int;
22use function is_string;
23
24/**
25 * Persister for one-to-many collections.
26 */
27class OneToManyPersister extends AbstractCollectionPersister
28{
29 public function delete(PersistentCollection $collection): void
30 {
31 // The only valid case here is when you have weak entities. In this
32 // scenario, you have @OneToMany with orphanRemoval=true, and replacing
33 // the entire collection with a new would trigger this operation.
34 $mapping = $this->getMapping($collection);
35
36 if (! $mapping->orphanRemoval) {
37 // Handling non-orphan removal should never happen, as @OneToMany
38 // can only be inverse side. For owning side one to many, it is
39 // required to have a join table, which would classify as a ManyToManyPersister.
40 return;
41 }
42
43 $targetClass = $this->em->getClassMetadata($mapping->targetEntity);
44
45 $targetClass->isInheritanceTypeJoined()
46 ? $this->deleteJoinedEntityCollection($collection)
47 : $this->deleteEntityCollection($collection);
48 }
49
50 public function update(PersistentCollection $collection): void
51 {
52 // This can never happen. One to many can only be inverse side.
53 // For owning side one to many, it is required to have a join table,
54 // then classifying it as a ManyToManyPersister.
55 return;
56 }
57
58 public function get(PersistentCollection $collection, mixed $index): object|null
59 {
60 $mapping = $this->getMapping($collection);
61
62 if (! $mapping->isIndexed()) {
63 throw new BadMethodCallException('Selecting a collection by index is only supported on indexed collections.');
64 }
65
66 $persister = $this->uow->getEntityPersister($mapping->targetEntity);
67
68 return $persister->load(
69 [
70 $mapping->mappedBy => $collection->getOwner(),
71 $mapping->indexBy() => $index,
72 ],
73 null,
74 $mapping,
75 [],
76 null,
77 1,
78 );
79 }
80
81 public function count(PersistentCollection $collection): int
82 {
83 $mapping = $this->getMapping($collection);
84 $persister = $this->uow->getEntityPersister($mapping->targetEntity);
85
86 // only works with single id identifier entities. Will throw an
87 // exception in Entity Persisters if that is not the case for the
88 // 'mappedBy' field.
89 $criteria = new Criteria(Criteria::expr()->eq($mapping->mappedBy, $collection->getOwner()));
90
91 return $persister->count($criteria);
92 }
93
94 /**
95 * {@inheritDoc}
96 */
97 public function slice(PersistentCollection $collection, int $offset, int|null $length = null): array
98 {
99 $mapping = $this->getMapping($collection);
100 $persister = $this->uow->getEntityPersister($mapping->targetEntity);
101
102 return $persister->getOneToManyCollection($mapping, $collection->getOwner(), $offset, $length);
103 }
104
105 public function containsKey(PersistentCollection $collection, mixed $key): bool
106 {
107 $mapping = $this->getMapping($collection);
108
109 if (! $mapping->isIndexed()) {
110 throw new BadMethodCallException('Selecting a collection by index is only supported on indexed collections.');
111 }
112
113 $persister = $this->uow->getEntityPersister($mapping->targetEntity);
114
115 // only works with single id identifier entities. Will throw an
116 // exception in Entity Persisters if that is not the case for the
117 // 'mappedBy' field.
118 $criteria = new Criteria();
119
120 $criteria->andWhere(Criteria::expr()->eq($mapping->mappedBy, $collection->getOwner()));
121 $criteria->andWhere(Criteria::expr()->eq($mapping->indexBy(), $key));
122
123 return (bool) $persister->count($criteria);
124 }
125
126 public function contains(PersistentCollection $collection, object $element): bool
127 {
128 if (! $this->isValidEntityState($element)) {
129 return false;
130 }
131
132 $mapping = $this->getMapping($collection);
133 $persister = $this->uow->getEntityPersister($mapping->targetEntity);
134
135 // only works with single id identifier entities. Will throw an
136 // exception in Entity Persisters if that is not the case for the
137 // 'mappedBy' field.
138 $criteria = new Criteria(Criteria::expr()->eq($mapping->mappedBy, $collection->getOwner()));
139
140 return $persister->exists($element, $criteria);
141 }
142
143 /**
144 * {@inheritDoc}
145 */
146 public function loadCriteria(PersistentCollection $collection, Criteria $criteria): array
147 {
148 throw new BadMethodCallException('Filtering a collection by Criteria is not supported by this CollectionPersister.');
149 }
150
151 /**
152 * @throws DBALException
153 * @throws EntityNotFoundException
154 * @throws MappingException
155 */
156 private function deleteEntityCollection(PersistentCollection $collection): int
157 {
158 $mapping = $this->getMapping($collection);
159 $identifier = $this->uow->getEntityIdentifier($collection->getOwner());
160 $sourceClass = $this->em->getClassMetadata($mapping->sourceEntity);
161 $targetClass = $this->em->getClassMetadata($mapping->targetEntity);
162 $columns = [];
163 $parameters = [];
164 $types = [];
165
166 foreach ($this->em->getMetadataFactory()->getOwningSide($mapping)->joinColumns as $joinColumn) {
167 $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform);
168 $parameters[] = $identifier[$sourceClass->getFieldForColumn($joinColumn->referencedColumnName)];
169 $types[] = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $sourceClass, $this->em);
170 }
171
172 $statement = 'DELETE FROM ' . $this->quoteStrategy->getTableName($targetClass, $this->platform)
173 . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?';
174
175 if ($targetClass->isInheritanceTypeSingleTable()) {
176 $discriminatorColumn = $targetClass->getDiscriminatorColumn();
177 $statement .= ' AND ' . $discriminatorColumn->name . ' = ?';
178 $parameters[] = $targetClass->discriminatorValue;
179 $types[] = $discriminatorColumn->type;
180 }
181
182 $numAffected = $this->conn->executeStatement($statement, $parameters, $types);
183
184 assert(is_int($numAffected));
185
186 return $numAffected;
187 }
188
189 /**
190 * Delete Class Table Inheritance entities.
191 * A temporary table is needed to keep IDs to be deleted in both parent and child class' tables.
192 *
193 * Thanks Steve Ebersole (Hibernate) for idea on how to tackle reliably this scenario, we owe him a beer! =)
194 *
195 * @throws DBALException
196 */
197 private function deleteJoinedEntityCollection(PersistentCollection $collection): int
198 {
199 $mapping = $this->getMapping($collection);
200 $sourceClass = $this->em->getClassMetadata($mapping->sourceEntity);
201 $targetClass = $this->em->getClassMetadata($mapping->targetEntity);
202 $rootClass = $this->em->getClassMetadata($targetClass->rootEntityName);
203
204 // 1) Build temporary table DDL
205 $tempTable = $this->platform->getTemporaryTableName($rootClass->getTemporaryIdTableName());
206 $idColumnNames = $rootClass->getIdentifierColumnNames();
207 $idColumnList = implode(', ', $idColumnNames);
208 $columnDefinitions = [];
209
210 foreach ($idColumnNames as $idColumnName) {
211 $columnDefinitions[$idColumnName] = [
212 'name' => $idColumnName,
213 'notnull' => true,
214 'type' => Type::getType(PersisterHelper::getTypeOfColumn($idColumnName, $rootClass, $this->em)),
215 ];
216 }
217
218 $statement = $this->platform->getCreateTemporaryTableSnippetSQL() . ' ' . $tempTable
219 . ' (' . $this->platform->getColumnDeclarationListSQL($columnDefinitions) . ')';
220
221 $this->conn->executeStatement($statement);
222
223 // 2) Build insert table records into temporary table
224 $query = $this->em->createQuery(
225 ' SELECT t0.' . implode(', t0.', $rootClass->getIdentifierFieldNames())
226 . ' FROM ' . $targetClass->name . ' t0 WHERE t0.' . $mapping->mappedBy . ' = :owner',
227 )->setParameter('owner', $collection->getOwner());
228
229 $sql = $query->getSQL();
230 assert(is_string($sql));
231 $statement = 'INSERT INTO ' . $tempTable . ' (' . $idColumnList . ') ' . $sql;
232 $parameters = array_values($sourceClass->getIdentifierValues($collection->getOwner()));
233 $numDeleted = $this->conn->executeStatement($statement, $parameters);
234
235 // 3) Delete records on each table in the hierarchy
236 $classNames = [...$targetClass->parentClasses, ...[$targetClass->name], ...$targetClass->subClasses];
237
238 foreach (array_reverse($classNames) as $className) {
239 $tableName = $this->quoteStrategy->getTableName($this->em->getClassMetadata($className), $this->platform);
240 $statement = 'DELETE FROM ' . $tableName . ' WHERE (' . $idColumnList . ')'
241 . ' IN (SELECT ' . $idColumnList . ' FROM ' . $tempTable . ')';
242
243 $this->conn->executeStatement($statement);
244 }
245
246 // 4) Drop temporary table
247 $statement = $this->platform->getDropTemporaryTableSQL($tempTable);
248
249 $this->conn->executeStatement($statement);
250
251 assert(is_int($numDeleted));
252
253 return $numDeleted;
254 }
255
256 private function getMapping(PersistentCollection $collection): OneToManyAssociationMapping
257 {
258 $mapping = $collection->getMapping();
259
260 assert($mapping->isOneToMany());
261
262 return $mapping;
263 }
264}