ManyToManyPersister   F
last analyzed

Complexity

Total Complexity 83

Size/Duplication

Total Lines 781
Duplicated Lines 0 %

Test Coverage

Coverage 97.78%

Importance

Changes 0
Metric Value
eloc 345
dl 0
loc 781
ccs 353
cts 361
cp 0.9778
rs 2
c 0
b 0
f 0
wmc 83

24 Methods

Rating   Name   Duplication   Size   Complexity  
A getInsertRowSQLParameters() 0 3 1
A loadCriteria() 0 58 4
A expandCriteriaParameters() 0 15 2
A getLimitSql() 0 9 3
A count() 0 57 5
A containsKey() 0 13 3
A getFilterSql() 0 16 2
A generateFilterConditionSQL() 0 21 5
B getJoinTableRestrictions() 0 58 8
A delete() 0 20 3
A getDeleteSQLParameters() 0 21 3
A update() 0 24 4
A get() 0 19 4
A getDeleteRowSQL() 0 30 3
A getOrderingSql() 0 18 3
A getDeleteRowSQLParameters() 0 3 1
A getOnConditionSQL() 0 22 4
A collectJoinTableColumnParameters() 0 23 3
A getDeleteSQL() 0 12 2
C getJoinTableRestrictionsWithKey() 0 87 12
A slice() 0 6 1
A getInsertRowSQL() 0 33 3
A contains() 0 11 2
A removeElement() 0 11 2

How to fix   Complexity   

Complex Class

Complex classes like ManyToManyPersister often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ManyToManyPersister, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM\Persisters\Collection;
6
7
use BadMethodCallException;
8
use Doctrine\Common\Collections\Criteria;
9
use Doctrine\DBAL\DBALException;
10
use Doctrine\ORM\Mapping\ClassMetadata;
11
use Doctrine\ORM\Mapping\FieldMetadata;
12
use Doctrine\ORM\Mapping\JoinColumnMetadata;
13
use Doctrine\ORM\Mapping\ManyToManyAssociationMetadata;
14
use Doctrine\ORM\Mapping\ToManyAssociationMetadata;
15
use Doctrine\ORM\Mapping\ToOneAssociationMetadata;
16
use Doctrine\ORM\PersistentCollection;
17
use Doctrine\ORM\Persisters\SqlValueVisitor;
18
use Doctrine\ORM\Query;
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());
0 ignored issues
show
Unused Code introduced by
The assignment to $class is dead and can be removed.
Loading history...
44 18
        $joinTable = $association->getJoinTable();
0 ignored issues
show
Bug introduced by
The method getJoinTable() does not exist on Doctrine\ORM\Mapping\ToManyAssociationMetadata. It seems like you code against a sub-type of Doctrine\ORM\Mapping\ToManyAssociationMetadata such as Doctrine\ORM\Mapping\ManyToManyAssociationMetadata. ( Ignorable by Annotation )

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

44
        /** @scrutinizer ignore-call */ 
45
        $joinTable = $association->getJoinTable();
Loading history...
45 18
        $types     = [];
46
47 18
        foreach ($joinTable->getJoinColumns() as $joinColumn) {
48 18
            $types[] = $joinColumn->getType();
49
        }
50
51 18
        $sql    = $this->getDeleteSQL($collection);
52 18
        $params = $this->getDeleteSQLParameters($collection);
53
54 18
        $this->conn->executeUpdate($sql, $params, $types);
55 18
    }
56
57
    /**
58
     * {@inheritdoc}
59
     */
60 329
    public function update(PersistentCollection $collection)
61
    {
62 329
        $association = $collection->getMapping();
63
64 329
        if (! $association->isOwningSide()) {
65 236
            return; // ignore inverse side
66
        }
67
68 328
        [$deleteSql, $deleteTypes] = $this->getDeleteRowSQL($collection);
69 328
        [$insertSql, $insertTypes] = $this->getInsertRowSQL($collection);
70
71 328
        foreach ($collection->getDeleteDiff() as $element) {
72 10
            $this->conn->executeUpdate(
73 10
                $deleteSql,
74 10
                $this->getDeleteRowSQLParameters($collection, $element),
75
                $deleteTypes
76
            );
77
        }
78
79 328
        foreach ($collection->getInsertDiff() as $element) {
80 328
            $this->conn->executeUpdate(
81 328
                $insertSql,
82 328
                $this->getInsertRowSQLParameters($collection, $element),
83
                $insertTypes
84
            );
85
        }
86 328
    }
87
88
    /**
89
     * {@inheritdoc}
90
     */
91 3
    public function get(PersistentCollection $collection, $index)
92
    {
93 3
        $association = $collection->getMapping();
94
95 3
        if (! ($association instanceof ToManyAssociationMetadata && $association->getIndexedBy())) {
96
            throw new BadMethodCallException('Selecting a collection by index is only supported on indexed collections.');
97
        }
98
99 3
        $persister = $this->uow->getEntityPersister($association->getTargetEntity());
100 3
        $mappedKey = $association->isOwningSide()
101 2
            ? $association->getInversedBy()
102 3
            : $association->getMappedBy();
103
104
        $criteria = [
105 3
            $mappedKey                   => $collection->getOwner(),
106 3
            $association->getIndexedBy() => $index,
107
        ];
108
109 3
        return $persister->load($criteria, null, $association, [], 0, 1);
110
    }
111
112
    /**
113
     * {@inheritdoc}
114
     */
115 18
    public function count(PersistentCollection $collection)
116
    {
117 18
        $conditions        = [];
118 18
        $params            = [];
119 18
        $types             = [];
120 18
        $association       = $collection->getMapping();
121 18
        $identifier        = $this->uow->getEntityIdentifier($collection->getOwner());
122 18
        $sourceClass       = $this->em->getClassMetadata($association->getSourceEntity());
123 18
        $targetClass       = $this->em->getClassMetadata($association->getTargetEntity());
124 18
        $owningAssociation = ! $association->isOwningSide()
125 4
            ? $targetClass->getProperty($association->getMappedBy())
0 ignored issues
show
Bug introduced by
The method getProperty() does not exist on Doctrine\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

125
            ? $targetClass->/** @scrutinizer ignore-call */ getProperty($association->getMappedBy())

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...
126 18
            : $association;
127
128 18
        $joinTable     = $owningAssociation->getJoinTable();
129 18
        $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
130 18
        $joinColumns   = $association->isOwningSide()
131 14
            ? $joinTable->getJoinColumns()
132 18
            : $joinTable->getInverseJoinColumns();
133
134 18
        foreach ($joinColumns as $joinColumn) {
135
            /** @var JoinColumnMetadata $joinColumn */
136 18
            $quotedColumnName     = $this->platform->quoteIdentifier($joinColumn->getColumnName());
137 18
            $referencedColumnName = $joinColumn->getReferencedColumnName();
138
139 18
            $conditions[] = sprintf('t.%s = ?', $quotedColumnName);
140 18
            $params[]     = $identifier[$sourceClass->fieldNames[$referencedColumnName]];
0 ignored issues
show
Bug introduced by
Accessing fieldNames on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
141 18
            $types[]      = $joinColumn->getType();
142
        }
143
144 18
        [$joinTargetEntitySQL, $filterSql] = $this->getFilterSql($association);
145
146 18
        if ($filterSql) {
147 3
            $conditions[] = $filterSql;
148
        }
149
150
        // If there is a provided criteria, make part of conditions
151
        // @todo Fix this. Current SQL returns something like:
152
        /*if ($criteria && ($expression = $criteria->getWhereExpression()) !== null) {
153
            // A join is needed on the target entity
154
            $targetTableName = $targetClass->table->getQuotedQualifiedName($this->platform);
155
            $targetJoinSql   = ' JOIN ' . $targetTableName . ' te'
156
                . ' ON' . implode(' AND ', $this->getOnConditionSQL($association));
157
158
            // And criteria conditions needs to be added
159
            $persister    = $this->uow->getEntityPersister($targetClass->getClassName());
160
            $visitor      = new SqlExpressionVisitor($persister, $targetClass);
161
            $conditions[] = $visitor->dispatch($expression);
162
163
            $joinTargetEntitySQL = $targetJoinSql . $joinTargetEntitySQL;
164
        }*/
165
166
        $sql = 'SELECT COUNT(*)'
167 18
            . ' FROM ' . $joinTableName . ' t'
168 18
            . $joinTargetEntitySQL
169 18
            . ' WHERE ' . implode(' AND ', $conditions);
170
171 18
        return $this->conn->fetchColumn($sql, $params, $types);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->conn->fetc...($sql, $params, $types) also could return the type false which is incompatible with the return type mandated by Doctrine\ORM\Persisters\...ctionPersister::count() of integer.
Loading history...
172
    }
173
174
    /**
175
     * {@inheritDoc}
176
     */
177 8
    public function slice(PersistentCollection $collection, $offset, $length = null)
178
    {
179 8
        $association = $collection->getMapping();
180 8
        $persister   = $this->uow->getEntityPersister($association->getTargetEntity());
181
182 8
        return $persister->getManyToManyCollection($association, $collection->getOwner(), $offset, $length);
183
    }
184
185
    /**
186
     * {@inheritdoc}
187
     */
188 7
    public function containsKey(PersistentCollection $collection, $key)
189
    {
190 7
        $association = $collection->getMapping();
191
192 7
        if (! ($association instanceof ToManyAssociationMetadata && $association->getIndexedBy())) {
193
            throw new BadMethodCallException('Selecting a collection by index is only supported on indexed collections.');
194
        }
195
196 7
        [$quotedJoinTable, $whereClauses, $params, $types] = $this->getJoinTableRestrictionsWithKey($collection, $key, true);
197
198 7
        $sql = 'SELECT 1 FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses);
199
200 7
        return (bool) $this->conn->fetchColumn($sql, $params, $types);
201
    }
202
203
    /**
204
     * {@inheritDoc}
205
     */
206 7
    public function contains(PersistentCollection $collection, $element)
207
    {
208 7
        if (! $this->isValidEntityState($element)) {
209 2
            return false;
210
        }
211
212 7
        [$quotedJoinTable, $whereClauses, $params, $types] = $this->getJoinTableRestrictions($collection, $element, true);
213
214 7
        $sql = 'SELECT 1 FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses);
215
216 7
        return (bool) $this->conn->fetchColumn($sql, $params, $types);
217
    }
218
219
    /**
220
     * {@inheritDoc}
221
     */
222 2
    public function removeElement(PersistentCollection $collection, $element)
223
    {
224 2
        if (! $this->isValidEntityState($element)) {
225 2
            return false;
226
        }
227
228 2
        [$quotedJoinTable, $whereClauses, $params, $types] = $this->getJoinTableRestrictions($collection, $element, false);
229
230 2
        $sql = 'DELETE FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses);
231
232 2
        return (bool) $this->conn->executeUpdate($sql, $params, $types);
233
    }
234
235
    /**
236
     * {@inheritDoc}
237
     */
238 12
    public function loadCriteria(PersistentCollection $collection, Criteria $criteria)
239
    {
240 12
        $association   = $collection->getMapping();
241 12
        $owner         = $collection->getOwner();
242 12
        $ownerMetadata = $this->em->getClassMetadata(get_class($owner));
243 12
        $identifier    = $this->uow->getEntityIdentifier($owner);
244 12
        $targetClass   = $this->em->getClassMetadata($association->getTargetEntity());
245 12
        $onConditions  = $this->getOnConditionSQL($association);
246 12
        $whereClauses  = $params = $types = [];
247
248 12
        if (! $association->isOwningSide()) {
249 1
            $association = $targetClass->getProperty($association->getMappedBy());
250 1
            $joinColumns = $association->getJoinTable()->getInverseJoinColumns();
251
        } else {
252 11
            $joinColumns = $association->getJoinTable()->getJoinColumns();
253
        }
254
255 12
        foreach ($joinColumns as $joinColumn) {
256
            /** @var JoinColumnMetadata $joinColumn */
257 12
            $quotedColumnName     = $this->platform->quoteIdentifier($joinColumn->getColumnName());
258 12
            $referencedColumnName = $joinColumn->getReferencedColumnName();
259
260 12
            $whereClauses[] = sprintf('t.%s = ?', $quotedColumnName);
261 12
            $params[]       = $identifier[$ownerMetadata->fieldNames[$referencedColumnName]];
0 ignored issues
show
Bug introduced by
Accessing fieldNames on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
262 12
            $types[]        = $joinColumn->getType();
263
        }
264
265 12
        $parameters = $this->expandCriteriaParameters($criteria);
266
267 12
        foreach ($parameters as $parameter) {
268 7
            [$name, $value, $operator] = $parameter;
269
270 7
            $property   = $targetClass->getProperty($name);
271 7
            $columnName = $this->platform->quoteIdentifier($property->getColumnName());
272
273 7
            $whereClauses[] = sprintf('te.%s %s ?', $columnName, $operator);
274 7
            $params[]       = $value;
275 7
            $types[]        = $property->getType();
276
        }
277
278 12
        $tableName        = $targetClass->table->getQuotedQualifiedName($this->platform);
0 ignored issues
show
Bug introduced by
Accessing table on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
279 12
        $joinTableName    = $association->getJoinTable()->getQuotedQualifiedName($this->platform);
280 12
        $resultSetMapping = new Query\ResultSetMappingBuilder($this->em);
281
282 12
        $resultSetMapping->addRootEntityFromClassMetadata($targetClass->getClassName(), 'te');
0 ignored issues
show
Bug introduced by
The method getClassName() does not exist on Doctrine\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

282
        $resultSetMapping->addRootEntityFromClassMetadata($targetClass->/** @scrutinizer ignore-call */ getClassName(), 'te');

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...
283
284 12
        $sql = 'SELECT ' . $resultSetMapping->generateSelectClause()
285 12
            . ' FROM ' . $tableName . ' te'
286 12
            . ' JOIN ' . $joinTableName . ' t ON'
287 12
            . implode(' AND ', $onConditions)
288 12
            . ' WHERE ' . implode(' AND ', $whereClauses);
289
290 12
        $sql .= $this->getOrderingSql($criteria, $targetClass);
291 12
        $sql .= $this->getLimitSql($criteria);
292
293 12
        $stmt = $this->conn->executeQuery($sql, $params, $types);
294
295 12
        return $this->em->newHydrator(Query::HYDRATE_OBJECT)->hydrateAll($stmt, $resultSetMapping);
296
    }
297
298
    /**
299
     * Generates the filter SQL for a given mapping.
300
     *
301
     * This method is not used for actually grabbing the related entities
302
     * but when the extra-lazy collection methods are called on a filtered
303
     * association. This is why besides the many to many table we also
304
     * have to join in the actual entities table leading to additional
305
     * JOIN.
306
     *
307
     * @return string[] ordered tuple:
308
     *                   - JOIN condition to add to the SQL
309
     *                   - WHERE condition to add to the SQL
310
     */
311 32
    public function getFilterSql(ManyToManyAssociationMetadata $association)
312
    {
313 32
        $targetClass = $this->em->getClassMetadata($association->getTargetEntity());
314 32
        $rootClass   = $this->em->getClassMetadata($targetClass->getRootClassName());
0 ignored issues
show
Bug introduced by
The method getRootClassName() does not exist on Doctrine\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

314
        $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...
315 32
        $filterSql   = $this->generateFilterConditionSQL($rootClass, 'te');
316
317 32
        if ($filterSql === '') {
318 32
            return ['', ''];
319
        }
320
321
        // A join is needed if there is filtering on the target entity
322 6
        $tableName = $rootClass->table->getQuotedQualifiedName($this->platform);
0 ignored issues
show
Bug introduced by
Accessing table on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
323 6
        $joinSql   = ' JOIN ' . $tableName . ' te'
324 6
            . ' ON' . implode(' AND ', $this->getOnConditionSQL($association));
325
326 6
        return [$joinSql, $filterSql];
327
    }
328
329
    /**
330
     * Generates the filter SQL for a given entity and table alias.
331
     *
332
     * @param ClassMetadata $targetEntity     Metadata of the target entity.
333
     * @param string        $targetTableAlias The table alias of the joined/selected table.
334
     *
335
     * @return string The SQL query part to add to a query.
336
     */
337 32
    protected function generateFilterConditionSQL(ClassMetadata $targetEntity, $targetTableAlias)
338
    {
339 32
        $filterClauses = [];
340
341 32
        foreach ($this->em->getFilters()->getEnabledFilters() as $filter) {
342 6
            $filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias);
343
344 6
            if ($filterExpr) {
345 6
                $filterClauses[] = '(' . $filterExpr . ')';
346
            }
347
        }
348
349 32
        if (! $filterClauses) {
350 32
            return '';
351
        }
352
353 6
        $filterSql = implode(' AND ', $filterClauses);
354
355 6
        return isset($filterClauses[1])
356
            ? '(' . $filterSql . ')'
357 6
            : $filterSql;
358
    }
359
360
    /**
361
     * Generate ON condition
362
     *
363
     * @return string[]
364
     */
365 18
    protected function getOnConditionSQL(ManyToManyAssociationMetadata $association)
366
    {
367 18
        $targetClass       = $this->em->getClassMetadata($association->getTargetEntity());
368 18
        $owningAssociation = ! $association->isOwningSide()
369 3
            ? $targetClass->getProperty($association->getMappedBy())
370 18
            : $association;
371
372 18
        $joinTable   = $owningAssociation->getJoinTable();
373 18
        $joinColumns = $association->isOwningSide()
374 15
            ? $joinTable->getInverseJoinColumns()
375 18
            : $joinTable->getJoinColumns();
376
377 18
        $conditions = [];
378
379 18
        foreach ($joinColumns as $joinColumn) {
380 18
            $quotedColumnName           = $this->platform->quoteIdentifier($joinColumn->getColumnName());
381 18
            $quotedReferencedColumnName = $this->platform->quoteIdentifier($joinColumn->getReferencedColumnName());
382
383 18
            $conditions[] = ' t.' . $quotedColumnName . ' = te.' . $quotedReferencedColumnName;
384
        }
385
386 18
        return $conditions;
387
    }
388
389
    /**
390
     * {@inheritdoc}
391
     *
392
     * @override
393
     */
394 18
    protected function getDeleteSQL(PersistentCollection $collection)
395
    {
396 18
        $association   = $collection->getMapping();
397 18
        $joinTable     = $association->getJoinTable();
398 18
        $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
399 18
        $columns       = [];
400
401 18
        foreach ($joinTable->getJoinColumns() as $joinColumn) {
402 18
            $columns[] = $this->platform->quoteIdentifier($joinColumn->getColumnName());
403
        }
404
405 18
        return 'DELETE FROM ' . $joinTableName . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?';
406
    }
407
408
    /**
409
     * {@inheritdoc}
410
     *
411
     * {@internal Order of the parameters must be the same as the order of the columns in getDeleteSql. }}
412
     *
413
     * @override
414
     */
415 18
    protected function getDeleteSQLParameters(PersistentCollection $collection)
416
    {
417 18
        $association = $collection->getMapping();
418 18
        $identifier  = $this->uow->getEntityIdentifier($collection->getOwner());
419 18
        $joinTable   = $association->getJoinTable();
420 18
        $joinColumns = $joinTable->getJoinColumns();
421
422
        // Optimization for single column identifier
423 18
        if (count($joinColumns) === 1) {
424 15
            return [reset($identifier)];
425
        }
426
427
        // Composite identifier
428 3
        $sourceClass = $this->em->getClassMetadata($association->getSourceEntity());
429 3
        $params      = [];
430
431 3
        foreach ($joinColumns as $joinColumn) {
432 3
            $params[] = $identifier[$sourceClass->fieldNames[$joinColumn->getReferencedColumnName()]];
0 ignored issues
show
Bug introduced by
Accessing fieldNames on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
433
        }
434
435 3
        return $params;
436
    }
437
438
    /**
439
     * Gets the SQL statement used for deleting a row from the collection.
440
     *
441
     * @return string[]|string[][] ordered tuple containing the SQL to be executed and an array
442
     *                             of types for bound parameters
443
     */
444 328
    protected function getDeleteRowSQL(PersistentCollection $collection)
445
    {
446 328
        $association = $collection->getMapping();
447 328
        $class       = $this->em->getClassMetadata($association->getSourceEntity());
0 ignored issues
show
Unused Code introduced by
The assignment to $class is dead and can be removed.
Loading history...
448 328
        $targetClass = $this->em->getClassMetadata($association->getTargetEntity());
0 ignored issues
show
Unused Code introduced by
The assignment to $targetClass is dead and can be removed.
Loading history...
449 328
        $columns     = [];
450 328
        $types       = [];
451
452 328
        $joinTable     = $association->getJoinTable();
453 328
        $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
454
455 328
        foreach ($joinTable->getJoinColumns() as $joinColumn) {
456
            /** @var JoinColumnMetadata $joinColumn */
457 328
            $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName());
458
459 328
            $columns[] = $quotedColumnName;
460 328
            $types[]   = $joinColumn->getType();
461
        }
462
463 328
        foreach ($joinTable->getInverseJoinColumns() as $joinColumn) {
464
            /** @var JoinColumnMetadata $joinColumn */
465 328
            $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName());
466
467 328
            $columns[] = $quotedColumnName;
468 328
            $types[]   = $joinColumn->getType();
469
        }
470
471
        return [
472 328
            sprintf('DELETE FROM %s WHERE %s = ?', $joinTableName, implode(' = ? AND ', $columns)),
473 328
            $types,
474
        ];
475
    }
476
477
    /**
478
     * Gets the SQL parameters for the corresponding SQL statement to delete the given
479
     * element from the given collection.
480
     *
481
     * {@internal Order of the parameters must be the same as the order of the columns in getDeleteRowSql. }}
482
     *
483
     * @param mixed $element
484
     *
485
     * @return mixed[]
486
     */
487 10
    protected function getDeleteRowSQLParameters(PersistentCollection $collection, $element)
488
    {
489 10
        return $this->collectJoinTableColumnParameters($collection, $element);
490
    }
491
492
    /**
493
     * Gets the SQL statement used for inserting a row in the collection.
494
     *
495
     * @return string[]|string[][] ordered tuple containing the SQL to be executed and an array
496
     *                             of types for bound parameters
497
     */
498 328
    protected function getInsertRowSQL(PersistentCollection $collection)
499
    {
500 328
        $association = $collection->getMapping();
501 328
        $class       = $this->em->getClassMetadata($association->getSourceEntity());
0 ignored issues
show
Unused Code introduced by
The assignment to $class is dead and can be removed.
Loading history...
502 328
        $targetClass = $this->em->getClassMetadata($association->getTargetEntity());
0 ignored issues
show
Unused Code introduced by
The assignment to $targetClass is dead and can be removed.
Loading history...
503 328
        $columns     = [];
504 328
        $types       = [];
505
506 328
        $joinTable     = $association->getJoinTable();
507 328
        $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
508
509 328
        foreach ($joinTable->getJoinColumns() as $joinColumn) {
510
            /** @var JoinColumnMetadata $joinColumn */
511 328
            $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName());
512
513 328
            $columns[] = $quotedColumnName;
514 328
            $types[]   = $joinColumn->getType();
515
        }
516
517 328
        foreach ($joinTable->getInverseJoinColumns() as $joinColumn) {
518
            /** @var JoinColumnMetadata $joinColumn */
519 328
            $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName());
520
521 328
            $columns[] = $quotedColumnName;
522 328
            $types[]   = $joinColumn->getType();
523
        }
524
525 328
        $columnNamesAsString  = implode(', ', $columns);
526 328
        $columnValuesAsString = implode(', ', array_fill(0, count($columns), '?'));
527
528
        return [
529 328
            sprintf('INSERT INTO %s (%s) VALUES (%s)', $joinTableName, $columnNamesAsString, $columnValuesAsString),
530 328
            $types,
531
        ];
532
    }
533
534
    /**
535
     * Gets the SQL parameters for the corresponding SQL statement to insert the given
536
     * element of the given collection into the database.
537
     *
538
     * {@internal Order of the parameters must be the same as the order of the columns in getInsertRowSql. }}
539
     *
540
     * @param mixed $element
541
     *
542
     * @return mixed[]
543
     */
544 328
    protected function getInsertRowSQLParameters(PersistentCollection $collection, $element)
545
    {
546 328
        return $this->collectJoinTableColumnParameters($collection, $element);
547
    }
548
549
    /**
550
     * Collects the parameters for inserting/deleting on the join table in the order
551
     * of the join table columns.
552
     *
553
     * @param object $element
554
     *
555
     * @return mixed[]
556
     */
557 328
    private function collectJoinTableColumnParameters(PersistentCollection $collection, $element)
558
    {
559 328
        $params           = [];
560 328
        $association      = $collection->getMapping();
561 328
        $owningClass      = $this->em->getClassMetadata(get_class($collection->getOwner()));
562 328
        $targetClass      = $collection->getTypeClass();
563 328
        $owningIdentifier = $this->uow->getEntityIdentifier($collection->getOwner());
564 328
        $targetIdentifier = $this->uow->getEntityIdentifier($element);
565 328
        $joinTable        = $association->getJoinTable();
566
567 328
        foreach ($joinTable->getJoinColumns() as $joinColumn) {
568 328
            $fieldName = $owningClass->fieldNames[$joinColumn->getReferencedColumnName()];
0 ignored issues
show
Bug introduced by
Accessing fieldNames on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
569
570 328
            $params[] = $owningIdentifier[$fieldName];
571
        }
572
573 328
        foreach ($joinTable->getInverseJoinColumns() as $joinColumn) {
574 328
            $fieldName = $targetClass->fieldNames[$joinColumn->getReferencedColumnName()];
575
576 328
            $params[] = $targetIdentifier[$fieldName];
577
        }
578
579 328
        return $params;
580
    }
581
582
    /**
583
     * @param string $key
584
     * @param bool   $addFilters Whether the filter SQL should be included or not.
585
     *
586
     * @return mixed[] ordered vector:
587
     *                - quoted join table name
588
     *                - where clauses to be added for filtering
589
     *                - parameters to be bound for filtering
590
     *                - types of the parameters to be bound for filtering
591
     */
592 7
    private function getJoinTableRestrictionsWithKey(PersistentCollection $collection, $key, $addFilters)
593
    {
594 7
        $association       = $collection->getMapping();
595 7
        $owningAssociation = $association;
596 7
        $indexBy           = $owningAssociation->getIndexedBy();
597 7
        $identifier        = $this->uow->getEntityIdentifier($collection->getOwner());
598 7
        $sourceClass       = $this->em->getClassMetadata($owningAssociation->getSourceEntity());
599 7
        $targetClass       = $this->em->getClassMetadata($owningAssociation->getTargetEntity());
600
601 7
        if (! $owningAssociation->isOwningSide()) {
602 3
            $owningAssociation  = $targetClass->getProperty($owningAssociation->getMappedBy());
603 3
            $joinTable          = $owningAssociation->getJoinTable();
604 3
            $joinColumns        = $joinTable->getJoinColumns();
605 3
            $inverseJoinColumns = $joinTable->getInverseJoinColumns();
606
        } else {
607 4
            $joinTable          = $owningAssociation->getJoinTable();
608 4
            $joinColumns        = $joinTable->getInverseJoinColumns();
609 4
            $inverseJoinColumns = $joinTable->getJoinColumns();
610
        }
611
612 7
        $joinTableName   = $joinTable->getQuotedQualifiedName($this->platform);
613 7
        $quotedJoinTable = $joinTableName . ' t';
614 7
        $whereClauses    = [];
615 7
        $params          = [];
616 7
        $types           = [];
617 7
        $joinNeeded      = ! in_array($indexBy, $targetClass->identifier, true);
0 ignored issues
show
Bug introduced by
Accessing identifier on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
618
619 7
        if ($joinNeeded) { // extra join needed if indexBy is not a @id
620 3
            $joinConditions = [];
621
622 3
            foreach ($joinColumns as $joinColumn) {
623
                /** @var JoinColumnMetadata $joinColumn */
624 3
                $quotedColumnName           = $this->platform->quoteIdentifier($joinColumn->getColumnName());
625 3
                $quotedReferencedColumnName = $this->platform->quoteIdentifier($joinColumn->getReferencedColumnName());
626
627 3
                $joinConditions[] = ' t.' . $quotedColumnName . ' = tr.' . $quotedReferencedColumnName;
628
            }
629
630 3
            $tableName        = $targetClass->table->getQuotedQualifiedName($this->platform);
0 ignored issues
show
Bug introduced by
Accessing table on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
631 3
            $quotedJoinTable .= ' JOIN ' . $tableName . ' tr ON ' . implode(' AND ', $joinConditions);
632 3
            $indexByProperty  = $targetClass->getProperty($indexBy);
633
634
            switch (true) {
635 3
                case $indexByProperty instanceof FieldMetadata:
636 3
                    $quotedColumnName = $this->platform->quoteIdentifier($indexByProperty->getColumnName());
637
638 3
                    $whereClauses[] = sprintf('tr.%s = ?', $quotedColumnName);
639 3
                    $params[]       = $key;
640 3
                    $types[]        = $indexByProperty->getType();
641 3
                    break;
642
643
                case $indexByProperty instanceof ToOneAssociationMetadata && $indexByProperty->isOwningSide():
644
                    // Cannot be supported because PHP does not accept objects as keys. =(
645
                    break;
646
            }
647
        }
648
649 7
        foreach ($inverseJoinColumns as $joinColumn) {
650
            /** @var JoinColumnMetadata $joinColumn */
651 7
            $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName());
652
653 7
            $whereClauses[] = sprintf('t.%s = ?', $quotedColumnName);
654 7
            $params[]       = $identifier[$sourceClass->fieldNames[$joinColumn->getReferencedColumnName()]];
0 ignored issues
show
Bug introduced by
Accessing fieldNames on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
655 7
            $types[]        = $joinColumn->getType();
656
        }
657
658 7
        if (! $joinNeeded) {
659 4
            foreach ($joinColumns as $joinColumn) {
660
                /** @var JoinColumnMetadata $joinColumn */
661 4
                $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName());
662
663 4
                $whereClauses[] = sprintf('t.%s = ?', $quotedColumnName);
664 4
                $params[]       = $key;
665 4
                $types[]        = $joinColumn->getType();
666
            }
667
        }
668
669 7
        if ($addFilters) {
670 7
            [$joinTargetEntitySQL, $filterSql] = $this->getFilterSql($association);
671
672 7
            if ($filterSql) {
673
                $quotedJoinTable .= ' ' . $joinTargetEntitySQL;
674
                $whereClauses[]   = $filterSql;
675
            }
676
        }
677
678 7
        return [$quotedJoinTable, $whereClauses, $params, $types];
679
    }
680
681
    /**
682
     * @param object $element
683
     * @param bool   $addFilters Whether the filter SQL should be included or not.
684
     *
685
     * @return mixed[] ordered vector:
686
     *                - quoted join table name
687
     *                - where clauses to be added for filtering
688
     *                - parameters to be bound for filtering
689
     *                - types of the parameters to be bound for filtering
690
     */
691 9
    private function getJoinTableRestrictions(PersistentCollection $collection, $element, $addFilters)
692
    {
693 9
        $association       = $collection->getMapping();
694 9
        $owningAssociation = $association;
695
696 9
        if (! $association->isOwningSide()) {
697 4
            $sourceClass      = $this->em->getClassMetadata($association->getTargetEntity());
698 4
            $targetClass      = $this->em->getClassMetadata($association->getSourceEntity());
699 4
            $sourceIdentifier = $this->uow->getEntityIdentifier($element);
700 4
            $targetIdentifier = $this->uow->getEntityIdentifier($collection->getOwner());
701
702 4
            $owningAssociation = $sourceClass->getProperty($association->getMappedBy());
703
        } else {
704 5
            $sourceClass      = $this->em->getClassMetadata($association->getSourceEntity());
705 5
            $targetClass      = $this->em->getClassMetadata($association->getTargetEntity());
706 5
            $sourceIdentifier = $this->uow->getEntityIdentifier($collection->getOwner());
707 5
            $targetIdentifier = $this->uow->getEntityIdentifier($element);
708
        }
709
710 9
        $joinTable       = $owningAssociation->getJoinTable();
711 9
        $joinTableName   = $joinTable->getQuotedQualifiedName($this->platform);
712 9
        $quotedJoinTable = $joinTableName;
713 9
        $whereClauses    = [];
714 9
        $params          = [];
715 9
        $types           = [];
716
717 9
        foreach ($joinTable->getJoinColumns() as $joinColumn) {
718
            /** @var JoinColumnMetadata $joinColumn */
719 9
            $quotedColumnName     = $this->platform->quoteIdentifier($joinColumn->getColumnName());
720 9
            $referencedColumnName = $joinColumn->getReferencedColumnName();
721
722 9
            $whereClauses[] = ($addFilters ? 't.' : '') . $quotedColumnName . ' = ?';
723 9
            $params[]       = $sourceIdentifier[$sourceClass->fieldNames[$referencedColumnName]];
0 ignored issues
show
Bug introduced by
Accessing fieldNames on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
724 9
            $types[]        = $joinColumn->getType();
725
        }
726
727 9
        foreach ($joinTable->getInverseJoinColumns() as $joinColumn) {
728
            /** @var JoinColumnMetadata $joinColumn */
729 9
            $quotedColumnName     = $this->platform->quoteIdentifier($joinColumn->getColumnName());
730 9
            $referencedColumnName = $joinColumn->getReferencedColumnName();
731
732 9
            $whereClauses[] = ($addFilters ? 't.' : '') . $quotedColumnName . ' = ?';
733 9
            $params[]       = $targetIdentifier[$targetClass->fieldNames[$referencedColumnName]];
734 9
            $types[]        = $joinColumn->getType();
735
        }
736
737 9
        if ($addFilters) {
738 7
            $quotedJoinTable .= ' t';
739
740 7
            [$joinTargetEntitySQL, $filterSql] = $this->getFilterSql($association);
741
742 7
            if ($filterSql) {
743 3
                $quotedJoinTable .= ' ' . $joinTargetEntitySQL;
744 3
                $whereClauses[]   = $filterSql;
745
            }
746
        }
747
748 9
        return [$quotedJoinTable, $whereClauses, $params, $types];
749
    }
750
751
    /**
752
     * Expands Criteria Parameters by walking the expressions and grabbing all
753
     * parameters and types from it.
754
     *
755
     * @return mixed[]
756
     */
757 12
    private function expandCriteriaParameters(Criteria $criteria)
758
    {
759 12
        $expression = $criteria->getWhereExpression();
760
761 12
        if ($expression === null) {
762 5
            return [];
763
        }
764
765 7
        $valueVisitor = new SqlValueVisitor();
766
767 7
        $valueVisitor->dispatch($expression);
768
769 7
        [, $types] = $valueVisitor->getParamsAndTypes();
770
771 7
        return $types;
772
    }
773
774
    /**
775
     * @return string
776
     */
777 12
    private function getOrderingSql(Criteria $criteria, ClassMetadata $targetClass)
778
    {
779 12
        $orderings = $criteria->getOrderings();
780
781 12
        if ($orderings) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $orderings of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
782 3
            $orderBy = [];
783
784 3
            foreach ($orderings as $name => $direction) {
785 3
                $property   = $targetClass->getProperty($name);
786 3
                $columnName = $this->platform->quoteIdentifier($property->getColumnName());
0 ignored issues
show
Bug introduced by
The method getColumnName() does not exist on Doctrine\ORM\Mapping\Property. It seems like you code against a sub-type of Doctrine\ORM\Mapping\Property such as Doctrine\ORM\Mapping\FieldMetadata. ( Ignorable by Annotation )

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

786
                $columnName = $this->platform->quoteIdentifier($property->/** @scrutinizer ignore-call */ getColumnName());
Loading history...
787
788 3
                $orderBy[] = $columnName . ' ' . $direction;
789
            }
790
791 3
            return ' ORDER BY ' . implode(', ', $orderBy);
792
        }
793
794 9
        return '';
795
    }
796
797
    /**
798
     * @return string
799
     *
800
     * @throws DBALException
801
     */
802 12
    private function getLimitSql(Criteria $criteria)
803
    {
804 12
        $limit  = $criteria->getMaxResults();
805 12
        $offset = $criteria->getFirstResult();
806 12
        if ($limit !== null || $offset !== null) {
807 3
            return $this->platform->modifyLimitQuery('', $limit, $offset ?? 0);
808
        }
809
810 9
        return '';
811
    }
812
}
813