1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace Doctrine\ORM\Persisters\Collection; |
||
6 | |||
7 | use Doctrine\Common\Collections\Criteria; |
||
8 | use Doctrine\DBAL\DBALException; |
||
9 | use Doctrine\ORM\Mapping\ClassMetadata; |
||
10 | use Doctrine\ORM\Mapping\FieldMetadata; |
||
11 | use Doctrine\ORM\Mapping\JoinColumnMetadata; |
||
12 | use Doctrine\ORM\Mapping\ManyToManyAssociationMetadata; |
||
13 | use Doctrine\ORM\Mapping\ToManyAssociationMetadata; |
||
14 | use Doctrine\ORM\Mapping\ToOneAssociationMetadata; |
||
15 | use Doctrine\ORM\PersistentCollection; |
||
16 | use Doctrine\ORM\Persisters\SqlValueVisitor; |
||
17 | use Doctrine\ORM\Query; |
||
18 | use Doctrine\ORM\Utility\PersisterHelper; |
||
19 | use function array_fill; |
||
20 | use function count; |
||
21 | use function get_class; |
||
22 | use function implode; |
||
23 | use function in_array; |
||
24 | use function reset; |
||
25 | use function sprintf; |
||
26 | |||
27 | /** |
||
28 | * Persister for many-to-many collections. |
||
29 | */ |
||
30 | class ManyToManyPersister extends AbstractCollectionPersister |
||
31 | { |
||
32 | /** |
||
33 | * {@inheritdoc} |
||
34 | */ |
||
35 | 18 | public function delete(PersistentCollection $collection) |
|
36 | { |
||
37 | 18 | $association = $collection->getMapping(); |
|
38 | |||
39 | 18 | if (! $association->isOwningSide()) { |
|
40 | return; // ignore inverse side |
||
41 | } |
||
42 | |||
43 | 18 | $class = $this->em->getClassMetadata($association->getSourceEntity()); |
|
44 | 18 | $joinTable = $association->getJoinTable(); |
|
45 | 18 | $types = []; |
|
46 | |||
47 | 18 | foreach ($joinTable->getJoinColumns() as $joinColumn) { |
|
48 | /** @var JoinColumnMetadata $joinColumn */ |
||
49 | 18 | $referencedColumnName = $joinColumn->getReferencedColumnName(); |
|
50 | |||
51 | 18 | if (! $joinColumn->getType()) { |
|
52 | $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $class, $this->em)); |
||
53 | } |
||
54 | |||
55 | 18 | $types[] = $joinColumn->getType(); |
|
56 | } |
||
57 | |||
58 | 18 | $sql = $this->getDeleteSQL($collection); |
|
59 | 18 | $params = $this->getDeleteSQLParameters($collection); |
|
60 | |||
61 | 18 | $this->conn->executeUpdate($sql, $params, $types); |
|
62 | 18 | } |
|
63 | |||
64 | /** |
||
65 | * {@inheritdoc} |
||
66 | */ |
||
67 | 329 | public function update(PersistentCollection $collection) |
|
68 | { |
||
69 | 329 | $association = $collection->getMapping(); |
|
70 | |||
71 | 329 | if (! $association->isOwningSide()) { |
|
72 | 236 | return; // ignore inverse side |
|
73 | } |
||
74 | |||
75 | 328 | list($deleteSql, $deleteTypes) = $this->getDeleteRowSQL($collection); |
|
76 | 328 | list($insertSql, $insertTypes) = $this->getInsertRowSQL($collection); |
|
77 | |||
78 | 328 | foreach ($collection->getDeleteDiff() as $element) { |
|
79 | 10 | $this->conn->executeUpdate( |
|
80 | 10 | $deleteSql, |
|
81 | 10 | $this->getDeleteRowSQLParameters($collection, $element), |
|
82 | 10 | $deleteTypes |
|
83 | ); |
||
84 | } |
||
85 | |||
86 | 328 | foreach ($collection->getInsertDiff() as $element) { |
|
87 | 328 | $this->conn->executeUpdate( |
|
88 | 328 | $insertSql, |
|
89 | 328 | $this->getInsertRowSQLParameters($collection, $element), |
|
90 | 328 | $insertTypes |
|
91 | ); |
||
92 | } |
||
93 | 328 | } |
|
94 | |||
95 | /** |
||
96 | * {@inheritdoc} |
||
97 | */ |
||
98 | 3 | public function get(PersistentCollection $collection, $index) |
|
99 | { |
||
100 | 3 | $association = $collection->getMapping(); |
|
101 | |||
102 | 3 | if (! ($association instanceof ToManyAssociationMetadata && $association->getIndexedBy())) { |
|
103 | throw new \BadMethodCallException('Selecting a collection by index is only supported on indexed collections.'); |
||
104 | } |
||
105 | |||
106 | 3 | $persister = $this->uow->getEntityPersister($association->getTargetEntity()); |
|
107 | 3 | $mappedKey = $association->isOwningSide() |
|
108 | 2 | ? $association->getInversedBy() |
|
109 | 3 | : $association->getMappedBy() |
|
110 | ; |
||
111 | |||
112 | $criteria = [ |
||
113 | 3 | $mappedKey => $collection->getOwner(), |
|
114 | 3 | $association->getIndexedBy() => $index, |
|
115 | ]; |
||
116 | |||
117 | 3 | return $persister->load($criteria, null, $association, [], 0, 1); |
|
118 | } |
||
119 | |||
120 | /** |
||
121 | * {@inheritdoc} |
||
122 | */ |
||
123 | 18 | public function count(PersistentCollection $collection) |
|
124 | { |
||
125 | 18 | $conditions = []; |
|
126 | 18 | $params = []; |
|
127 | 18 | $types = []; |
|
128 | 18 | $association = $collection->getMapping(); |
|
129 | 18 | $identifier = $this->uow->getEntityIdentifier($collection->getOwner()); |
|
130 | 18 | $sourceClass = $this->em->getClassMetadata($association->getSourceEntity()); |
|
131 | 18 | $targetClass = $this->em->getClassMetadata($association->getTargetEntity()); |
|
132 | 18 | $owningAssociation = ! $association->isOwningSide() |
|
133 | 4 | ? $targetClass->getProperty($association->getMappedBy()) |
|
134 | 18 | : $association |
|
135 | ; |
||
136 | |||
137 | 18 | $joinTable = $owningAssociation->getJoinTable(); |
|
138 | 18 | $joinTableName = $joinTable->getQuotedQualifiedName($this->platform); |
|
139 | 18 | $joinColumns = $association->isOwningSide() |
|
140 | 14 | ? $joinTable->getJoinColumns() |
|
141 | 18 | : $joinTable->getInverseJoinColumns() |
|
142 | ; |
||
143 | |||
144 | 18 | foreach ($joinColumns as $joinColumn) { |
|
145 | /** @var JoinColumnMetadata $joinColumn */ |
||
146 | 18 | $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName()); |
|
147 | 18 | $referencedColumnName = $joinColumn->getReferencedColumnName(); |
|
148 | |||
149 | 18 | if (! $joinColumn->getType()) { |
|
150 | 1 | $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $sourceClass, $this->em)); |
|
151 | } |
||
152 | |||
153 | 18 | $conditions[] = sprintf('t.%s = ?', $quotedColumnName); |
|
154 | 18 | $params[] = $identifier[$sourceClass->fieldNames[$referencedColumnName]]; |
|
155 | 18 | $types[] = $joinColumn->getType(); |
|
156 | } |
||
157 | |||
158 | 18 | list($joinTargetEntitySQL, $filterSql) = $this->getFilterSql($association); |
|
159 | |||
160 | 18 | if ($filterSql) { |
|
161 | 3 | $conditions[] = $filterSql; |
|
162 | } |
||
163 | |||
164 | // If there is a provided criteria, make part of conditions |
||
165 | // @todo Fix this. Current SQL returns something like: |
||
166 | // |
||
167 | /*if ($criteria && ($expression = $criteria->getWhereExpression()) !== null) { |
||
168 | // A join is needed on the target entity |
||
169 | $targetTableName = $targetClass->table->getQuotedQualifiedName($this->platform); |
||
170 | $targetJoinSql = ' JOIN ' . $targetTableName . ' te' |
||
171 | . ' ON' . implode(' AND ', $this->getOnConditionSQL($association)); |
||
172 | |||
173 | // And criteria conditions needs to be added |
||
174 | $persister = $this->uow->getEntityPersister($targetClass->getClassName()); |
||
175 | $visitor = new SqlExpressionVisitor($persister, $targetClass); |
||
176 | $conditions[] = $visitor->dispatch($expression); |
||
177 | |||
178 | $joinTargetEntitySQL = $targetJoinSql . $joinTargetEntitySQL; |
||
179 | }*/ |
||
180 | |||
181 | $sql = 'SELECT COUNT(*)' |
||
182 | 18 | . ' FROM ' . $joinTableName . ' t' |
|
183 | 18 | . $joinTargetEntitySQL |
|
184 | 18 | . ' WHERE ' . implode(' AND ', $conditions); |
|
185 | |||
186 | 18 | return $this->conn->fetchColumn($sql, $params, 0, $types); |
|
187 | } |
||
188 | |||
189 | /** |
||
190 | * {@inheritDoc} |
||
191 | */ |
||
192 | 8 | public function slice(PersistentCollection $collection, $offset, $length = null) |
|
193 | { |
||
194 | 8 | $association = $collection->getMapping(); |
|
195 | 8 | $persister = $this->uow->getEntityPersister($association->getTargetEntity()); |
|
196 | |||
197 | 8 | return $persister->getManyToManyCollection($association, $collection->getOwner(), $offset, $length); |
|
198 | } |
||
199 | /** |
||
200 | * {@inheritdoc} |
||
201 | */ |
||
202 | 7 | public function containsKey(PersistentCollection $collection, $key) |
|
203 | { |
||
204 | 7 | $association = $collection->getMapping(); |
|
205 | |||
206 | 7 | if (! ($association instanceof ToManyAssociationMetadata && $association->getIndexedBy())) { |
|
207 | throw new \BadMethodCallException('Selecting a collection by index is only supported on indexed collections.'); |
||
208 | } |
||
209 | |||
210 | 7 | list($quotedJoinTable, $whereClauses, $params, $types) = $this->getJoinTableRestrictionsWithKey($collection, $key, true); |
|
211 | |||
212 | 7 | $sql = 'SELECT 1 FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses); |
|
213 | |||
214 | 7 | return (bool) $this->conn->fetchColumn($sql, $params, 0, $types); |
|
215 | } |
||
216 | |||
217 | /** |
||
218 | * {@inheritDoc} |
||
219 | */ |
||
220 | 7 | public function contains(PersistentCollection $collection, $element) |
|
221 | { |
||
222 | 7 | if (! $this->isValidEntityState($element)) { |
|
223 | 2 | return false; |
|
224 | } |
||
225 | |||
226 | 7 | list($quotedJoinTable, $whereClauses, $params, $types) = $this->getJoinTableRestrictions($collection, $element, true); |
|
227 | |||
228 | 7 | $sql = 'SELECT 1 FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses); |
|
229 | |||
230 | 7 | return (bool) $this->conn->fetchColumn($sql, $params, 0, $types); |
|
231 | } |
||
232 | |||
233 | /** |
||
234 | * {@inheritDoc} |
||
235 | */ |
||
236 | 2 | public function removeElement(PersistentCollection $collection, $element) |
|
237 | { |
||
238 | 2 | if (! $this->isValidEntityState($element)) { |
|
239 | 2 | return false; |
|
240 | } |
||
241 | |||
242 | 2 | list($quotedJoinTable, $whereClauses, $params, $types) = $this->getJoinTableRestrictions($collection, $element, false); |
|
243 | |||
244 | 2 | $sql = 'DELETE FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses); |
|
245 | |||
246 | 2 | return (bool) $this->conn->executeUpdate($sql, $params, $types); |
|
247 | } |
||
248 | |||
249 | /** |
||
250 | * {@inheritDoc} |
||
251 | */ |
||
252 | 12 | public function loadCriteria(PersistentCollection $collection, Criteria $criteria) |
|
253 | { |
||
254 | 12 | $association = $collection->getMapping(); |
|
255 | 12 | $owner = $collection->getOwner(); |
|
256 | 12 | $ownerMetadata = $this->em->getClassMetadata(get_class($owner)); |
|
257 | 12 | $identifier = $this->uow->getEntityIdentifier($owner); |
|
258 | 12 | $targetClass = $this->em->getClassMetadata($association->getTargetEntity()); |
|
259 | 12 | $onConditions = $this->getOnConditionSQL($association); |
|
260 | 12 | $whereClauses = $params = $types = []; |
|
261 | |||
262 | 12 | if (! $association->isOwningSide()) { |
|
263 | 1 | $association = $targetClass->getProperty($association->getMappedBy()); |
|
264 | 1 | $joinColumns = $association->getJoinTable()->getInverseJoinColumns(); |
|
265 | } else { |
||
266 | 11 | $joinColumns = $association->getJoinTable()->getJoinColumns(); |
|
267 | } |
||
268 | |||
269 | 12 | foreach ($joinColumns as $joinColumn) { |
|
270 | /** @var JoinColumnMetadata $joinColumn */ |
||
271 | 12 | $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName()); |
|
272 | 12 | $referencedColumnName = $joinColumn->getReferencedColumnName(); |
|
273 | |||
274 | 12 | if (! $joinColumn->getType()) { |
|
275 | $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $ownerMetadata, $this->em)); |
||
276 | } |
||
277 | |||
278 | 12 | $whereClauses[] = sprintf('t.%s = ?', $quotedColumnName); |
|
279 | 12 | $params[] = $identifier[$ownerMetadata->fieldNames[$referencedColumnName]]; |
|
280 | 12 | $types[] = $joinColumn->getType(); |
|
281 | } |
||
282 | |||
283 | 12 | $parameters = $this->expandCriteriaParameters($criteria); |
|
284 | |||
285 | 12 | foreach ($parameters as $parameter) { |
|
286 | 7 | [$name, $value, $operator] = $parameter; |
|
287 | |||
288 | 7 | $property = $targetClass->getProperty($name); |
|
289 | 7 | $columnName = $this->platform->quoteIdentifier($property->getColumnName()); |
|
290 | |||
291 | 7 | $whereClauses[] = sprintf('te.%s %s ?', $columnName, $operator); |
|
292 | 7 | $params[] = $value; |
|
293 | 7 | $types[] = $property->getType(); |
|
294 | } |
||
295 | |||
296 | 12 | $tableName = $targetClass->table->getQuotedQualifiedName($this->platform); |
|
297 | 12 | $joinTableName = $association->getJoinTable()->getQuotedQualifiedName($this->platform); |
|
298 | 12 | $resultSetMapping = new Query\ResultSetMappingBuilder($this->em); |
|
299 | |||
300 | 12 | $resultSetMapping->addRootEntityFromClassMetadata($targetClass->getClassName(), 'te'); |
|
301 | |||
302 | 12 | $sql = 'SELECT ' . $resultSetMapping->generateSelectClause() |
|
303 | 12 | . ' FROM ' . $tableName . ' te' |
|
304 | 12 | . ' JOIN ' . $joinTableName . ' t ON' |
|
305 | 12 | . implode(' AND ', $onConditions) |
|
306 | 12 | . ' WHERE ' . implode(' AND ', $whereClauses); |
|
307 | |||
308 | 12 | $sql .= $this->getOrderingSql($criteria, $targetClass); |
|
309 | 12 | $sql .= $this->getLimitSql($criteria); |
|
310 | |||
311 | 12 | $stmt = $this->conn->executeQuery($sql, $params, $types); |
|
312 | |||
313 | 12 | return $this->em->newHydrator(Query::HYDRATE_OBJECT)->hydrateAll($stmt, $resultSetMapping); |
|
314 | } |
||
315 | |||
316 | /** |
||
317 | * Generates the filter SQL for a given mapping. |
||
318 | * |
||
319 | * This method is not used for actually grabbing the related entities |
||
320 | * but when the extra-lazy collection methods are called on a filtered |
||
321 | * association. This is why besides the many to many table we also |
||
322 | * have to join in the actual entities table leading to additional |
||
323 | * JOIN. |
||
324 | * |
||
325 | * @return string[] ordered tuple: |
||
326 | * - JOIN condition to add to the SQL |
||
327 | * - WHERE condition to add to the SQL |
||
328 | */ |
||
329 | 32 | public function getFilterSql(ManyToManyAssociationMetadata $association) |
|
330 | { |
||
331 | 32 | $targetClass = $this->em->getClassMetadata($association->getTargetEntity()); |
|
332 | 32 | $rootClass = $this->em->getClassMetadata($targetClass->getRootClassName()); |
|
0 ignored issues
–
show
|
|||
333 | 32 | $filterSql = $this->generateFilterConditionSQL($rootClass, 'te'); |
|
334 | |||
335 | 32 | if ($filterSql === '') { |
|
336 | 32 | return ['', '']; |
|
337 | } |
||
338 | |||
339 | // A join is needed if there is filtering on the target entity |
||
340 | 6 | $tableName = $rootClass->table->getQuotedQualifiedName($this->platform); |
|
0 ignored issues
–
show
|
|||
341 | 6 | $joinSql = ' JOIN ' . $tableName . ' te' |
|
342 | 6 | . ' ON' . implode(' AND ', $this->getOnConditionSQL($association)); |
|
343 | |||
344 | 6 | return [$joinSql, $filterSql]; |
|
345 | } |
||
346 | |||
347 | /** |
||
348 | * Generates the filter SQL for a given entity and table alias. |
||
349 | * |
||
350 | * @param ClassMetadata $targetEntity Metadata of the target entity. |
||
351 | * @param string $targetTableAlias The table alias of the joined/selected table. |
||
352 | * |
||
353 | * @return string The SQL query part to add to a query. |
||
354 | */ |
||
355 | 32 | protected function generateFilterConditionSQL(ClassMetadata $targetEntity, $targetTableAlias) |
|
356 | { |
||
357 | 32 | $filterClauses = []; |
|
358 | |||
359 | 32 | foreach ($this->em->getFilters()->getEnabledFilters() as $filter) { |
|
360 | 6 | $filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias); |
|
361 | |||
362 | 6 | if ($filterExpr) { |
|
363 | 6 | $filterClauses[] = '(' . $filterExpr . ')'; |
|
364 | } |
||
365 | } |
||
366 | |||
367 | 32 | if (! $filterClauses) { |
|
368 | 32 | return ''; |
|
369 | } |
||
370 | |||
371 | 6 | $filterSql = implode(' AND ', $filterClauses); |
|
372 | |||
373 | 6 | return isset($filterClauses[1]) |
|
374 | ? '(' . $filterSql . ')' |
||
375 | 6 | : $filterSql |
|
376 | ; |
||
377 | } |
||
378 | |||
379 | /** |
||
380 | * Generate ON condition |
||
381 | * |
||
382 | * @return string[] |
||
383 | */ |
||
384 | 18 | protected function getOnConditionSQL(ManyToManyAssociationMetadata $association) |
|
385 | { |
||
386 | 18 | $targetClass = $this->em->getClassMetadata($association->getTargetEntity()); |
|
387 | 18 | $owningAssociation = ! $association->isOwningSide() |
|
388 | 3 | ? $targetClass->getProperty($association->getMappedBy()) |
|
389 | 18 | : $association; |
|
390 | |||
391 | 18 | $joinTable = $owningAssociation->getJoinTable(); |
|
392 | 18 | $joinColumns = $association->isOwningSide() |
|
393 | 15 | ? $joinTable->getInverseJoinColumns() |
|
394 | 18 | : $joinTable->getJoinColumns() |
|
395 | ; |
||
396 | |||
397 | 18 | $conditions = []; |
|
398 | |||
399 | 18 | foreach ($joinColumns as $joinColumn) { |
|
400 | 18 | $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName()); |
|
401 | 18 | $quotedReferencedColumnName = $this->platform->quoteIdentifier($joinColumn->getReferencedColumnName()); |
|
402 | |||
403 | 18 | $conditions[] = ' t.' . $quotedColumnName . ' = te.' . $quotedReferencedColumnName; |
|
404 | } |
||
405 | |||
406 | 18 | return $conditions; |
|
407 | } |
||
408 | |||
409 | /** |
||
410 | * {@inheritdoc} |
||
411 | * |
||
412 | * @override |
||
413 | */ |
||
414 | 18 | protected function getDeleteSQL(PersistentCollection $collection) |
|
415 | { |
||
416 | 18 | $association = $collection->getMapping(); |
|
417 | 18 | $joinTable = $association->getJoinTable(); |
|
418 | 18 | $joinTableName = $joinTable->getQuotedQualifiedName($this->platform); |
|
419 | 18 | $columns = []; |
|
420 | |||
421 | 18 | foreach ($joinTable->getJoinColumns() as $joinColumn) { |
|
422 | 18 | $columns[] = $this->platform->quoteIdentifier($joinColumn->getColumnName()); |
|
423 | } |
||
424 | |||
425 | 18 | return 'DELETE FROM ' . $joinTableName . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?'; |
|
426 | } |
||
427 | |||
428 | /** |
||
429 | * {@inheritdoc} |
||
430 | * |
||
431 | * {@internal Order of the parameters must be the same as the order of the columns in getDeleteSql. }} |
||
432 | * |
||
433 | * @override |
||
434 | */ |
||
435 | 18 | protected function getDeleteSQLParameters(PersistentCollection $collection) |
|
436 | { |
||
437 | 18 | $association = $collection->getMapping(); |
|
438 | 18 | $identifier = $this->uow->getEntityIdentifier($collection->getOwner()); |
|
439 | 18 | $joinTable = $association->getJoinTable(); |
|
440 | 18 | $joinColumns = $joinTable->getJoinColumns(); |
|
441 | |||
442 | // Optimization for single column identifier |
||
443 | 18 | if (count($joinColumns) === 1) { |
|
444 | 15 | return [reset($identifier)]; |
|
445 | } |
||
446 | |||
447 | // Composite identifier |
||
448 | 3 | $sourceClass = $this->em->getClassMetadata($association->getSourceEntity()); |
|
449 | 3 | $params = []; |
|
450 | |||
451 | 3 | foreach ($joinColumns as $joinColumn) { |
|
452 | 3 | $params[] = $identifier[$sourceClass->fieldNames[$joinColumn->getReferencedColumnName()]]; |
|
453 | } |
||
454 | |||
455 | 3 | return $params; |
|
456 | } |
||
457 | |||
458 | /** |
||
459 | * Gets the SQL statement used for deleting a row from the collection. |
||
460 | * |
||
461 | * @return string[]|string[][] ordered tuple containing the SQL to be executed and an array |
||
462 | * of types for bound parameters |
||
463 | */ |
||
464 | 328 | protected function getDeleteRowSQL(PersistentCollection $collection) |
|
465 | { |
||
466 | 328 | $association = $collection->getMapping(); |
|
467 | 328 | $class = $this->em->getClassMetadata($association->getSourceEntity()); |
|
468 | 328 | $targetClass = $this->em->getClassMetadata($association->getTargetEntity()); |
|
469 | 328 | $columns = []; |
|
470 | 328 | $types = []; |
|
471 | |||
472 | 328 | $joinTable = $association->getJoinTable(); |
|
473 | 328 | $joinTableName = $joinTable->getQuotedQualifiedName($this->platform); |
|
474 | |||
475 | 328 | foreach ($joinTable->getJoinColumns() as $joinColumn) { |
|
476 | /** @var JoinColumnMetadata $joinColumn */ |
||
477 | 328 | $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName()); |
|
478 | 328 | $referencedColumnName = $joinColumn->getReferencedColumnName(); |
|
479 | |||
480 | 328 | if (! $joinColumn->getType()) { |
|
481 | 35 | $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $class, $this->em)); |
|
482 | } |
||
483 | |||
484 | 328 | $columns[] = $quotedColumnName; |
|
485 | 328 | $types[] = $joinColumn->getType(); |
|
486 | } |
||
487 | |||
488 | 328 | foreach ($joinTable->getInverseJoinColumns() as $joinColumn) { |
|
489 | /** @var JoinColumnMetadata $joinColumn */ |
||
490 | 328 | $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName()); |
|
491 | 328 | $referencedColumnName = $joinColumn->getReferencedColumnName(); |
|
492 | |||
493 | 328 | if (! $joinColumn->getType()) { |
|
494 | 35 | $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em)); |
|
495 | } |
||
496 | |||
497 | 328 | $columns[] = $quotedColumnName; |
|
498 | 328 | $types[] = $joinColumn->getType(); |
|
499 | } |
||
500 | |||
501 | return [ |
||
502 | 328 | sprintf('DELETE FROM %s WHERE %s = ?', $joinTableName, implode(' = ? AND ', $columns)), |
|
503 | 328 | $types, |
|
504 | ]; |
||
505 | } |
||
506 | |||
507 | /** |
||
508 | * Gets the SQL parameters for the corresponding SQL statement to delete the given |
||
509 | * element from the given collection. |
||
510 | * |
||
511 | * {@internal Order of the parameters must be the same as the order of the columns in getDeleteRowSql. }} |
||
512 | * |
||
513 | * @param mixed $element |
||
514 | * |
||
515 | * @return mixed[] |
||
516 | */ |
||
517 | 10 | protected function getDeleteRowSQLParameters(PersistentCollection $collection, $element) |
|
518 | { |
||
519 | 10 | return $this->collectJoinTableColumnParameters($collection, $element); |
|
520 | } |
||
521 | |||
522 | /** |
||
523 | * Gets the SQL statement used for inserting a row in the collection. |
||
524 | * |
||
525 | * @return string[]|string[][] ordered tuple containing the SQL to be executed and an array |
||
526 | * of types for bound parameters |
||
527 | */ |
||
528 | 328 | protected function getInsertRowSQL(PersistentCollection $collection) |
|
529 | { |
||
530 | 328 | $association = $collection->getMapping(); |
|
531 | 328 | $class = $this->em->getClassMetadata($association->getSourceEntity()); |
|
532 | 328 | $targetClass = $this->em->getClassMetadata($association->getTargetEntity()); |
|
533 | 328 | $columns = []; |
|
534 | 328 | $types = []; |
|
535 | |||
536 | 328 | $joinTable = $association->getJoinTable(); |
|
537 | 328 | $joinTableName = $joinTable->getQuotedQualifiedName($this->platform); |
|
538 | |||
539 | 328 | foreach ($joinTable->getJoinColumns() as $joinColumn) { |
|
540 | /** @var JoinColumnMetadata $joinColumn */ |
||
541 | 328 | $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName()); |
|
542 | 328 | $referencedColumnName = $joinColumn->getReferencedColumnName(); |
|
543 | |||
544 | 328 | if (! $joinColumn->getType()) { |
|
545 | $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $class, $this->em)); |
||
546 | } |
||
547 | |||
548 | 328 | $columns[] = $quotedColumnName; |
|
549 | 328 | $types[] = $joinColumn->getType(); |
|
550 | } |
||
551 | |||
552 | 328 | foreach ($joinTable->getInverseJoinColumns() as $joinColumn) { |
|
553 | /** @var JoinColumnMetadata $joinColumn */ |
||
554 | 328 | $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName()); |
|
555 | 328 | $referencedColumnName = $joinColumn->getReferencedColumnName(); |
|
556 | |||
557 | 328 | if (! $joinColumn->getType()) { |
|
558 | $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em)); |
||
559 | } |
||
560 | |||
561 | 328 | $columns[] = $quotedColumnName; |
|
562 | 328 | $types[] = $joinColumn->getType(); |
|
563 | } |
||
564 | |||
565 | 328 | $columnNamesAsString = implode(', ', $columns); |
|
566 | 328 | $columnValuesAsString = implode(', ', array_fill(0, count($columns), '?')); |
|
567 | |||
568 | return [ |
||
569 | 328 | sprintf('INSERT INTO %s (%s) VALUES (%s)', $joinTableName, $columnNamesAsString, $columnValuesAsString), |
|
570 | 328 | $types, |
|
571 | ]; |
||
572 | } |
||
573 | |||
574 | /** |
||
575 | * Gets the SQL parameters for the corresponding SQL statement to insert the given |
||
576 | * element of the given collection into the database. |
||
577 | * |
||
578 | * {@internal Order of the parameters must be the same as the order of the columns in getInsertRowSql. }} |
||
579 | * |
||
580 | * @param mixed $element |
||
581 | * |
||
582 | * @return mixed[] |
||
583 | */ |
||
584 | 328 | protected function getInsertRowSQLParameters(PersistentCollection $collection, $element) |
|
585 | { |
||
586 | 328 | return $this->collectJoinTableColumnParameters($collection, $element); |
|
587 | } |
||
588 | |||
589 | /** |
||
590 | * Collects the parameters for inserting/deleting on the join table in the order |
||
591 | * of the join table columns. |
||
592 | * |
||
593 | * @param object $element |
||
594 | * |
||
595 | * @return mixed[] |
||
596 | */ |
||
597 | 328 | private function collectJoinTableColumnParameters(PersistentCollection $collection, $element) |
|
598 | { |
||
599 | 328 | $params = []; |
|
600 | 328 | $association = $collection->getMapping(); |
|
601 | 328 | $owningClass = $this->em->getClassMetadata(get_class($collection->getOwner())); |
|
602 | 328 | $targetClass = $collection->getTypeClass(); |
|
603 | 328 | $owningIdentifier = $this->uow->getEntityIdentifier($collection->getOwner()); |
|
604 | 328 | $targetIdentifier = $this->uow->getEntityIdentifier($element); |
|
605 | 328 | $joinTable = $association->getJoinTable(); |
|
606 | |||
607 | 328 | foreach ($joinTable->getJoinColumns() as $joinColumn) { |
|
608 | 328 | $fieldName = $owningClass->fieldNames[$joinColumn->getReferencedColumnName()]; |
|
609 | |||
610 | 328 | $params[] = $owningIdentifier[$fieldName]; |
|
611 | } |
||
612 | |||
613 | 328 | foreach ($joinTable->getInverseJoinColumns() as $joinColumn) { |
|
614 | 328 | $fieldName = $targetClass->fieldNames[$joinColumn->getReferencedColumnName()]; |
|
615 | |||
616 | 328 | $params[] = $targetIdentifier[$fieldName]; |
|
617 | } |
||
618 | |||
619 | 328 | return $params; |
|
620 | } |
||
621 | |||
622 | /** |
||
623 | * @param string $key |
||
624 | * @param bool $addFilters Whether the filter SQL should be included or not. |
||
625 | * |
||
626 | * @return mixed[] ordered vector: |
||
627 | * - quoted join table name |
||
628 | * - where clauses to be added for filtering |
||
629 | * - parameters to be bound for filtering |
||
630 | * - types of the parameters to be bound for filtering |
||
631 | */ |
||
632 | 7 | private function getJoinTableRestrictionsWithKey(PersistentCollection $collection, $key, $addFilters) |
|
633 | { |
||
634 | 7 | $association = $collection->getMapping(); |
|
635 | 7 | $owningAssociation = $association; |
|
636 | 7 | $indexBy = $owningAssociation->getIndexedBy(); |
|
637 | 7 | $identifier = $this->uow->getEntityIdentifier($collection->getOwner()); |
|
638 | 7 | $sourceClass = $this->em->getClassMetadata($owningAssociation->getSourceEntity()); |
|
639 | 7 | $targetClass = $this->em->getClassMetadata($owningAssociation->getTargetEntity()); |
|
640 | |||
641 | 7 | if (! $owningAssociation->isOwningSide()) { |
|
642 | 3 | $owningAssociation = $targetClass->getProperty($owningAssociation->getMappedBy()); |
|
643 | 3 | $joinTable = $owningAssociation->getJoinTable(); |
|
644 | 3 | $joinColumns = $joinTable->getJoinColumns(); |
|
645 | 3 | $inverseJoinColumns = $joinTable->getInverseJoinColumns(); |
|
646 | } else { |
||
647 | 4 | $joinTable = $owningAssociation->getJoinTable(); |
|
648 | 4 | $joinColumns = $joinTable->getInverseJoinColumns(); |
|
649 | 4 | $inverseJoinColumns = $joinTable->getJoinColumns(); |
|
650 | } |
||
651 | |||
652 | 7 | $joinTableName = $joinTable->getQuotedQualifiedName($this->platform); |
|
653 | 7 | $quotedJoinTable = $joinTableName . ' t'; |
|
654 | 7 | $whereClauses = []; |
|
655 | 7 | $params = []; |
|
656 | 7 | $types = []; |
|
657 | 7 | $joinNeeded = ! in_array($indexBy, $targetClass->identifier, true); |
|
658 | |||
659 | 7 | if ($joinNeeded) { // extra join needed if indexBy is not a @id |
|
660 | 3 | $joinConditions = []; |
|
661 | |||
662 | 3 | foreach ($joinColumns as $joinColumn) { |
|
663 | /** @var JoinColumnMetadata $joinColumn */ |
||
664 | 3 | $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName()); |
|
665 | 3 | $quotedReferencedColumnName = $this->platform->quoteIdentifier($joinColumn->getReferencedColumnName()); |
|
666 | |||
667 | 3 | $joinConditions[] = ' t.' . $quotedColumnName . ' = tr.' . $quotedReferencedColumnName; |
|
668 | } |
||
669 | |||
670 | 3 | $tableName = $targetClass->table->getQuotedQualifiedName($this->platform); |
|
671 | 3 | $quotedJoinTable .= ' JOIN ' . $tableName . ' tr ON ' . implode(' AND ', $joinConditions); |
|
672 | 3 | $indexByProperty = $targetClass->getProperty($indexBy); |
|
673 | |||
674 | switch (true) { |
||
675 | 3 | case ($indexByProperty instanceof FieldMetadata): |
|
676 | 3 | $quotedColumnName = $this->platform->quoteIdentifier($indexByProperty->getColumnName()); |
|
677 | |||
678 | 3 | $whereClauses[] = sprintf('tr.%s = ?', $quotedColumnName); |
|
679 | 3 | $params[] = $key; |
|
680 | 3 | $types[] = $indexByProperty->getType(); |
|
681 | 3 | break; |
|
682 | |||
683 | case ($indexByProperty instanceof ToOneAssociationMetadata && $indexByProperty->isOwningSide()): |
||
684 | // Cannot be supported because PHP does not accept objects as keys. =( |
||
685 | break; |
||
686 | } |
||
687 | } |
||
688 | |||
689 | 7 | foreach ($inverseJoinColumns as $joinColumn) { |
|
690 | /** @var JoinColumnMetadata $joinColumn */ |
||
691 | 7 | $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName()); |
|
692 | 7 | $referencedColumnName = $joinColumn->getReferencedColumnName(); |
|
693 | |||
694 | 7 | if (! $joinColumn->getType()) { |
|
695 | $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $sourceClass, $this->em)); |
||
696 | } |
||
697 | |||
698 | 7 | $whereClauses[] = sprintf('t.%s = ?', $quotedColumnName); |
|
699 | 7 | $params[] = $identifier[$sourceClass->fieldNames[$joinColumn->getReferencedColumnName()]]; |
|
700 | 7 | $types[] = $joinColumn->getType(); |
|
701 | } |
||
702 | |||
703 | 7 | if (! $joinNeeded) { |
|
704 | 4 | foreach ($joinColumns as $joinColumn) { |
|
705 | /** @var JoinColumnMetadata $joinColumn */ |
||
706 | 4 | $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName()); |
|
707 | 4 | $referencedColumnName = $joinColumn->getReferencedColumnName(); |
|
708 | |||
709 | 4 | if (! $joinColumn->getType()) { |
|
710 | $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em)); |
||
711 | } |
||
712 | |||
713 | 4 | $whereClauses[] = sprintf('t.%s = ?', $quotedColumnName); |
|
714 | 4 | $params[] = $key; |
|
715 | 4 | $types[] = $joinColumn->getType(); |
|
716 | } |
||
717 | } |
||
718 | |||
719 | 7 | if ($addFilters) { |
|
720 | 7 | list($joinTargetEntitySQL, $filterSql) = $this->getFilterSql($association); |
|
721 | |||
722 | 7 | if ($filterSql) { |
|
723 | $quotedJoinTable .= ' ' . $joinTargetEntitySQL; |
||
724 | $whereClauses[] = $filterSql; |
||
725 | } |
||
726 | } |
||
727 | |||
728 | 7 | return [$quotedJoinTable, $whereClauses, $params, $types]; |
|
729 | } |
||
730 | |||
731 | /** |
||
732 | * @param object $element |
||
733 | * @param bool $addFilters Whether the filter SQL should be included or not. |
||
734 | * |
||
735 | * @return mixed[] ordered vector: |
||
736 | * - quoted join table name |
||
737 | * - where clauses to be added for filtering |
||
738 | * - parameters to be bound for filtering |
||
739 | * - types of the parameters to be bound for filtering |
||
740 | */ |
||
741 | 9 | private function getJoinTableRestrictions(PersistentCollection $collection, $element, $addFilters) |
|
742 | { |
||
743 | 9 | $association = $collection->getMapping(); |
|
744 | 9 | $owningAssociation = $association; |
|
745 | |||
746 | 9 | if (! $association->isOwningSide()) { |
|
747 | 4 | $sourceClass = $this->em->getClassMetadata($association->getTargetEntity()); |
|
748 | 4 | $targetClass = $this->em->getClassMetadata($association->getSourceEntity()); |
|
749 | 4 | $sourceIdentifier = $this->uow->getEntityIdentifier($element); |
|
750 | 4 | $targetIdentifier = $this->uow->getEntityIdentifier($collection->getOwner()); |
|
751 | |||
752 | 4 | $owningAssociation = $sourceClass->getProperty($association->getMappedBy()); |
|
753 | } else { |
||
754 | 5 | $sourceClass = $this->em->getClassMetadata($association->getSourceEntity()); |
|
755 | 5 | $targetClass = $this->em->getClassMetadata($association->getTargetEntity()); |
|
756 | 5 | $sourceIdentifier = $this->uow->getEntityIdentifier($collection->getOwner()); |
|
757 | 5 | $targetIdentifier = $this->uow->getEntityIdentifier($element); |
|
758 | } |
||
759 | |||
760 | 9 | $joinTable = $owningAssociation->getJoinTable(); |
|
761 | 9 | $joinTableName = $joinTable->getQuotedQualifiedName($this->platform); |
|
762 | 9 | $quotedJoinTable = $joinTableName; |
|
763 | 9 | $whereClauses = []; |
|
764 | 9 | $params = []; |
|
765 | 9 | $types = []; |
|
766 | |||
767 | 9 | foreach ($joinTable->getJoinColumns() as $joinColumn) { |
|
768 | /** @var JoinColumnMetadata $joinColumn */ |
||
769 | 9 | $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName()); |
|
770 | 9 | $referencedColumnName = $joinColumn->getReferencedColumnName(); |
|
771 | |||
772 | 9 | if (! $joinColumn->getType()) { |
|
773 | $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $sourceClass, $this->em)); |
||
774 | } |
||
775 | |||
776 | 9 | $whereClauses[] = ($addFilters ? 't.' : '') . $quotedColumnName . ' = ?'; |
|
777 | 9 | $params[] = $sourceIdentifier[$sourceClass->fieldNames[$referencedColumnName]]; |
|
778 | 9 | $types[] = $joinColumn->getType(); |
|
779 | } |
||
780 | |||
781 | 9 | foreach ($joinTable->getInverseJoinColumns() as $joinColumn) { |
|
782 | /** @var JoinColumnMetadata $joinColumn */ |
||
783 | 9 | $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName()); |
|
784 | 9 | $referencedColumnName = $joinColumn->getReferencedColumnName(); |
|
785 | |||
786 | 9 | if (! $joinColumn->getType()) { |
|
787 | $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em)); |
||
788 | } |
||
789 | |||
790 | 9 | $whereClauses[] = ($addFilters ? 't.' : '') . $quotedColumnName . ' = ?'; |
|
791 | 9 | $params[] = $targetIdentifier[$targetClass->fieldNames[$referencedColumnName]]; |
|
792 | 9 | $types[] = $joinColumn->getType(); |
|
793 | } |
||
794 | |||
795 | 9 | if ($addFilters) { |
|
796 | 7 | $quotedJoinTable .= ' t'; |
|
797 | |||
798 | 7 | list($joinTargetEntitySQL, $filterSql) = $this->getFilterSql($association); |
|
799 | |||
800 | 7 | if ($filterSql) { |
|
801 | 3 | $quotedJoinTable .= ' ' . $joinTargetEntitySQL; |
|
802 | 3 | $whereClauses[] = $filterSql; |
|
803 | } |
||
804 | } |
||
805 | |||
806 | 9 | return [$quotedJoinTable, $whereClauses, $params, $types]; |
|
807 | } |
||
808 | |||
809 | /** |
||
810 | * Expands Criteria Parameters by walking the expressions and grabbing all |
||
811 | * parameters and types from it. |
||
812 | * |
||
813 | * @return mixed[] |
||
814 | */ |
||
815 | 12 | private function expandCriteriaParameters(Criteria $criteria) |
|
816 | { |
||
817 | 12 | $expression = $criteria->getWhereExpression(); |
|
818 | |||
819 | 12 | if ($expression === null) { |
|
820 | 5 | return []; |
|
821 | } |
||
822 | |||
823 | 7 | $valueVisitor = new SqlValueVisitor(); |
|
824 | |||
825 | 7 | $valueVisitor->dispatch($expression); |
|
826 | |||
827 | 7 | list(, $types) = $valueVisitor->getParamsAndTypes(); |
|
828 | |||
829 | 7 | return $types; |
|
830 | } |
||
831 | |||
832 | /** |
||
833 | * @return string |
||
834 | */ |
||
835 | 12 | private function getOrderingSql(Criteria $criteria, ClassMetadata $targetClass) |
|
836 | { |
||
837 | 12 | $orderings = $criteria->getOrderings(); |
|
838 | |||
839 | 12 | if ($orderings) { |
|
840 | 3 | $orderBy = []; |
|
841 | |||
842 | 3 | foreach ($orderings as $name => $direction) { |
|
843 | 3 | $property = $targetClass->getProperty($name); |
|
844 | 3 | $columnName = $this->platform->quoteIdentifier($property->getColumnName()); |
|
845 | |||
846 | 3 | $orderBy[] = $columnName . ' ' . $direction; |
|
847 | } |
||
848 | |||
849 | 3 | return ' ORDER BY ' . implode(', ', $orderBy); |
|
850 | } |
||
851 | 9 | return ''; |
|
852 | } |
||
853 | |||
854 | /** |
||
855 | * @return string |
||
856 | * @throws DBALException |
||
857 | */ |
||
858 | 12 | private function getLimitSql(Criteria $criteria) |
|
859 | { |
||
860 | 12 | $limit = $criteria->getMaxResults(); |
|
861 | 12 | $offset = $criteria->getFirstResult(); |
|
862 | 12 | if ($limit !== null || $offset !== null) { |
|
863 | 3 | return $this->platform->modifyLimitQuery('', $limit, $offset); |
|
864 | } |
||
865 | 9 | return ''; |
|
866 | } |
||
867 | } |
||
868 |
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.
This is most likely a typographical error or the method has been renamed.