diff options
Diffstat (limited to 'vendor/doctrine/orm/src/Persisters/Collection/ManyToManyPersister.php')
-rw-r--r-- | vendor/doctrine/orm/src/Persisters/Collection/ManyToManyPersister.php | 770 |
1 files changed, 770 insertions, 0 deletions
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 | |||
3 | declare(strict_types=1); | ||
4 | |||
5 | namespace Doctrine\ORM\Persisters\Collection; | ||
6 | |||
7 | use BadMethodCallException; | ||
8 | use Doctrine\Common\Collections\Criteria; | ||
9 | use Doctrine\Common\Collections\Expr\Comparison; | ||
10 | use Doctrine\DBAL\Exception as DBALException; | ||
11 | use Doctrine\DBAL\LockMode; | ||
12 | use Doctrine\ORM\Mapping\AssociationMapping; | ||
13 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
14 | use Doctrine\ORM\Mapping\InverseSideMapping; | ||
15 | use Doctrine\ORM\Mapping\ManyToManyAssociationMapping; | ||
16 | use Doctrine\ORM\PersistentCollection; | ||
17 | use Doctrine\ORM\Persisters\SqlValueVisitor; | ||
18 | use Doctrine\ORM\Query; | ||
19 | use Doctrine\ORM\Utility\PersisterHelper; | ||
20 | |||
21 | use function array_fill; | ||
22 | use function array_pop; | ||
23 | use function assert; | ||
24 | use function count; | ||
25 | use function implode; | ||
26 | use function in_array; | ||
27 | use function reset; | ||
28 | use function sprintf; | ||
29 | |||
30 | /** | ||
31 | * Persister for many-to-many collections. | ||
32 | */ | ||
33 | class 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 | } | ||