Failed Conditions
Push — master ( 8601d9...376c5e )
by Luís
09:32 queued 11s
created

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

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;
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());
45 18
        $joinTable = $association->getJoinTable();
46 18
        $types     = [];
47
48 18
        foreach ($joinTable->getJoinColumns() as $joinColumn) {
49
            /** @var JoinColumnMetadata $joinColumn */
50 18
            $referencedColumnName = $joinColumn->getReferencedColumnName();
51
52 18
            if (! $joinColumn->getType()) {
53
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $class, $this->em));
0 ignored issues
show
$class of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Utility\Per...lper::getTypeOfColumn(). ( Ignorable by Annotation )

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

53
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, /** @scrutinizer ignore-type */ $class, $this->em));
Loading history...
54
            }
55
56 18
            $types[] = $joinColumn->getType();
57
        }
58
59 18
        $sql    = $this->getDeleteSQL($collection);
60 18
        $params = $this->getDeleteSQLParameters($collection);
61
62 18
        $this->conn->executeUpdate($sql, $params, $types);
63 18
    }
64
65
    /**
66
     * {@inheritdoc}
67
     */
68 329
    public function update(PersistentCollection $collection)
69
    {
70 329
        $association = $collection->getMapping();
71
72 329
        if (! $association->isOwningSide()) {
73 236
            return; // ignore inverse side
74
        }
75
76 328
        [$deleteSql, $deleteTypes] = $this->getDeleteRowSQL($collection);
77 328
        [$insertSql, $insertTypes] = $this->getInsertRowSQL($collection);
78
79 328
        foreach ($collection->getDeleteDiff() as $element) {
80 10
            $this->conn->executeUpdate(
81 10
                $deleteSql,
82 10
                $this->getDeleteRowSQLParameters($collection, $element),
83 10
                $deleteTypes
84
            );
85
        }
86
87 328
        foreach ($collection->getInsertDiff() as $element) {
88 328
            $this->conn->executeUpdate(
89 328
                $insertSql,
90 328
                $this->getInsertRowSQLParameters($collection, $element),
91 328
                $insertTypes
92
            );
93
        }
94 328
    }
95
96
    /**
97
     * {@inheritdoc}
98
     */
99 3
    public function get(PersistentCollection $collection, $index)
100
    {
101 3
        $association = $collection->getMapping();
102
103 3
        if (! ($association instanceof ToManyAssociationMetadata && $association->getIndexedBy())) {
104
            throw new BadMethodCallException('Selecting a collection by index is only supported on indexed collections.');
105
        }
106
107 3
        $persister = $this->uow->getEntityPersister($association->getTargetEntity());
108 3
        $mappedKey = $association->isOwningSide()
109 2
            ? $association->getInversedBy()
110 3
            : $association->getMappedBy();
111
112
        $criteria = [
113 3
            $mappedKey                   => $collection->getOwner(),
114 3
            $association->getIndexedBy() => $index,
115
        ];
116
117 3
        return $persister->load($criteria, null, $association, [], 0, 1);
118
    }
119
120
    /**
121
     * {@inheritdoc}
122
     */
123 18
    public function count(PersistentCollection $collection)
124
    {
125 18
        $conditions        = [];
126 18
        $params            = [];
127 18
        $types             = [];
128 18
        $association       = $collection->getMapping();
129 18
        $identifier        = $this->uow->getEntityIdentifier($collection->getOwner());
130 18
        $sourceClass       = $this->em->getClassMetadata($association->getSourceEntity());
131 18
        $targetClass       = $this->em->getClassMetadata($association->getTargetEntity());
132 18
        $owningAssociation = ! $association->isOwningSide()
133 4
            ? $targetClass->getProperty($association->getMappedBy())
134 18
            : $association;
135
136 18
        $joinTable     = $owningAssociation->getJoinTable();
137 18
        $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
138 18
        $joinColumns   = $association->isOwningSide()
139 14
            ? $joinTable->getJoinColumns()
140 18
            : $joinTable->getInverseJoinColumns();
141
142 18
        foreach ($joinColumns as $joinColumn) {
143
            /** @var JoinColumnMetadata $joinColumn */
144 18
            $quotedColumnName     = $this->platform->quoteIdentifier($joinColumn->getColumnName());
145 18
            $referencedColumnName = $joinColumn->getReferencedColumnName();
146
147 18
            if (! $joinColumn->getType()) {
148 1
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $sourceClass, $this->em));
0 ignored issues
show
$sourceClass of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Utility\Per...lper::getTypeOfColumn(). ( Ignorable by Annotation )

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

148
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, /** @scrutinizer ignore-type */ $sourceClass, $this->em));
Loading history...
149
            }
150
151 18
            $conditions[] = sprintf('t.%s = ?', $quotedColumnName);
152 18
            $params[]     = $identifier[$sourceClass->fieldNames[$referencedColumnName]];
153 18
            $types[]      = $joinColumn->getType();
154
        }
155
156 18
        [$joinTargetEntitySQL, $filterSql] = $this->getFilterSql($association);
157
158 18
        if ($filterSql) {
159 3
            $conditions[] = $filterSql;
160
        }
161
162
        // If there is a provided criteria, make part of conditions
163
        // @todo Fix this. Current SQL returns something like:
164
        /*if ($criteria && ($expression = $criteria->getWhereExpression()) !== null) {
165
            // A join is needed on the target entity
166
            $targetTableName = $targetClass->table->getQuotedQualifiedName($this->platform);
167
            $targetJoinSql   = ' JOIN ' . $targetTableName . ' te'
168
                . ' ON' . implode(' AND ', $this->getOnConditionSQL($association));
169
170
            // And criteria conditions needs to be added
171
            $persister    = $this->uow->getEntityPersister($targetClass->getClassName());
172
            $visitor      = new SqlExpressionVisitor($persister, $targetClass);
173
            $conditions[] = $visitor->dispatch($expression);
174
175
            $joinTargetEntitySQL = $targetJoinSql . $joinTargetEntitySQL;
176
        }*/
177
178
        $sql = 'SELECT COUNT(*)'
179 18
            . ' FROM ' . $joinTableName . ' t'
180 18
            . $joinTargetEntitySQL
181 18
            . ' WHERE ' . implode(' AND ', $conditions);
182
183 18
        return $this->conn->fetchColumn($sql, $params, 0, $types);
184
    }
185
186
    /**
187
     * {@inheritDoc}
188
     */
189 8
    public function slice(PersistentCollection $collection, $offset, $length = null)
190
    {
191 8
        $association = $collection->getMapping();
192 8
        $persister   = $this->uow->getEntityPersister($association->getTargetEntity());
193
194 8
        return $persister->getManyToManyCollection($association, $collection->getOwner(), $offset, $length);
195
    }
196
197
    /**
198
     * {@inheritdoc}
199
     */
200 7
    public function containsKey(PersistentCollection $collection, $key)
201
    {
202 7
        $association = $collection->getMapping();
203
204 7
        if (! ($association instanceof ToManyAssociationMetadata && $association->getIndexedBy())) {
205
            throw new BadMethodCallException('Selecting a collection by index is only supported on indexed collections.');
206
        }
207
208 7
        [$quotedJoinTable, $whereClauses, $params, $types] = $this->getJoinTableRestrictionsWithKey($collection, $key, true);
209
210 7
        $sql = 'SELECT 1 FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses);
211
212 7
        return (bool) $this->conn->fetchColumn($sql, $params, 0, $types);
213
    }
214
215
    /**
216
     * {@inheritDoc}
217
     */
218 7
    public function contains(PersistentCollection $collection, $element)
219
    {
220 7
        if (! $this->isValidEntityState($element)) {
221 2
            return false;
222
        }
223
224 7
        [$quotedJoinTable, $whereClauses, $params, $types] = $this->getJoinTableRestrictions($collection, $element, true);
225
226 7
        $sql = 'SELECT 1 FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses);
227
228 7
        return (bool) $this->conn->fetchColumn($sql, $params, 0, $types);
229
    }
230
231
    /**
232
     * {@inheritDoc}
233
     */
234 2
    public function removeElement(PersistentCollection $collection, $element)
235
    {
236 2
        if (! $this->isValidEntityState($element)) {
237 2
            return false;
238
        }
239
240 2
        [$quotedJoinTable, $whereClauses, $params, $types] = $this->getJoinTableRestrictions($collection, $element, false);
241
242 2
        $sql = 'DELETE FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses);
243
244 2
        return (bool) $this->conn->executeUpdate($sql, $params, $types);
245
    }
246
247
    /**
248
     * {@inheritDoc}
249
     */
250 12
    public function loadCriteria(PersistentCollection $collection, Criteria $criteria)
