Failed Conditions
Pull Request — master (#7085)
by Guilherme
14:34
created

ORM/Persisters/Collection/ManyToManyPersister.php (2 issues)

Labels
Severity
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
The method getRootClassName() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

332
        $rootClass   = $this->em->getClassMetadata($targetClass->/** @scrutinizer ignore-call */ getRootClassName());

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.

Loading history...
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
Accessing table on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
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