Failed Conditions
Pull Request — master (#7898)
by Guilherme
63:09
created

getJoinTableRestrictionsWithKey()   C

Complexity

Conditions 12
Paths 96

Size

Total Lines 87
Code Lines 56

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 51
CRAP Score 12.001

Importance

Changes 0
Metric Value
cc 12
eloc 56
nc 96
nop 3
dl 0
loc 87
ccs 51
cts 52
cp 0.9808
crap 12.001
rs 6.5333
c 0
b 0
f 0

How to fix   Long Method    Complexity   

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

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

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

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