251
    {
252 12
        $association   = $collection->getMapping();
253 12
        $owner         = $collection->getOwner();
254 12
        $ownerMetadata = $this->em->getClassMetadata(get_class($owner));
255 12
        $identifier    = $this->uow->getEntityIdentifier($owner);
256 12
        $targetClass   = $this->em->getClassMetadata($association->getTargetEntity());
257 12
        $onConditions  = $this->getOnConditionSQL($association);
258 12
        $whereClauses  = $params = $types = [];
259
260 12
        if (! $association->isOwningSide()) {
261 1
            $association = $targetClass->getProperty($association->getMappedBy());
262 1
            $joinColumns = $association->getJoinTable()->getInverseJoinColumns();
263
        } else {
264 11
            $joinColumns = $association->getJoinTable()->getJoinColumns();
265
        }
266
267 12
        foreach ($joinColumns as $joinColumn) {
268
            /** @var JoinColumnMetadata $joinColumn */
269 12
            $quotedColumnName     = $this->platform->quoteIdentifier($joinColumn->getColumnName());
270 12
            $referencedColumnName = $joinColumn->getReferencedColumnName();
271
272 12
            if (! $joinColumn->getType()) {
273
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $ownerMetadata, $this->em));
0 ignored issues
show
$ownerMetadata of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Utility\Per...lper::getTypeOfColumn(). ( Ignorable by Annotation )

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

273
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, /** @scrutinizer ignore-type */ $ownerMetadata, $this->em));
Loading history...
274
            }
275
276 12
            $whereClauses[] = sprintf('t.%s = ?', $quotedColumnName);
277 12
            $params[]       = $identifier[$ownerMetadata->fieldNames[$referencedColumnName]];
278 12
            $types[]        = $joinColumn->getType();
279
        }
280
281 12
        $parameters = $this->expandCriteriaParameters($criteria);
282
283 12
        foreach ($parameters as $parameter) {
284 7
            [$name, $value, $operator] = $parameter;
285
286 7
            $property   = $targetClass->getProperty($name);
287 7
            $columnName = $this->platform->quoteIdentifier($property->getColumnName());
288
289 7
            $whereClauses[] = sprintf('te.%s %s ?', $columnName, $operator);
290 7
            $params[]       = $value;
291 7
            $types[]        = $property->getType();
292
        }
293
294 12
        $tableName        = $targetClass->table->getQuotedQualifiedName($this->platform);
295 12
        $joinTableName    = $association->getJoinTable()->getQuotedQualifiedName($this->platform);
296 12
        $resultSetMapping = new Query\ResultSetMappingBuilder($this->em);
297
298 12
        $resultSetMapping->addRootEntityFromClassMetadata($targetClass->getClassName(), 'te');
299
300 12
        $sql = 'SELECT ' . $resultSetMapping->generateSelectClause()
301 12
            . ' FROM ' . $tableName . ' te'
302 12
            . ' JOIN ' . $joinTableName . ' t ON'
303 12
            . implode(' AND ', $onConditions)
304 12
            . ' WHERE ' . implode(' AND ', $whereClauses);
305
306 12
        $sql .= $this->getOrderingSql($criteria, $targetClass);
0 ignored issues
show
$targetClass of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $targetClass of Doctrine\ORM\Persisters\...ister::getOrderingSql(). ( Ignorable by Annotation )

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

306
        $sql .= $this->getOrderingSql($criteria, /** @scrutinizer ignore-type */ $targetClass);
Loading history...
307 12
        $sql .= $this->getLimitSql($criteria);
308
309 12
        $stmt = $this->conn->executeQuery($sql, $params, $types);
310
311 12
        return $this->em->newHydrator(Query::HYDRATE_OBJECT)->hydrateAll($stmt, $resultSetMapping);
312
    }
313
314
    /**
315
     * Generates the filter SQL for a given mapping.
316
     *
317
     * This method is not used for actually grabbing the related entities
318
     * but when the extra-lazy collection methods are called on a filtered
319
     * association. This is why besides the many to many table we also
320
     * have to join in the actual entities table leading to additional
321
     * JOIN.
322
     *
323
     * @return string[] ordered tuple:
324
     *                   - JOIN condition to add to the SQL
325
     *                   - WHERE condition to add to the SQL
326
     */
327 32
    public function getFilterSql(ManyToManyAssociationMetadata $association)
328
    {
329 32
        $targetClass = $this->em->getClassMetadata($association->getTargetEntity());
330 32
        $rootClass   = $this->em->getClassMetadata($targetClass->getRootClassName());
331 32
        $filterSql   = $this->generateFilterConditionSQL($rootClass, 'te');
0 ignored issues
show
$rootClass of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $targetEntity of Doctrine\ORM\Persisters\...ateFilterConditionSQL(). ( Ignorable by Annotation )

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

331
        $filterSql   = $this->generateFilterConditionSQL(/** @scrutinizer ignore-type */ $rootClass, 'te');
Loading history...
332
333 32
        if ($filterSql === '') {
334 32
            return ['', ''];
335
        }
336
337
        // A join is needed if there is filtering on the target entity
338 6
        $tableName = $rootClass->table->getQuotedQualifiedName($this->platform);
339 6
        $joinSql   = ' JOIN ' . $tableName . ' te'
340 6
            . ' ON' . implode(' AND ', $this->getOnConditionSQL($association));
341
342 6
        return [$joinSql, $filterSql];
343
    }
