1 | <?php |
||
2 | /* |
||
3 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||
4 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
||
5 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
||
6 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
||
7 | * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||
8 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
||
9 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
||
10 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
||
11 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
||
12 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
||
13 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||
14 | * |
||
15 | * This software consists of voluntary contributions made by many individuals |
||
16 | * and is licensed under the MIT license. For more information, see |
||
17 | * <http://www.doctrine-project.org>. |
||
18 | */ |
||
19 | |||
20 | namespace Doctrine\ORM\Persisters\Collection; |
||
21 | |||
22 | use Doctrine\Common\Collections\Criteria; |
||
23 | use Doctrine\ORM\Mapping\ClassMetadata; |
||
24 | use Doctrine\ORM\Persisters\SqlValueVisitor; |
||
25 | use Doctrine\ORM\PersistentCollection; |
||
26 | use Doctrine\ORM\Query; |
||
27 | use Doctrine\ORM\Utility\PersisterHelper; |
||
28 | |||
29 | /** |
||
30 | * Persister for many-to-many collections. |
||
31 | * |
||
32 | * @author Roman Borschel <[email protected]> |
||
33 | * @author Guilherme Blanco <[email protected]> |
||
34 | * @author Alexander <[email protected]> |
||
35 | * @since 2.0 |
||
36 | */ |
||
37 | class ManyToManyPersister extends AbstractCollectionPersister |
||
38 | { |
||
39 | /** |
||
40 | * {@inheritdoc} |
||
41 | */ |
||
42 | 18 | public function delete(PersistentCollection $collection) |
|
43 | { |
||
44 | 18 | $mapping = $collection->getMapping(); |
|
45 | |||
46 | 18 | if ( ! $mapping['isOwningSide']) { |
|
47 | return; // ignore inverse side |
||
48 | } |
||
49 | |||
50 | 18 | $types = []; |
|
51 | 18 | $class = $this->em->getClassMetadata($mapping['sourceEntity']); |
|
52 | |||
53 | 18 | foreach ($mapping['joinTable']['joinColumns'] as $joinColumn) { |
|
54 | 18 | $types[] = PersisterHelper::getTypeOfColumn($joinColumn['referencedColumnName'], $class, $this->em); |
|
55 | } |
||
56 | |||
57 | 18 | $this->conn->executeUpdate($this->getDeleteSQL($collection), $this->getDeleteSQLParameters($collection), $types); |
|
58 | 18 | } |
|
59 | |||
60 | /** |
||
61 | * {@inheritdoc} |
||
62 | */ |
||
63 | 335 | public function update(PersistentCollection $collection) |
|
64 | { |
||
65 | 335 | $mapping = $collection->getMapping(); |
|
66 | |||
67 | 335 | if ( ! $mapping['isOwningSide']) { |
|
68 | 237 | return; // ignore inverse side |
|
69 | } |
||
70 | |||
71 | 334 | list($deleteSql, $deleteTypes) = $this->getDeleteRowSQL($collection); |
|
72 | 334 | list($insertSql, $insertTypes) = $this->getInsertRowSQL($collection); |
|
73 | |||
74 | 334 | foreach ($collection->getDeleteDiff() as $element) { |
|
75 | 12 | $this->conn->executeUpdate( |
|
76 | 12 | $deleteSql, |
|
77 | 12 | $this->getDeleteRowSQLParameters($collection, $element), |
|
78 | 12 | $deleteTypes |
|
79 | ); |
||
80 | } |
||
81 | |||
82 | 334 | foreach ($collection->getInsertDiff() as $element) { |
|
83 | 334 | $this->conn->executeUpdate( |
|
84 | 334 | $insertSql, |
|
85 | 334 | $this->getInsertRowSQLParameters($collection, $element), |
|
86 | 334 | $insertTypes |
|
87 | ); |
||
88 | } |
||
89 | 334 | } |
|
90 | |||
91 | /** |
||
92 | * {@inheritdoc} |
||
93 | */ |
||
94 | 3 | public function get(PersistentCollection $collection, $index) |
|
95 | { |
||
96 | 3 | $mapping = $collection->getMapping(); |
|
97 | |||
98 | 3 | if ( ! isset($mapping['indexBy'])) { |
|
99 | throw new \BadMethodCallException("Selecting a collection by index is only supported on indexed collections."); |
||
100 | } |
||
101 | |||
102 | 3 | $persister = $this->uow->getEntityPersister($mapping['targetEntity']); |
|
103 | 3 | $mappedKey = $mapping['isOwningSide'] |
|
104 | 2 | ? $mapping['inversedBy'] |
|
105 | 3 | : $mapping['mappedBy']; |
|
106 | |||
107 | 3 | return $persister->load([$mappedKey => $collection->getOwner(), $mapping['indexBy'] => $index], null, $mapping, [], 0, 1); |
|
108 | } |
||
109 | |||
110 | /** |
||
111 | * {@inheritdoc} |
||
112 | */ |
||
113 | 18 | public function count(PersistentCollection $collection) |
|
114 | { |
||
115 | 18 | $conditions = []; |
|
116 | 18 | $params = []; |
|
117 | 18 | $types = []; |
|
118 | 18 | $mapping = $collection->getMapping(); |
|
119 | 18 | $id = $this->uow->getEntityIdentifier($collection->getOwner()); |
|
120 | 18 | $sourceClass = $this->em->getClassMetadata($mapping['sourceEntity']); |
|
121 | 18 | $targetClass = $this->em->getClassMetadata($mapping['targetEntity']); |
|
122 | 18 | $association = ( ! $mapping['isOwningSide']) |
|
123 | 4 | ? $targetClass->associationMappings[$mapping['mappedBy']] |
|
124 | 18 | : $mapping; |
|
125 | |||
126 | 18 | $joinTableName = $this->quoteStrategy->getJoinTableName($association, $sourceClass, $this->platform); |
|
127 | 18 | $joinColumns = ( ! $mapping['isOwningSide']) |
|
128 | 4 | ? $association['joinTable']['inverseJoinColumns'] |
|
129 | 18 | : $association['joinTable']['joinColumns']; |
|
130 | |||
131 | 18 | foreach ($joinColumns as $joinColumn) { |
|
132 | 18 | $columnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $sourceClass, $this->platform); |
|
133 | 18 | $referencedName = $joinColumn['referencedColumnName']; |
|
134 | 18 | $conditions[] = 't.' . $columnName . ' = ?'; |
|
135 | 18 | $params[] = $id[$sourceClass->getFieldForColumn($referencedName)]; |
|
136 | 18 | $types[] = PersisterHelper::getTypeOfColumn($referencedName, $sourceClass, $this->em); |
|
137 | } |
||
138 | |||
139 | 18 | list($joinTargetEntitySQL, $filterSql) = $this->getFilterSql($mapping); |
|
140 | |||
141 | 18 | if ($filterSql) { |
|
142 | 3 | $conditions[] = $filterSql; |
|
143 | } |
||
144 | |||
145 | // If there is a provided criteria, make part of conditions |
||
146 | // @todo Fix this. Current SQL returns something like: |
||
147 | // |
||
148 | /*if ($criteria && ($expression = $criteria->getWhereExpression()) !== null) { |
||
149 | // A join is needed on the target entity |
||
150 | $targetTableName = $this->quoteStrategy->getTableName($targetClass, $this->platform); |
||
151 | $targetJoinSql = ' JOIN ' . $targetTableName . ' te' |
||
152 | . ' ON' . implode(' AND ', $this->getOnConditionSQL($association)); |
||
153 | |||
154 | // And criteria conditions needs to be added |
||
155 | $persister = $this->uow->getEntityPersister($targetClass->name); |
||
156 | $visitor = new SqlExpressionVisitor($persister, $targetClass); |
||
157 | $conditions[] = $visitor->dispatch($expression); |
||
158 | |||
159 | $joinTargetEntitySQL = $targetJoinSql . $joinTargetEntitySQL; |
||
160 | }*/ |
||
161 | |||
162 | $sql = 'SELECT COUNT(*)' |
||
163 | 18 | . ' FROM ' . $joinTableName . ' t' |
|
164 | 18 | . $joinTargetEntitySQL |
|
165 | 18 | . ' WHERE ' . implode(' AND ', $conditions); |
|
166 | |||
167 | 18 | return $this->conn->fetchColumn($sql, $params, 0, $types); |
|
0 ignored issues
–
show
|
|||
168 | } |
||
169 | |||
170 | /** |
||
171 | * {@inheritDoc} |
||
172 | */ |
||
173 | 8 | public function slice(PersistentCollection $collection, $offset, $length = null) |
|
174 | { |
||
175 | 8 | $mapping = $collection->getMapping(); |
|
176 | 8 | $persister = $this->uow->getEntityPersister($mapping['targetEntity']); |
|
177 | |||
178 | 8 | return $persister->getManyToManyCollection($mapping, $collection->getOwner(), $offset, $length); |
|
179 | } |
||
180 | /** |
||
181 | * {@inheritdoc} |
||
182 | */ |
||
183 | 7 | public function containsKey(PersistentCollection $collection, $key) |
|
184 | { |
||
185 | 7 | $mapping = $collection->getMapping(); |
|
186 | |||
187 | 7 | if ( ! isset($mapping['indexBy'])) { |
|
188 | throw new \BadMethodCallException("Selecting a collection by index is only supported on indexed collections."); |
||
189 | } |
||
190 | |||
191 | 7 | list($quotedJoinTable, $whereClauses, $params, $types) = $this->getJoinTableRestrictionsWithKey($collection, $key, true); |
|
192 | |||
193 | 7 | $sql = 'SELECT 1 FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses); |
|
194 | |||
195 | 7 | return (bool) $this->conn->fetchColumn($sql, $params, 0, $types); |
|
196 | } |
||
197 | |||
198 | /** |
||
199 | * {@inheritDoc} |
||
200 | */ |
||
201 | 7 | public function contains(PersistentCollection $collection, $element) |
|
202 | { |
||
203 | 7 | if ( ! $this->isValidEntityState($element)) { |
|
204 | 2 | return false; |
|
205 | } |
||
206 | |||
207 | 7 | list($quotedJoinTable, $whereClauses, $params, $types) = $this->getJoinTableRestrictions($collection, $element, true); |
|
208 | |||
209 | 7 | $sql = 'SELECT 1 FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses); |
|
210 | |||
211 | 7 | return (bool) $this->conn->fetchColumn($sql, $params, 0, $types); |
|
212 | } |
||
213 | |||
214 | /** |
||
215 | * {@inheritDoc} |
||
216 | */ |
||
217 | 2 | public function removeElement(PersistentCollection $collection, $element) |
|
218 | { |
||
219 | 2 | if ( ! $this->isValidEntityState($element)) { |
|
220 | 2 | return false; |
|
221 | } |
||
222 | |||
223 | 2 | list($quotedJoinTable, $whereClauses, $params, $types) = $this->getJoinTableRestrictions($collection, $element, false); |
|
224 | |||
225 | 2 | $sql = 'DELETE FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses); |
|
226 | |||
227 | 2 | return (bool) $this->conn->executeUpdate($sql, $params, $types); |
|
228 | } |
||
229 | |||
230 | /** |
||
231 | * {@inheritDoc} |
||
232 | */ |
||
233 | 12 | public function loadCriteria(PersistentCollection $collection, Criteria $criteria) |
|
234 | { |
||
235 | 12 | $mapping = $collection->getMapping(); |
|
236 | 12 | $owner = $collection->getOwner(); |
|
237 | 12 | $ownerMetadata = $this->em->getClassMetadata(get_class($owner)); |
|
238 | 12 | $id = $this->uow->getEntityIdentifier($owner); |
|
239 | 12 | $targetClass = $this->em->getClassMetadata($mapping['targetEntity']); |
|
240 | 12 | $onConditions = $this->getOnConditionSQL($mapping); |
|
241 | 12 | $whereClauses = $params = []; |
|
242 | |||
243 | 12 | if ( ! $mapping['isOwningSide']) { |
|
244 | 1 | $associationSourceClass = $targetClass; |
|
245 | 1 | $mapping = $targetClass->associationMappings[$mapping['mappedBy']]; |
|
246 | 1 | $sourceRelationMode = 'relationToTargetKeyColumns'; |
|
247 | } else { |
||
248 | 11 | $associationSourceClass = $ownerMetadata; |
|
249 | 11 | $sourceRelationMode = 'relationToSourceKeyColumns'; |
|
250 | } |
||
251 | |||
252 | 12 | foreach ($mapping[$sourceRelationMode] as $key => $value) { |
|
253 | 12 | $whereClauses[] = sprintf('t.%s = ?', $key); |
|
254 | 12 | $params[] = $ownerMetadata->containsForeignIdentifier |
|
255 | ? $id[$ownerMetadata->getFieldForColumn($value)] |
||
256 | 12 | : $id[$ownerMetadata->fieldNames[$value]]; |
|
257 | } |
||
258 | |||
259 | 12 | $parameters = $this->expandCriteriaParameters($criteria); |
|
260 | |||
261 | 12 | foreach ($parameters as $parameter) { |
|
262 | 7 | [$name, $value, $operator] = $parameter; |
|
263 | |||
264 | 7 | $field = $this->quoteStrategy->getColumnName($name, $targetClass, $this->platform); |
|
265 | 7 | $whereClauses[] = sprintf('te.%s %s ?', $field, $operator); |
|
266 | 7 | $params[] = $value; |
|
267 | } |
||
268 | |||
269 | 12 | $tableName = $this->quoteStrategy->getTableName($targetClass, $this->platform); |
|
270 | 12 | $joinTable = $this->quoteStrategy->getJoinTableName($mapping, $associationSourceClass, $this->platform); |
|
271 | |||
272 | 12 | $rsm = new Query\ResultSetMappingBuilder($this->em); |
|
273 | 12 | $rsm->addRootEntityFromClassMetadata($targetClass->name, 'te'); |
|
274 | |||
275 | 12 | $sql = 'SELECT ' . $rsm->generateSelectClause() |
|
276 | 12 | . ' FROM ' . $tableName . ' te' |
|
277 | 12 | . ' JOIN ' . $joinTable . ' t ON' |
|
278 | 12 | . implode(' AND ', $onConditions) |
|
279 | 12 | . ' WHERE ' . implode(' AND ', $whereClauses); |
|
280 | |||
281 | 12 | $sql .= $this->getOrderingSql($criteria, $targetClass); |
|
282 | |||
283 | 12 | $sql .= $this->getLimitSql($criteria); |
|
284 | |||
285 | 12 | $stmt = $this->conn->executeQuery($sql, $params); |
|
286 | |||
287 | return $this |
||
288 | 12 | ->em |
|
289 | 12 | ->newHydrator(Query::HYDRATE_OBJECT) |
|
290 | 12 | ->hydrateAll($stmt, $rsm); |
|
291 | } |
||
292 | |||
293 | /** |
||
294 | * Generates the filter SQL for a given mapping. |
||
295 | * |
||
296 | * This method is not used for actually grabbing the related entities |
||
297 | * but when the extra-lazy collection methods are called on a filtered |
||
298 | * association. This is why besides the many to many table we also |
||
299 | * have to join in the actual entities table leading to additional |
||
300 | * JOIN. |
||
301 | * |
||
302 | * @param array $mapping Array containing mapping information. |
||
303 | * |
||
304 | * @return string[] ordered tuple: |
||
305 | * - JOIN condition to add to the SQL |
||
306 | * - WHERE condition to add to the SQL |
||
307 | */ |
||
308 | 32 | public function getFilterSql($mapping) |
|
309 | { |
||
310 | 32 | $targetClass = $this->em->getClassMetadata($mapping['targetEntity']); |
|
311 | 32 | $rootClass = $this->em->getClassMetadata($targetClass->rootEntityName); |
|
312 | 32 | $filterSql = $this->generateFilterConditionSQL($rootClass, 'te'); |
|
313 | |||
314 | 32 | if ('' === $filterSql) { |
|
315 | 32 | return ['', '']; |
|
316 | } |
||
317 | |||
318 | // A join is needed if there is filtering on the target entity |
||
319 | 6 | $tableName = $this->quoteStrategy->getTableName($rootClass, $this->platform); |
|
320 | 6 | $joinSql = ' JOIN ' . $tableName . ' te' |
|
321 | 6 | . ' ON' . implode(' AND ', $this->getOnConditionSQL($mapping)); |
|
322 | |||
323 | 6 | return [$joinSql, $filterSql]; |
|
324 | } |
||
325 | |||
326 | /** |
||
327 | * Generates the filter SQL for a given entity and table alias. |
||
328 | * |
||
329 | * @param ClassMetadata $targetEntity Metadata of the target entity. |
||
330 | * @param string $targetTableAlias The table alias of the joined/selected table. |
||
331 | * |
||
332 | * @return string The SQL query part to add to a query. |
||
333 | */ |
||
334 | 32 | protected function generateFilterConditionSQL(ClassMetadata $targetEntity, $targetTableAlias) |
|
335 | { |
||
336 | 32 | $filterClauses = []; |
|
337 | |||
338 | 32 | foreach ($this->em->getFilters()->getEnabledFilters() as $filter) { |
|
339 | 6 | if ($filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias)) { |
|
340 | 6 | $filterClauses[] = '(' . $filterExpr . ')'; |
|
341 | } |
||
342 | } |
||
343 | |||
344 | 32 | return $filterClauses |
|
345 | 6 | ? '(' . implode(' AND ', $filterClauses) . ')' |
|
346 | 32 | : ''; |
|
347 | } |
||
348 | |||
349 | /** |
||
350 | * Generate ON condition |
||
351 | * |
||
352 | * @param array $mapping |
||
353 | * |
||
354 | * @return array |
||
355 | */ |
||
356 | 18 | protected function getOnConditionSQL($mapping) |
|
357 | { |
||
358 | 18 | $targetClass = $this->em->getClassMetadata($mapping['targetEntity']); |
|
359 | 18 | $association = ( ! $mapping['isOwningSide']) |
|
360 | 3 | ? $targetClass->associationMappings[$mapping['mappedBy']] |
|
361 | 18 | : $mapping; |
|
362 | |||
363 | 18 | $joinColumns = $mapping['isOwningSide'] |
|
364 | 15 | ? $association['joinTable']['inverseJoinColumns'] |
|
365 | 18 | : $association['joinTable']['joinColumns']; |
|
366 | |||
367 | 18 | $conditions = []; |
|
368 | |||
369 | 18 | foreach ($joinColumns as $joinColumn) { |
|
370 | 18 | $joinColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); |
|
371 | 18 | $refColumnName = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform); |
|
372 | |||
373 | 18 | $conditions[] = ' t.' . $joinColumnName . ' = ' . 'te.' . $refColumnName; |
|
374 | } |
||
375 | |||
376 | 18 | return $conditions; |
|
377 | } |
||
378 | |||
379 | /** |
||
380 | * {@inheritdoc} |
||
381 | * |
||
382 | * @override |
||
383 | */ |
||
384 | 18 | protected function getDeleteSQL(PersistentCollection $collection) |
|
385 | { |
||
386 | 18 | $columns = []; |
|
387 | 18 | $mapping = $collection->getMapping(); |
|
388 | 18 | $class = $this->em->getClassMetadata(get_class($collection->getOwner())); |
|
389 | 18 | $joinTable = $this->quoteStrategy->getJoinTableName($mapping, $class, $this->platform); |
|
390 | |||
391 | 18 | foreach ($mapping['joinTable']['joinColumns'] as $joinColumn) { |
|
392 | 18 | $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); |
|
393 | } |
||
394 | |||
395 | 18 | return 'DELETE FROM ' . $joinTable |
|
396 | 18 | . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?'; |
|
397 | } |
||
398 | |||
399 | /** |
||
400 | * {@inheritdoc} |
||
401 | * |
||
402 | * Internal note: Order of the parameters must be the same as the order of the columns in getDeleteSql. |
||
403 | * @override |
||
404 | */ |
||
405 | 18 | protected function getDeleteSQLParameters(PersistentCollection $collection) |
|
406 | { |
||
407 | 18 | $mapping = $collection->getMapping(); |
|
408 | 18 | $identifier = $this->uow->getEntityIdentifier($collection->getOwner()); |
|
409 | |||
410 | // Optimization for single column identifier |
||
411 | 18 | if (count($mapping['relationToSourceKeyColumns']) === 1) { |
|
412 | 15 | return [reset($identifier)]; |
|
413 | } |
||
414 | |||
415 | // Composite identifier |
||
416 | 3 | $sourceClass = $this->em->getClassMetadata($mapping['sourceEntity']); |
|
417 | 3 | $params = []; |
|
418 | |||
419 | 3 | foreach ($mapping['relationToSourceKeyColumns'] as $columnName => $refColumnName) { |
|
420 | 3 | $params[] = isset($sourceClass->fieldNames[$refColumnName]) |
|
421 | 2 | ? $identifier[$sourceClass->fieldNames[$refColumnName]] |
|
422 | 3 | : $identifier[$sourceClass->getFieldForColumn($refColumnName)]; |
|
423 | } |
||
424 | |||
425 | 3 | return $params; |
|
426 | } |
||
427 | |||
428 | /** |
||
429 | * Gets the SQL statement used for deleting a row from the collection. |
||
430 | * |
||
431 | * @param \Doctrine\ORM\PersistentCollection $collection |
||
432 | * |
||
433 | * @return string[]|string[][] ordered tuple containing the SQL to be executed and an array |
||
434 | * of types for bound parameters |
||
435 | */ |
||
436 | 334 | protected function getDeleteRowSQL(PersistentCollection $collection) |
|
437 | { |
||
438 | 334 | $mapping = $collection->getMapping(); |
|
439 | 334 | $class = $this->em->getClassMetadata($mapping['sourceEntity']); |
|
440 | 334 | $targetClass = $this->em->getClassMetadata($mapping['targetEntity']); |
|
441 | 334 | $columns = []; |
|
442 | 334 | $types = []; |
|
443 | |||
444 | 334 | foreach ($mapping['joinTable']['joinColumns'] as $joinColumn) { |
|
445 | 334 | $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); |
|
446 | 334 | $types[] = PersisterHelper::getTypeOfColumn($joinColumn['referencedColumnName'], $class, $this->em); |
|
447 | } |
||
448 | |||
449 | 334 | foreach ($mapping['joinTable']['inverseJoinColumns'] as $joinColumn) { |
|
450 | 334 | $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); |
|
451 | 334 | $types[] = PersisterHelper::getTypeOfColumn($joinColumn['referencedColumnName'], $targetClass, $this->em); |
|
452 | } |
||
453 | |||
454 | return [ |
||
455 | 334 | 'DELETE FROM ' . $this->quoteStrategy->getJoinTableName($mapping, $class, $this->platform) |
|
456 | 334 | . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?', |
|
457 | 334 | $types, |
|
458 | ]; |
||
459 | } |
||
460 | |||
461 | /** |
||
462 | * Gets the SQL parameters for the corresponding SQL statement to delete the given |
||
463 | * element from the given collection. |
||
464 | * |
||
465 | * Internal note: Order of the parameters must be the same as the order of the columns in getDeleteRowSql. |
||
466 | * |
||
467 | * @param \Doctrine\ORM\PersistentCollection $collection |
||
468 | * @param mixed $element |
||
469 | * |
||
470 | * @return array |
||
471 | */ |
||
472 | 12 | protected function getDeleteRowSQLParameters(PersistentCollection $collection, $element) |
|
473 | { |
||
474 | 12 | return $this->collectJoinTableColumnParameters($collection, $element); |
|
475 | } |
||
476 | |||
477 | /** |
||
478 | * Gets the SQL statement used for inserting a row in the collection. |
||
479 | * |
||
480 | * @param \Doctrine\ORM\PersistentCollection $collection |
||
481 | * |
||
482 | * @return string[]|string[][] ordered tuple containing the SQL to be executed and an array |
||
483 | * of types for bound parameters |
||
484 | */ |
||
485 | 334 | protected function getInsertRowSQL(PersistentCollection $collection) |
|
486 | { |
||
487 | 334 | $columns = []; |
|
488 | 334 | $types = []; |
|
489 | 334 | $mapping = $collection->getMapping(); |
|
490 | 334 | $class = $this->em->getClassMetadata($mapping['sourceEntity']); |
|
491 | 334 | $targetClass = $this->em->getClassMetadata($mapping['targetEntity']); |
|
492 | |||
493 | 334 | foreach ($mapping['joinTable']['joinColumns'] as $joinColumn) { |
|
494 | 334 | $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); |
|
495 | 334 | $types[] = PersisterHelper::getTypeOfColumn($joinColumn['referencedColumnName'], $class, $this->em); |
|
496 | } |
||
497 | |||
498 | 334 | foreach ($mapping['joinTable']['inverseJoinColumns'] as $joinColumn) { |
|
499 | 334 | $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); |
|
500 | 334 | $types[] = PersisterHelper::getTypeOfColumn($joinColumn['referencedColumnName'], $targetClass, $this->em); |
|
501 | } |
||
502 | |||
503 | return [ |
||
504 | 334 | 'INSERT INTO ' . $this->quoteStrategy->getJoinTableName($mapping, $class, $this->platform) |
|
505 | 334 | . ' (' . implode(', ', $columns) . ')' |
|
506 | 334 | . ' VALUES' |
|
507 | 334 | . ' (' . implode(', ', array_fill(0, count($columns), '?')) . ')', |
|
508 | 334 | $types, |
|
509 | ]; |
||
510 | } |
||
511 | |||
512 | /** |
||
513 | * Gets the SQL parameters for the corresponding SQL statement to insert the given |
||
514 | * element of the given collection into the database. |
||
515 | * |
||
516 | * Internal note: Order of the parameters must be the same as the order of the columns in getInsertRowSql. |
||
517 | * |
||
518 | * @param \Doctrine\ORM\PersistentCollection $collection |
||
519 | * @param mixed $element |
||
520 | * |
||
521 | * @return array |
||
522 | */ |
||
523 | 334 | protected function getInsertRowSQLParameters(PersistentCollection $collection, $element) |
|
524 | { |
||
525 | 334 | return $this->collectJoinTableColumnParameters($collection, $element); |
|
526 | } |
||
527 | |||
528 | /** |
||
529 | * Collects the parameters for inserting/deleting on the join table in the order |
||
530 | * of the join table columns as specified in ManyToManyMapping#joinTableColumns. |
||
531 | * |
||
532 | * @param \Doctrine\ORM\PersistentCollection $collection |
||
533 | * @param object $element |
||
534 | * |
||
535 | * @return array |
||
536 | */ |
||
537 | 334 | private function collectJoinTableColumnParameters(PersistentCollection $collection, $element) |
|
538 | { |
||
539 | 334 | $params = []; |
|
540 | 334 | $mapping = $collection->getMapping(); |
|
541 | 334 | $isComposite = count($mapping['joinTableColumns']) > 2; |
|
542 | |||
543 | 334 | $identifier1 = $this->uow->getEntityIdentifier($collection->getOwner()); |
|
544 | 334 | $identifier2 = $this->uow->getEntityIdentifier($element); |
|
545 | |||
546 | 334 | $class1 = $class2 = null; |
|
547 | 334 | if ($isComposite) { |
|
548 | 22 | $class1 = $this->em->getClassMetadata(get_class($collection->getOwner())); |
|
549 | 22 | $class2 = $collection->getTypeClass(); |
|
550 | } |
||
551 | |||
552 | 334 | foreach ($mapping['joinTableColumns'] as $joinTableColumn) { |
|
553 | 334 | $isRelationToSource = isset($mapping['relationToSourceKeyColumns'][$joinTableColumn]); |
|
554 | |||
555 | 334 | if ( ! $isComposite) { |
|
556 | 312 | $params[] = $isRelationToSource ? array_pop($identifier1) : array_pop($identifier2); |
|
557 | |||
558 | 312 | continue; |
|
559 | } |
||
560 | |||
561 | 22 | if ($isRelationToSource) { |
|
562 | 22 | $params[] = $identifier1[$class1->getFieldForColumn($mapping['relationToSourceKeyColumns'][$joinTableColumn])]; |
|
563 | |||
564 | 22 | continue; |
|
565 | } |
||
566 | |||
567 | 22 | $params[] = $identifier2[$class2->getFieldForColumn($mapping['relationToTargetKeyColumns'][$joinTableColumn])]; |
|
568 | } |
||
569 | |||
570 | 334 | return $params; |
|
571 | } |
||
572 | |||
573 | /** |
||
574 | * @param \Doctrine\ORM\PersistentCollection $collection |
||
575 | * @param string $key |
||
576 | * @param boolean $addFilters Whether the filter SQL should be included or not. |
||
577 | * |
||
578 | * @return array ordered vector: |
||
579 | * - quoted join table name |
||
580 | * - where clauses to be added for filtering |
||
581 | * - parameters to be bound for filtering |
||
582 | * - types of the parameters to be bound for filtering |
||
583 | */ |
||
584 | 7 | private function getJoinTableRestrictionsWithKey(PersistentCollection $collection, $key, $addFilters) |
|
585 | { |
||
586 | 7 | $filterMapping = $collection->getMapping(); |
|
587 | 7 | $mapping = $filterMapping; |
|
588 | 7 | $indexBy = $mapping['indexBy']; |
|
589 | 7 | $id = $this->uow->getEntityIdentifier($collection->getOwner()); |
|
590 | 7 | $sourceClass = $this->em->getClassMetadata($mapping['sourceEntity']); |
|
591 | 7 | $targetClass = $this->em->getClassMetadata($mapping['targetEntity']); |
|
592 | |||
593 | 7 | if (! $mapping['isOwningSide']) { |
|
594 | 3 | $associationSourceClass = $this->em->getClassMetadata($mapping['targetEntity']); |
|
595 | 3 | $mapping = $associationSourceClass->associationMappings[$mapping['mappedBy']]; |
|
596 | 3 | $joinColumns = $mapping['joinTable']['joinColumns']; |
|
597 | 3 | $sourceRelationMode = 'relationToTargetKeyColumns'; |
|
598 | 3 | $targetRelationMode = 'relationToSourceKeyColumns'; |
|
599 | } else { |
||
600 | 4 | $associationSourceClass = $this->em->getClassMetadata($mapping['sourceEntity']); |
|
601 | 4 | $joinColumns = $mapping['joinTable']['inverseJoinColumns']; |
|
602 | 4 | $sourceRelationMode = 'relationToSourceKeyColumns'; |
|
603 | 4 | $targetRelationMode = 'relationToTargetKeyColumns'; |
|
604 | } |
||
605 | |||
606 | 7 | $quotedJoinTable = $this->quoteStrategy->getJoinTableName($mapping, $associationSourceClass, $this->platform). ' t'; |
|
607 | 7 | $whereClauses = []; |
|
608 | 7 | $params = []; |
|
609 | 7 | $types = []; |
|
610 | |||
611 | 7 | $joinNeeded = ! in_array($indexBy, $targetClass->identifier); |
|
612 | |||
613 | 7 | if ($joinNeeded) { // extra join needed if indexBy is not a @id |
|
614 | 3 | $joinConditions = []; |
|
615 | |||
616 | 3 | foreach ($joinColumns as $joinTableColumn) { |
|
617 | 3 | $joinConditions[] = 't.' . $joinTableColumn['name'] . ' = tr.' . $joinTableColumn['referencedColumnName']; |
|
618 | } |
||
619 | |||
620 | 3 | $tableName = $this->quoteStrategy->getTableName($targetClass, $this->platform); |
|
621 | 3 | $quotedJoinTable .= ' JOIN ' . $tableName . ' tr ON ' . implode(' AND ', $joinConditions); |
|
622 | 3 | $columnName = $targetClass->getColumnName($indexBy); |
|
623 | |||
624 | 3 | $whereClauses[] = 'tr.' . $columnName . ' = ?'; |
|
625 | 3 | $params[] = $key; |
|
626 | 3 | $types[] = PersisterHelper::getTypeOfColumn($columnName, $targetClass, $this->em); |
|
627 | } |
||
628 | |||
629 | 7 | foreach ($mapping['joinTableColumns'] as $joinTableColumn) { |
|
630 | 7 | if (isset($mapping[$sourceRelationMode][$joinTableColumn])) { |
|
631 | 7 | $column = $mapping[$sourceRelationMode][$joinTableColumn]; |
|
632 | 7 | $whereClauses[] = 't.' . $joinTableColumn . ' = ?'; |
|
633 | 7 | $params[] = $sourceClass->containsForeignIdentifier |
|
634 | ? $id[$sourceClass->getFieldForColumn($column)] |
||
635 | 7 | : $id[$sourceClass->fieldNames[$column]]; |
|
636 | 7 | $types[] = PersisterHelper::getTypeOfColumn($column, $sourceClass, $this->em); |
|
637 | 7 | } elseif ( ! $joinNeeded) { |
|
638 | 4 | $column = $mapping[$targetRelationMode][$joinTableColumn]; |
|
639 | |||
640 | 4 | $whereClauses[] = 't.' . $joinTableColumn . ' = ?'; |
|
641 | 4 | $params[] = $key; |
|
642 | 7 | $types[] = PersisterHelper::getTypeOfColumn($column, $targetClass, $this->em); |
|
643 | } |
||
644 | } |
||
645 | |||
646 | 7 | if ($addFilters) { |
|
647 | 7 | list($joinTargetEntitySQL, $filterSql) = $this->getFilterSql($filterMapping); |
|
648 | |||
649 | 7 | if ($filterSql) { |
|
650 | $quotedJoinTable .= ' ' . $joinTargetEntitySQL; |
||
651 | $whereClauses[] = $filterSql; |
||
652 | } |
||
653 | } |
||
654 | |||
655 | 7 | return [$quotedJoinTable, $whereClauses, $params, $types]; |
|
656 | } |
||
657 | |||
658 | /** |
||
659 | * @param \Doctrine\ORM\PersistentCollection $collection |
||
660 | * @param object $element |
||
661 | * @param boolean $addFilters Whether the filter SQL should be included or not. |
||
662 | * |
||
663 | * @return array ordered vector: |
||
664 | * - quoted join table name |
||
665 | * - where clauses to be added for filtering |
||
666 | * - parameters to be bound for filtering |
||
667 | * - types of the parameters to be bound for filtering |
||
668 | */ |
||
669 | 9 | private function getJoinTableRestrictions(PersistentCollection $collection, $element, $addFilters) |
|
670 | { |
||
671 | 9 | $filterMapping = $collection->getMapping(); |
|
672 | 9 | $mapping = $filterMapping; |
|
673 | |||
674 | 9 | if ( ! $mapping['isOwningSide']) { |
|
675 | 4 | $sourceClass = $this->em->getClassMetadata($mapping['targetEntity']); |
|
676 | 4 | $targetClass = $this->em->getClassMetadata($mapping['sourceEntity']); |
|
677 | 4 | $sourceId = $this->uow->getEntityIdentifier($element); |
|
678 | 4 | $targetId = $this->uow->getEntityIdentifier($collection->getOwner()); |
|
679 | |||
680 | 4 | $mapping = $sourceClass->associationMappings[$mapping['mappedBy']]; |
|
681 | } else { |
||
682 | 5 | $sourceClass = $this->em->getClassMetadata($mapping['sourceEntity']); |
|
683 | 5 | $targetClass = $this->em->getClassMetadata($mapping['targetEntity']); |
|
684 | 5 | $sourceId = $this->uow->getEntityIdentifier($collection->getOwner()); |
|
685 | 5 | $targetId = $this->uow->getEntityIdentifier($element); |
|
686 | } |
||
687 | |||
688 | 9 | $quotedJoinTable = $this->quoteStrategy->getJoinTableName($mapping, $sourceClass, $this->platform); |
|
689 | 9 | $whereClauses = []; |
|
690 | 9 | $params = []; |
|
691 | 9 | $types = []; |
|
692 | |||
693 | 9 | foreach ($mapping['joinTableColumns'] as $joinTableColumn) { |
|
694 | 9 | $whereClauses[] = ($addFilters ? 't.' : '') . $joinTableColumn . ' = ?'; |
|
695 | |||
696 | 9 | if (isset($mapping['relationToTargetKeyColumns'][$joinTableColumn])) { |
|
697 | 9 | $targetColumn = $mapping['relationToTargetKeyColumns'][$joinTableColumn]; |
|
698 | 9 | $params[] = $targetId[$targetClass->getFieldForColumn($targetColumn)]; |
|
699 | 9 | $types[] = PersisterHelper::getTypeOfColumn($targetColumn, $targetClass, $this->em); |
|
700 | |||
701 | 9 | continue; |
|
702 | } |
||
703 | |||
704 | // relationToSourceKeyColumns |
||
705 | 9 | $targetColumn = $mapping['relationToSourceKeyColumns'][$joinTableColumn]; |
|
706 | 9 | $params[] = $sourceId[$sourceClass->getFieldForColumn($targetColumn)]; |
|
707 | 9 | $types[] = PersisterHelper::getTypeOfColumn($targetColumn, $sourceClass, $this->em); |
|
708 | } |
||
709 | |||
710 | 9 | if ($addFilters) { |
|
711 | 7 | $quotedJoinTable .= ' t'; |
|
712 | |||
713 | 7 | list($joinTargetEntitySQL, $filterSql) = $this->getFilterSql($filterMapping); |
|
714 | |||
715 | 7 | if ($filterSql) { |
|
716 | 3 | $quotedJoinTable .= ' ' . $joinTargetEntitySQL; |
|
717 | 3 | $whereClauses[] = $filterSql; |
|
718 | } |
||
719 | } |
||
720 | |||
721 | 9 | return [$quotedJoinTable, $whereClauses, $params, $types]; |
|
722 | } |
||
723 | |||
724 | /** |
||
725 | * Expands Criteria Parameters by walking the expressions and grabbing all |
||
726 | * parameters and types from it. |
||
727 | * |
||
728 | * @param \Doctrine\Common\Collections\Criteria $criteria |
||
729 | * |
||
730 | * @return array |
||
731 | */ |
||
732 | 12 | private function expandCriteriaParameters(Criteria $criteria) |
|
733 | { |
||
734 | 12 | $expression = $criteria->getWhereExpression(); |
|
735 | |||
736 | 12 | if ($expression === null) { |
|
737 | 5 | return []; |
|
738 | } |
||
739 | |||
740 | 7 | $valueVisitor = new SqlValueVisitor(); |
|
741 | |||
742 | 7 | $valueVisitor->dispatch($expression); |
|
743 | |||
744 | 7 | list(, $types) = $valueVisitor->getParamsAndTypes(); |
|
745 | |||
746 | 7 | return $types; |
|
747 | } |
||
748 | |||
749 | /** |
||
750 | * @param Criteria $criteria |
||
751 | * @param ClassMetadata $targetClass |
||
752 | * @return string |
||
753 | */ |
||
754 | 12 | private function getOrderingSql(Criteria $criteria, ClassMetadata $targetClass) |
|
755 | { |
||
756 | 12 | $orderings = $criteria->getOrderings(); |
|
757 | 12 | if ($orderings) { |
|
758 | 2 | $orderBy = []; |
|
759 | 2 | foreach ($orderings as $name => $direction) { |
|
760 | 2 | $field = $this->quoteStrategy->getColumnName( |
|
761 | 2 | $name, |
|
762 | 2 | $targetClass, |
|
763 | 2 | $this->platform |
|
764 | ); |
||
765 | 2 | $orderBy[] = $field . ' ' . $direction; |
|
766 | } |
||
767 | |||
768 | 2 | return ' ORDER BY ' . implode(', ', $orderBy); |
|
769 | } |
||
770 | 10 | return ''; |
|
771 | } |
||
772 | |||
773 | /** |
||
774 | * @param Criteria $criteria |
||
775 | * @return string |
||
776 | * @throws \Doctrine\DBAL\DBALException |
||
777 | */ |
||
778 | 12 | private function getLimitSql(Criteria $criteria) |
|
779 | { |
||
780 | 12 | $limit = $criteria->getMaxResults(); |
|
781 | 12 | $offset = $criteria->getFirstResult(); |
|
782 | 12 | if ($limit !== null || $offset !== null) { |
|
783 | 3 | return $this->platform->modifyLimitQuery('', $limit, $offset); |
|
784 | } |
||
785 | 9 | return ''; |
|
786 | } |
||
787 | } |
||
788 |
In the issue above, the returned value is violating the contract defined by the mentioned interface.
Let's take a look at an example: