Failed Conditions
Push — master ( a3e53b...559253 )
by Guilherme
14:58
created

ManyToManyPersister::loadCriteria()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 58
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 39
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 39
nc 8
nop 2
dl 0
loc 58
ccs 39
cts 39
cp 1
crap 4
rs 9.296
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 Doctrine\ORM\Utility\PersisterHelper;
0 ignored issues
show
Bug introduced by
The type Doctrine\ORM\Utility\PersisterHelper was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
introduced by
Type Doctrine\ORM\Utility\PersisterHelper is not used in this file.
Loading history...
20
use function array_fill;
21
use function count;
22
use function get_class;
23
use function implode;
24
use function in_array;
25
use function reset;
26
use function sprintf;
27
28
/**
29
 * Persister for many-to-many collections.
30
 */
31
class ManyToManyPersister extends AbstractCollectionPersister
32
{
33
    /**
34
     * {@inheritdoc}
35
     */
36 18
    public function delete(PersistentCollection $collection)
37
    {
38 18
        $association = $collection->getMapping();
39
40 18
        if (! $association->isOwningSide()) {
41
            return; // ignore inverse side
42
        }
43
44 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...
45 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

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

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

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

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

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