344
345
    /**
346
     * Generates the filter SQL for a given entity and table alias.
347
     *
348
     * @param ClassMetadata $targetEntity     Metadata of the target entity.
349
     * @param string        $targetTableAlias The table alias of the joined/selected table.
350
     *
351
     * @return string The SQL query part to add to a query.
352
     */
353 32
    protected function generateFilterConditionSQL(ClassMetadata $targetEntity, $targetTableAlias)
354
    {
355 32
        $filterClauses = [];
356
357 32
        foreach ($this->em->getFilters()->getEnabledFilters() as $filter) {
358 6
            $filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias);
359
360 6
            if ($filterExpr) {
361 6
                $filterClauses[] = '(' . $filterExpr . ')';
362
            }
363
        }
364
365 32
        if (! $filterClauses) {
366 32
            return '';
367
        }
368
369 6
        $filterSql = implode(' AND ', $filterClauses);
370
371 6
        return isset($filterClauses[1])
372
            ? '(' . $filterSql . ')'
373 6
            : $filterSql;
374
    }
375
376
    /**
377
     * Generate ON condition
378
     *
379
     * @return string[]
380
     */
381 18
    protected function getOnConditionSQL(ManyToManyAssociationMetadata $association)
382
    {
383 18
        $targetClass       = $this->em->getClassMetadata($association->getTargetEntity());
384 18
        $owningAssociation = ! $association->isOwningSide()
385 3
            ? $targetClass->getProperty($association->getMappedBy())
386 18
            : $association;
387
388 18
        $joinTable   = $owningAssociation->getJoinTable();
389 18
        $joinColumns = $association->isOwningSide()
390 15
            ? $joinTable->getInverseJoinColumns()
391 18
            : $joinTable->getJoinColumns();
392
393 18
        $conditions = [];
394
395 18
        foreach ($joinColumns as $joinColumn) {
396 18
            $quotedColumnName           = $this->platform->quoteIdentifier($joinColumn->getColumnName());
397 18
            $quotedReferencedColumnName = $this->platform->quoteIdentifier($joinColumn->getReferencedColumnName());
398
399 18
            $conditions[] = ' t.' . $quotedColumnName . ' = te.' . $quotedReferencedColumnName;
400
        }
401
402 18
        return $conditions;
403
    }
404
405
    /**
406
     * {@inheritdoc}
407
     *
408
     * @override
409
     */
410 18
    protected function getDeleteSQL(PersistentCollection $collection)
411
    {
412 18
        $association   = $collection->getMapping();
413 18
        $joinTable     = $association->getJoinTable();
414 18
        $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
415 18
        $columns       = [];
416
417 18
        foreach ($joinTable->getJoinColumns() as $joinColumn) {
418 18
            $columns[] = $this->platform->quoteIdentifier($joinColumn->getColumnName());
419
        }
420
421 18
        return 'DELETE FROM ' . $joinTableName . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?';
422
    }
423
424
    /**
425
     * {@inheritdoc}
426
     *
427
     * {@internal Order of the parameters must be the same as the order of the columns in getDeleteSql. }}
428
     *
429
     * @override
430
     */
431 18
    protected function getDeleteSQLParameters(PersistentCollection $collection)
432
    {
433 18
        $association = $collection->getMapping();
434 18
        $identifier  = $this->uow->getEntityIdentifier($collection->getOwner());
435 18
        $joinTable   = $association->getJoinTable();
436 18
        $joinColumns = $joinTable->getJoinColumns();
437
438
        // Optimization for single column identifier
439 18
        if (count($joinColumns) === 1) {
440 15
            return [reset($identifier)];
441
        }
442
443
        // Composite identifier
444 3
        $sourceClass = $this->em->getClassMetadata($association->getSourceEntity());
445 3
        $params      = [];
446
447 3
        foreach ($joinColumns as $joinColumn) {
448 3
            $params[] = $identifier[$sourceClass->fieldNames[$joinColumn->getReferencedColumnName()]];
449
        }
450
451 3
        return $params;
452
    }
453
454
    /**
455
     * Gets the SQL statement used for deleting a row from the collection.
456
     *
457
     * @return string[]|string[][] ordered tuple containing the SQL to be executed and an array
458
     *                             of types for bound parameters
459
     */
460 328
    protected function getDeleteRowSQL(PersistentCollection $collection)
461
    {
462 328
        $association = $collection->getMapping();
463 328
        $class       = $this->em->getClassMetadata($association->getSourceEntity());
464 328
        $targetClass = $this->em->getClassMetadata($association->getTargetEntity());
465 328
        $columns     = [];
466 328
        $types       = [];
467
468 328
        $joinTable     = $association->getJoinTable();
469 328
        $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
470
471 328
        foreach ($joinTable->getJoinColumns() as $joinColumn) {
472
            /** @var JoinColumnMetadata $joinColumn */
473 328
            $quotedColumnName     = $this->platform->quoteIdentifier($joinColumn->getColumnName());
474 328
            $referencedColumnName = $joinColumn->getReferencedColumnName();
475
476 328
            if (! $joinColumn->getType()) {
477 34
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $class, $this->em));
0 ignored issues
show
$class of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Utility\Per...lper::getTypeOfColumn(). ( Ignorable by Annotation )

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

477
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, /** @scrutinizer ignore-type */ $class, $this->em));
Loading history...
478
            }
479
480 328
            $columns[] = $quotedColumnName;
481 328
            $types[]   = $joinColumn->getType();
482
        }
483
484 328
        foreach ($joinTable->getInverseJoinColumns() as $joinColumn) {
485
            /** @var JoinColumnMetadata $joinColumn */
486 328
            $quotedColumnName     = $this->platform->quoteIdentifier($joinColumn->getColumnName());
487 328
            $referencedColumnName = $joinColumn->getReferencedColumnName();
488
489 328
            if (! $joinColumn->getType()) {
490 34
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
491
            }
492
493 328
            $columns[] = $quotedColumnName;
494 328
            $types[]   = $joinColumn->getType();
495
        }
496
497
        return [
498 328
            sprintf('DELETE FROM %s WHERE %s = ?', $joinTableName, implode(' = ? AND ', $columns)),
499 328
            $types,
500
        ];
501
    }
502
503
    /**
504
     * Gets the SQL parameters for the corresponding SQL statement to delete the given
505
     * element from the given collection.
506
     *
507
     * {@internal Order of the parameters must be the same as the order of the columns in getDeleteRowSql. }}
508
     *
509
     * @param mixed $element
510
     *
511
     * @return mixed[]
512
     */
513 10
    protected function getDeleteRowSQLParameters(PersistentCollection $collection, $element)
514
    {
515 10
        return $this->collectJoinTableColumnParameters($collection, $element);
516
    }
517
518
    /**
519
     * Gets the SQL statement used for inserting a row in the collection.
520
     *
521
     * @return string[]|string[][] ordered tuple containing the SQL to be executed and an array
522
     *                             of types for bound parameters
523
     */
524 328
    protected function getInsertRowSQL(PersistentCollection $collection)
525
    {
526 328
        $association = $collection->getMapping();
527 328
        $class       = $this->em->getClassMetadata($association->getSourceEntity());
528 328
        $targetClass = $this->em->getClassMetadata($association->getTargetEntity());
529 328
        $columns     = [];
530 328
        $types       = [];
531
532 328
        $joinTable     = $association->getJoinTable();
533 328
        $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
534
535 328
        foreach ($joinTable->getJoinColumns() as $joinColumn) {
536
            /** @var JoinColumnMetadata $joinColumn */
537 328
            $quotedColumnName     = $this->platform->quoteIdentifier($joinColumn->getColumnName());
538 328
            $referencedColumnName = $joinColumn->getReferencedColumnName();
539
540 328
            if (! $joinColumn->getType()) {
541
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $class, $this->em));
0 ignored issues
show
$class of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Utility\Per...lper::getTypeOfColumn(). ( Ignorable by Annotation )

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

541
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, /** @scrutinizer ignore-type */ $class, $this->em));
Loading history...
542
            }
543
544 328
            $columns[] = $quotedColumnName;
545 328
            $types[]   = $joinColumn->getType();
546
        }
547
548 328
        foreach ($joinTable->getInverseJoinColumns() as $joinColumn) {
549
            /** @var JoinColumnMetadata $joinColumn */
550 328
            $quotedColumnName     = $this->platform->quoteIdentifier($joinColumn->getColumnName());
551 328
            $referencedColumnName = $joinColumn->getReferencedColumnName();
552
553 328
            if (! $joinColumn->getType()) {
554
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
555
            }
556
557 328
            $columns[] = $quotedColumnName;
558 328
            $types[]   = $joinColumn->getType();
559
        }
560
561 328
        $columnNamesAsString  = implode(', ', $columns);
562 328
        $columnValuesAsString = implode(', ', array_fill(0, count($columns), '?'));
563
564
        return [
565 328
            sprintf('INSERT INTO %s (%s) VALUES (%s)', $joinTableName, $columnNamesAsString, $columnValuesAsString),
566 328
            $types,
567
        ];
568
    }
569
570
    /**
571
     * Gets the SQL parameters for the corresponding SQL statement to insert the given
572
     * element of the given collection into the database.
573
     *
574
     * {@internal Order of the parameters must be the same as the order of the columns in getInsertRowSql. }}
575
     *
576
     * @param mixed $element
577
     *
578
     * @return mixed[]
579
     */
580 328
    protected function getInsertRowSQLParameters(PersistentCollection $collection, $element)
581
    {
582 328
        return $this->collectJoinTableColumnParameters($collection, $element);
583
    }
584
585
    /**
586
     * Collects the parameters for inserting/deleting on the join table in the order
587
     * of the join table columns.
588
     *
589
     * @param object $element
590
     *
591
     * @return mixed[]
592
     */
593 328
    private function collectJoinTableColumnParameters(PersistentCollection $collection, $element)
594
    {
595 328
        $params           = [];
596 328
        $association      = $collection->getMapping();
597 328
        $owningClass      = $this->em->getClassMetadata(get_class($collection->getOwner()));
598 328
        $targetClass      = $collection->getTypeClass();
599 328
        $owningIdentifier = $this->uow->getEntityIdentifier($collection->getOwner());
600 328
        $targetIdentifier = $this->uow->getEntityIdentifier($element);
601 328
        $joinTable        = $association->getJoinTable();
602
603 328
        foreach ($joinTable->getJoinColumns() as $joinColumn) {
604 328
            $fieldName = $owningClass->fieldNames[$joinColumn->getReferencedColumnName()];
605
606 328
            $params[] = $owningIdentifier[$fieldName];
607
        }
608
609 328
        foreach ($joinTable->getInverseJoinColumns() as $joinColumn) {
610 328
            $fieldName = $targetClass->fieldNames[$joinColumn->getReferencedColumnName()];
611
612 328
            $params[] = $targetIdentifier[$fieldName];
613
        }
614
615 328
        return $params;
616
    }
617
618
    /**
619
     * @param string $key
620
     * @param bool   $addFilters Whether the filter SQL should be included or not.
621
     *
622
     * @return mixed[] ordered vector:
623
     *                - quoted join table name
624
     *                - where clauses to be added for filtering
625
     *                - parameters to be bound for filtering
626
     *                - types of the parameters to be bound for filtering
627
     */
628 7
    private function getJoinTableRestrictionsWithKey(PersistentCollection $collection, $key, $addFilters)
629
    {
630 7
        $association       = $collection->getMapping();
631 7
        $owningAssociation = $association;
632 7
        $indexBy           = $owningAssociation->getIndexedBy();
633 7
        $identifier        = $this->uow->getEntityIdentifier($collection->getOwner());
634 7
        $sourceClass       = $this->em->getClassMetadata($owningAssociation->getSourceEntity());
635 7
        $targetClass       = $this->em->getClassMetadata($owningAssociation->getTargetEntity());
636
637 7
        if (! $owningAssociation->isOwningSide()) {
638 3
            $owningAssociation  = $targetClass->getProperty($owningAssociation->getMappedBy());
639 3
            $joinTable          = $owningAssociation->getJoinTable();
640 3
            $joinColumns        = $joinTable->getJoinColumns();
641 3
            $inverseJoinColumns = $joinTable->getInverseJoinColumns();
642
        } else {
643 4
            $joinTable          = $owningAssociation->getJoinTable();
644 4
            $joinColumns        = $joinTable->getInverseJoinColumns();
645 4
            $inverseJoinColumns = $joinTable->getJoinColumns();
646
        }
647
648 7
        $joinTableName   = $joinTable->getQuotedQualifiedName($this->platform);
649 7
        $quotedJoinTable = $joinTableName . ' t';
650 7
        $whereClauses    = [];
651 7
        $params          = [];
652 7
        $types           = [];
653 7
        $joinNeeded      = ! in_array($indexBy, $targetClass->identifier, true);
654
655 7
        if ($joinNeeded) { // extra join needed if indexBy is not a @id
656 3
            $joinConditions = [];
657
658 3
            foreach ($joinColumns as $joinColumn) {
659
                /** @var JoinColumnMetadata $joinColumn */
660 3
                $quotedColumnName           = $this->platform->quoteIdentifier($joinColumn->getColumnName());
661 3
                $quotedReferencedColumnName = $this->platform->quoteIdentifier($joinColumn->getReferencedColumnName());
662
663 3
                $joinConditions[] = ' t.' . $quotedColumnName . ' = tr.' . $quotedReferencedColumnName;
664
            }
665
666 3
            $tableName        = $targetClass->table->getQuotedQualifiedName($this->platform);
667 3
            $quotedJoinTable .= ' JOIN ' . $tableName . ' tr ON ' . implode(' AND ', $joinConditions);
668 3
            $indexByProperty  = $targetClass->getProperty($indexBy);
669
670
            switch (true) {
671 3
                case $indexByProperty instanceof FieldMetadata:
672 3
                    $quotedColumnName = $this->platform->quoteIdentifier($indexByProperty->getColumnName());
673
674 3
                    $whereClauses[] = sprintf('tr.%s = ?', $quotedColumnName);
675 3
                    $params[]       = $key;
676 3
                    $types[]        = $indexByProperty->getType();
677 3
                    break;
678
679
                case $indexByProperty instanceof ToOneAssociationMetadata && $indexByProperty->isOwningSide():
680
                    // Cannot be supported because PHP does not accept objects as keys. =(
681
                    break;
682
            }
683
        }
684
685 7
        foreach ($inverseJoinColumns as $joinColumn) {
686
            /** @var JoinColumnMetadata $joinColumn */
687 7
            $quotedColumnName     = $this->platform->quoteIdentifier($joinColumn->getColumnName());
688 7
            $referencedColumnName = $joinColumn->getReferencedColumnName();
689
690 7
            if (! $joinColumn->getType()) {
691
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $sourceClass, $this->em));
0 ignored issues
show
$sourceClass of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Utility\Per...lper::getTypeOfColumn(). ( Ignorable by Annotation )

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

691
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, /** @scrutinizer ignore-type */ $sourceClass, $this->em));
Loading history...
692
            }
693
694 7
            $whereClauses[] = sprintf('t.%s = ?', $quotedColumnName);
695 7
            $params[]       = $identifier[$sourceClass->fieldNames[$joinColumn->getReferencedColumnName()]];
696 7
            $types[]        = $joinColumn->getType();
697
        }
698
699 7
        if (! $joinNeeded) {
700 4
            foreach ($joinColumns as $joinColumn) {
701
                /** @var JoinColumnMetadata $joinColumn */
702 4
                $quotedColumnName     = $this->platform->quoteIdentifier($joinColumn->getColumnName());
703 4
                $referencedColumnName = $joinColumn->getReferencedColumnName();
704
705 4
                if (! $joinColumn->getType()) {
706
                    $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
707
                }
708
709 4
                $whereClauses[] = sprintf('t.%s = ?', $quotedColumnName);
710 4
                $params[]       = $key;
711 4
                $types[]        = $joinColumn->getType();
712
            }
713
        }
714
715 7
        if ($addFilters) {
716 7
            [$joinTargetEntitySQL, $filterSql] = $this->getFilterSql($association);
717
718 7
            if ($filterSql) {
719
                $quotedJoinTable .= ' ' . $joinTargetEntitySQL;
720
                $whereClauses[]   = $filterSql;
721
            }
722
        }
723
724 7
        return [$quotedJoinTable, $whereClauses, $params, $types];
725
    }
726
727
    /**
728
     * @param object $element
729
     * @param bool   $addFilters Whether the filter SQL should be included or not.
730
     *
731
     * @return mixed[] ordered vector:
732
     *                - quoted join table name
733
     *                - where clauses to be added for filtering
734
     *                - parameters to be bound for filtering
735
     *                - types of the parameters to be bound for filtering
736
     */
737 9
    private function getJoinTableRestrictions(PersistentCollection $collection, $element, $addFilters)
738
    {
739 9
        $association       = $collection->getMapping();
740 9
        $owningAssociation = $association;
741
742 9
        if (! $association->isOwningSide()) {
743 4
            $sourceClass      = $this->em->getClassMetadata($association->getTargetEntity());
744 4
            $targetClass      = $this->em->getClassMetadata($association->getSourceEntity());
745 4
            $sourceIdentifier = $this->uow->getEntityIdentifier($element);
746 4
            $targetIdentifier = $this->uow->getEntityIdentifier($collection->getOwner());
747
748 4
            $owningAssociation = $sourceClass->getProperty($association->getMappedBy());
749
        } else {
750 5
            $sourceClass      = $this->em->getClassMetadata($association->getSourceEntity());
751 5
            $targetClass      = $this->em->getClassMetadata($association->getTargetEntity());
752 5
            $sourceIdentifier = $this->uow->getEntityIdentifier($collection->getOwner());
753 5
            $targetIdentifier = $this->uow->getEntityIdentifier($element);
754
        }
755
756 9
        $joinTable       = $owningAssociation->getJoinTable();
757 9
        $joinTableName   = $joinTable->getQuotedQualifiedName($this->platform);
758 9
        $quotedJoinTable = $joinTableName;
759 9
        $whereClauses    = [];
760 9
        $params          = [];
761 9
        $types           = [];
762
763 9
        foreach ($joinTable->getJoinColumns() as $joinColumn) {
764
            /** @var JoinColumnMetadata $joinColumn */
765 9
            $quotedColumnName     = $this->platform->quoteIdentifier($joinColumn->getColumnName());
766 9
            $referencedColumnName = $joinColumn->getReferencedColumnName();
767
768 9
            if (! $joinColumn->getType()) {
769
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $sourceClass, $this->em));
0 ignored issues
show
$sourceClass of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Utility\Per...lper::getTypeOfColumn(). ( Ignorable by Annotation )

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

769
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, /** @scrutinizer ignore-type */ $sourceClass, $this->em));
Loading history...
770
            }
771
772 9
            $whereClauses[] = ($addFilters ? 't.' : '') . $quotedColumnName . ' = ?';
773 9
            $params[]       = $sourceIdentifier[$sourceClass->fieldNames[$referencedColumnName]];
774 9
            $types[]        = $joinColumn->getType();
775
        }
776
777 9
        foreach ($joinTable->getInverseJoinColumns() as $joinColumn) {
778
            /** @var JoinColumnMetadata $joinColumn */
779 9
            $quotedColumnName     = $this->platform->quoteIdentifier($joinColumn->getColumnName());
780 9
            $referencedColumnName = $joinColumn->getReferencedColumnName();
781
782 9
            if (! $joinColumn->getType()) {
783
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
784
            }
785
786 9
            $whereClauses[] = ($addFilters ? 't.' : '') . $quotedColumnName . ' = ?';
787 9
            $params[]       = $targetIdentifier[$targetClass->fieldNames[$referencedColumnName]];
788 9
            $types[]        = $joinColumn->getType();
789
        }
790
791 9
        if ($addFilters) {
792 7
            $quotedJoinTable .= ' t';
793
794 7
            [$joinTargetEntitySQL, $filterSql] = $this->getFilterSql($association);
795
796 7
            if ($filterSql) {
797 3
                $quotedJoinTable .= ' ' . $joinTargetEntitySQL;
798 3
                $whereClauses[]   = $filterSql;
799
            }
800
        }
801
802 9
        return [$quotedJoinTable, $whereClauses, $params, $types];
803
    }
804
805
    /**
806
     * Expands Criteria Parameters by walking the expressions and grabbing all
807
     * parameters and types from it.
808
     *
809
     * @return mixed[]
810
     */
811 12
    private function expandCriteriaParameters(Criteria $criteria)
812
    {
813 12
        $expression = $criteria->getWhereExpression();
814
815 12
        if ($expression === null) {
816 5
            return [];
817
        }
818
819 7
        $valueVisitor = new SqlValueVisitor();
820
821 7
        $valueVisitor->dispatch($expression);
822
823 7
        [, $types] = $valueVisitor->getParamsAndTypes();
824
825 7
        return $types;
826
    }
827
828
    /**
829
     * @return string
830
     */
831 12
    private function getOrderingSql(Criteria $criteria, ClassMetadata $targetClass)
832
    {
833 12
        $orderings = $criteria->getOrderings();
834
835 12
        if ($orderings) {
836 3
            $orderBy = [];
837
838 3
            foreach ($orderings as $name => $direction) {
839 3
                $property   = $targetClass->getProperty($name);
840 3
                $columnName = $this->platform->quoteIdentifier($property->getColumnName());
841
842 3
                $orderBy[] = $columnName . ' ' . $direction;
843
            }
844
845 3
            return ' ORDER BY ' . implode(', ', $orderBy);
846
        }
847
848 9
        return '';
849
    }
850
851
    /**
852
     * @return string
853
     *
854
     * @throws DBALException
855
     */
856 12
    private function getLimitSql(Criteria $criteria)
857
    {
858 12
        $limit  = $criteria->getMaxResults();
859 12
        $offset = $criteria->getFirstResult();
860 12
        if ($limit !== null || $offset !== null) {
861 3
            return $this->platform->modifyLimitQuery('', $limit, $offset ?? 0);
862
        }
863
864 9
        return '';
865
    }
866
}
867