Failed Conditions
Push — master ( ddb3cd...4476ec )
by Marco
11:47
created

ManyToManyPersister   F

Complexity

Total Complexity 94

Size/Duplication

Total Lines 835
Duplicated Lines 0 %

Test Coverage

Coverage 95.92%

Importance

Changes 0
Metric Value
dl 0
loc 835
ccs 376
cts 392
cp 0.9592
rs 1.263
c 0
b 0
f 0
wmc 94

24 Methods

Rating   Name   Duplication   Size   Complexity  
A getInsertRowSQLParameters() 0 3 1
B loadCriteria() 0 62 5
A expandCriteriaParameters() 0 15 2
A getLimitSql() 0 8 3
B count() 0 64 6
A containsKey() 0 13 3
A getFilterSql() 0 16 2
B generateFilterConditionSQL() 0 21 5
C getJoinTableRestrictions() 0 66 10
B delete() 0 27 4
A getDeleteSQLParameters() 0 21 3
B update() 0 24 4
A get() 0 20 4
B getDeleteRowSQL() 0 40 5
A getOrderingSql() 0 17 3
A getDeleteRowSQLParameters() 0 3 1
B getOnConditionSQL() 0 23 4
A collectJoinTableColumnParameters() 0 23 3
A getDeleteSQL() 0 12 2
D getJoinTableRestrictionsWithKey() 0 97 14
A slice() 0 6 1
B getInsertRowSQL() 0 43 5
A contains() 0 11 2
A removeElement() 0 11 2

How to fix   Complexity   

Complex Class

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

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

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

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM\Persisters\Collection;
6
7
use Doctrine\Common\Collections\Criteria;
8
use Doctrine\ORM\Mapping\ClassMetadata;
9
use Doctrine\ORM\Mapping\FieldMetadata;
10
use Doctrine\ORM\Mapping\JoinColumnMetadata;
11
use Doctrine\ORM\Mapping\ManyToManyAssociationMetadata;
12
use Doctrine\ORM\Mapping\ToManyAssociationMetadata;
13
use Doctrine\ORM\Mapping\ToOneAssociationMetadata;
14
use Doctrine\ORM\PersistentCollection;
15
use Doctrine\ORM\Persisters\SqlValueVisitor;
16
use Doctrine\ORM\Query;
17
use Doctrine\ORM\Utility\PersisterHelper;
18
19
/**
20
 * Persister for many-to-many collections.
21
 */
22
class ManyToManyPersister extends AbstractCollectionPersister
23
{
24
    /**
25
     * {@inheritdoc}
26
     */
27 17
    public function delete(PersistentCollection $collection)
28
    {
29 17
        $association = $collection->getMapping();
30
31 17
        if (! $association->isOwningSide()) {
32
            return; // ignore inverse side
33
        }
34
35 17
        $class     = $this->em->getClassMetadata($association->getSourceEntity());
36 17
        $joinTable = $association->getJoinTable();
37 17
        $types     = [];
38
39 17
        foreach ($joinTable->getJoinColumns() as $joinColumn) {
40
            /** @var JoinColumnMetadata $joinColumn */
41 17
            $referencedColumnName = $joinColumn->getReferencedColumnName();
42
43 17
            if (! $joinColumn->getType()) {
44
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $class, $this->em));
45
            }
46
47 17
            $types[] = $joinColumn->getType();
48
        }
49
50 17
        $sql    = $this->getDeleteSQL($collection);
51 17
        $params = $this->getDeleteSQLParameters($collection);
52
53 17
        $this->conn->executeUpdate($sql, $params, $types);
54 17
    }
55
56
    /**
57
     * {@inheritdoc}
58
     */
59 326
    public function update(PersistentCollection $collection)
60
    {
61 326
        $association = $collection->getMapping();
62
63 326
        if (! $association->isOwningSide()) {
64 235
            return; // ignore inverse side
65
        }
66
67 325
        list($deleteSql, $deleteTypes) = $this->getDeleteRowSQL($collection);
68 325
        list($insertSql, $insertTypes) = $this->getInsertRowSQL($collection);
69
70 325
        foreach ($collection->getDeleteDiff() as $element) {
71 10
            $this->conn->executeUpdate(
72 10
                $deleteSql,
73 10
                $this->getDeleteRowSQLParameters($collection, $element),
74 10
                $deleteTypes
75
            );
76
        }
77
78 325
        foreach ($collection->getInsertDiff() as $element) {
79 325
            $this->conn->executeUpdate(
80 325
                $insertSql,
81 325
                $this->getInsertRowSQLParameters($collection, $element),
82 325
                $insertTypes
83
            );
84
        }
85 325
    }
86
87
    /**
88
     * {@inheritdoc}
89
     */
90 3
    public function get(PersistentCollection $collection, $index)
91
    {
92 3
        $association = $collection->getMapping();
93
94 3
        if (! ($association instanceof ToManyAssociationMetadata && $association->getIndexedBy())) {
95
            throw new \BadMethodCallException('Selecting a collection by index is only supported on indexed collections.');
96
        }
97
98 3
        $persister = $this->uow->getEntityPersister($association->getTargetEntity());
99 3
        $mappedKey = $association->isOwningSide()
100 2
            ? $association->getInversedBy()
101 3
            : $association->getMappedBy()
102
        ;
103
104
        $criteria = [
105 3
            $mappedKey                   => $collection->getOwner(),
106 3
            $association->getIndexedBy() => $index,
107
        ];
108
109 3
        return $persister->load($criteria, null, $association, [], 0, 1);
110
    }
111
112
    /**
113
     * {@inheritdoc}
114
     */
115 17
    public function count(PersistentCollection $collection)
116
    {
117 17
        $conditions        = [];
118 17
        $params            = [];
119 17
        $types             = [];
120 17
        $association       = $collection->getMapping();
121 17
        $identifier        = $this->uow->getEntityIdentifier($collection->getOwner());
122 17
        $sourceClass       = $this->em->getClassMetadata($association->getSourceEntity());
123 17
        $targetClass       = $this->em->getClassMetadata($association->getTargetEntity());
124 17
        $owningAssociation = ! $association->isOwningSide()
125 4
            ? $targetClass->getProperty($association->getMappedBy())
126 17
            : $association
127
        ;
128
129 17
        $joinTable     = $owningAssociation->getJoinTable();
130 17
        $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
131 17
        $joinColumns   = $association->isOwningSide()
132 13
            ? $joinTable->getJoinColumns()
133 17
            : $joinTable->getInverseJoinColumns()
134
        ;
135
136 17
        foreach ($joinColumns as $joinColumn) {
137
            /** @var JoinColumnMetadata $joinColumn */
138 17
            $quotedColumnName     = $this->platform->quoteIdentifier($joinColumn->getColumnName());
139 17
            $referencedColumnName = $joinColumn->getReferencedColumnName();
140
141 17
            if (! $joinColumn->getType()) {
142 1
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $sourceClass, $this->em));
143
            }
144
145 17
            $conditions[] = sprintf('t.%s = ?', $quotedColumnName);
146 17
            $params[]     = $identifier[$sourceClass->fieldNames[$referencedColumnName]];
147 17
            $types[]      = $joinColumn->getType();
148
        }
149
150 17
        list($joinTargetEntitySQL, $filterSql) = $this->getFilterSql($association);
151
152 17
        if ($filterSql) {
153 3
            $conditions[] = $filterSql;
154
        }
155
156
        // If there is a provided criteria, make part of conditions
157
        // @todo Fix this. Current SQL returns something like:
158
        //
159
        /*if ($criteria && ($expression = $criteria->getWhereExpression()) !== null) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
160
            // A join is needed on the target entity
161
            $targetTableName = $targetClass->table->getQuotedQualifiedName($this->platform);
162
            $targetJoinSql   = ' JOIN ' . $targetTableName . ' te'
163
                . ' ON' . implode(' AND ', $this->getOnConditionSQL($association));
164
165
            // And criteria conditions needs to be added
166
            $persister    = $this->uow->getEntityPersister($targetClass->getClassName());
167
            $visitor      = new SqlExpressionVisitor($persister, $targetClass);
168
            $conditions[] = $visitor->dispatch($expression);
169
170
            $joinTargetEntitySQL = $targetJoinSql . $joinTargetEntitySQL;
171
        }*/
172
173
        $sql = 'SELECT COUNT(*)'
174 17
            . ' FROM ' . $joinTableName . ' t'
175 17
            . $joinTargetEntitySQL
176 17
            . ' WHERE ' . implode(' AND ', $conditions);
177
178 17
        return $this->conn->fetchColumn($sql, $params, 0, $types);
179
    }
180
181
    /**
182
     * {@inheritDoc}
183
     */
184 8
    public function slice(PersistentCollection $collection, $offset, $length = null)
185
    {
186 8
        $association = $collection->getMapping();
187 8
        $persister   = $this->uow->getEntityPersister($association->getTargetEntity());
188
189 8
        return $persister->getManyToManyCollection($association, $collection->getOwner(), $offset, $length);
190
    }
191
    /**
192
     * {@inheritdoc}
193
     */
194 7
    public function containsKey(PersistentCollection $collection, $key)
195
    {
196 7
        $association = $collection->getMapping();
197
198 7
        if (! ($association instanceof ToManyAssociationMetadata && $association->getIndexedBy())) {
199
            throw new \BadMethodCallException('Selecting a collection by index is only supported on indexed collections.');
200
        }
201
202 7
        list($quotedJoinTable, $whereClauses, $params, $types) = $this->getJoinTableRestrictionsWithKey($collection, $key, true);
203
204 7
        $sql = 'SELECT 1 FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses);
205
206 7
        return (bool) $this->conn->fetchColumn($sql, $params, 0, $types);
207
    }
208
209
    /**
210
     * {@inheritDoc}
211
     */
212 7
    public function contains(PersistentCollection $collection, $element)
213
    {
214 7
        if (! $this->isValidEntityState($element)) {
215 2
            return false;
216
        }
217
218 7
        list($quotedJoinTable, $whereClauses, $params, $types) = $this->getJoinTableRestrictions($collection, $element, true);
219
220 7
        $sql = 'SELECT 1 FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses);
221
222 7
        return (bool) $this->conn->fetchColumn($sql, $params, 0, $types);
223
    }
224
225
    /**
226
     * {@inheritDoc}
227
     */
228 2
    public function removeElement(PersistentCollection $collection, $element)
229
    {
230 2
        if (! $this->isValidEntityState($element)) {
231 2
            return false;
232
        }
233
234 2
        list($quotedJoinTable, $whereClauses, $params, $types) = $this->getJoinTableRestrictions($collection, $element, false);
235
236 2
        $sql = 'DELETE FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses);
237
238 2
        return (bool) $this->conn->executeUpdate($sql, $params, $types);
239
    }
240
241
    /**
242
     * {@inheritDoc}
243
     */
244 12
    public function loadCriteria(PersistentCollection $collection, Criteria $criteria)
245
    {
246 12
        $association   = $collection->getMapping();
247 12
        $owner         = $collection->getOwner();
248 12
        $ownerMetadata = $this->em->getClassMetadata(get_class($owner));
249 12
        $identifier    = $this->uow->getEntityIdentifier($owner);
250 12
        $targetClass   = $this->em->getClassMetadata($association->getTargetEntity());
251 12
        $onConditions  = $this->getOnConditionSQL($association);
252 12
        $whereClauses  = $params = $types = [];
253
254 12
        if (! $association->isOwningSide()) {
255 1
            $association = $targetClass->getProperty($association->getMappedBy());
256 1
            $joinColumns = $association->getJoinTable()->getInverseJoinColumns();
257
        } else {
258 11
            $joinColumns = $association->getJoinTable()->getJoinColumns();
259
        }
260
261 12
        foreach ($joinColumns as $joinColumn) {
262
            /** @var JoinColumnMetadata $joinColumn */
263 12
            $quotedColumnName     = $this->platform->quoteIdentifier($joinColumn->getColumnName());
264 12
            $referencedColumnName = $joinColumn->getReferencedColumnName();
265
266 12
            if (! $joinColumn->getType()) {
267
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $ownerMetadata, $this->em));
268
            }
269
270 12
            $whereClauses[] = sprintf('t.%s = ?', $quotedColumnName);
271 12
            $params[]       = $identifier[$ownerMetadata->fieldNames[$referencedColumnName]];
272 12
            $types[]        = $joinColumn->getType();
273
        }
274
275 12
        $parameters = $this->expandCriteriaParameters($criteria);
276
277 12
        foreach ($parameters as $parameter) {
278 7
            [$name, $value, $operator] = $parameter;
279
280 7
            $property   = $targetClass->getProperty($name);
281 7
            $columnName = $this->platform->quoteIdentifier($property->getColumnName());
282
283 7
            $whereClauses[] = sprintf('te.%s %s ?', $columnName, $operator);
284 7
            $params[]       = $value;
285 7
            $types[]        = $property->getType();
286
        }
287
288 12
        $tableName        = $targetClass->table->getQuotedQualifiedName($this->platform);
289 12
        $joinTableName    = $association->getJoinTable()->getQuotedQualifiedName($this->platform);
290 12
        $resultSetMapping = new Query\ResultSetMappingBuilder($this->em);
291
292 12
        $resultSetMapping->addRootEntityFromClassMetadata($targetClass->getClassName(), 'te');
293
294 12
        $sql = 'SELECT ' . $resultSetMapping->generateSelectClause()
295 12
            . ' FROM ' . $tableName . ' te'
296 12
            . ' JOIN ' . $joinTableName . ' t ON'
297 12
            . implode(' AND ', $onConditions)
298 12
            . ' WHERE ' . implode(' AND ', $whereClauses);
299
300 12
        $sql .= $this->getOrderingSql($criteria, $targetClass);
301 12
        $sql .= $this->getLimitSql($criteria);
302
303 12
        $stmt = $this->conn->executeQuery($sql, $params, $types);
304
305 12
        return $this->em->newHydrator(Query::HYDRATE_OBJECT)->hydrateAll($stmt, $resultSetMapping);
306
    }
307
308
    /**
309
     * Generates the filter SQL for a given mapping.
310
     *
311
     * This method is not used for actually grabbing the related entities
312
     * but when the extra-lazy collection methods are called on a filtered
313
     * association. This is why besides the many to many table we also
314
     * have to join in the actual entities table leading to additional
315
     * JOIN.
316
     *
317
     * @return string[] ordered tuple:
318
     *                   - JOIN condition to add to the SQL
319
     *                   - WHERE condition to add to the SQL
320
     */
321 31
    public function getFilterSql(ManyToManyAssociationMetadata $association)
322
    {
323 31
        $targetClass = $this->em->getClassMetadata($association->getTargetEntity());
324 31
        $rootClass   = $this->em->getClassMetadata($targetClass->getRootClassName());
325 31
        $filterSql   = $this->generateFilterConditionSQL($rootClass, 'te');
326
327 31
        if ($filterSql === '') {
328 31
            return ['', ''];
329
        }
330
331
        // A join is needed if there is filtering on the target entity
332 6
        $tableName = $rootClass->table->getQuotedQualifiedName($this->platform);
333 6
        $joinSql   = ' JOIN ' . $tableName . ' te'
334 6
            . ' ON' . implode(' AND ', $this->getOnConditionSQL($association));
335
336 6
        return [$joinSql, $filterSql];
337
    }
338
339
    /**
340
     * Generates the filter SQL for a given entity and table alias.
341
     *
342
     * @param ClassMetadata $targetEntity     Metadata of the target entity.
343
     * @param string        $targetTableAlias The table alias of the joined/selected table.
344
     *
345
     * @return string The SQL query part to add to a query.
346
     */
347 31
    protected function generateFilterConditionSQL(ClassMetadata $targetEntity, $targetTableAlias)
348
    {
349 31
        $filterClauses = [];
350
351 31
        foreach ($this->em->getFilters()->getEnabledFilters() as $filter) {
352 6
            $filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias);
353
354 6
            if ($filterExpr) {
355 6
                $filterClauses[] = '(' . $filterExpr . ')';
356
            }
357
        }
358
359 31
        if (! $filterClauses) {
360 31
            return '';
361
        }
362
363 6
        $filterSql = implode(' AND ', $filterClauses);
364
365 6
        return isset($filterClauses[1])
366
            ? '(' . $filterSql . ')'
367 6
            : $filterSql
368
        ;
369
    }
370
371
    /**
372
     * Generate ON condition
373
     *
374
     * @return string[]
375
     */
376 18
    protected function getOnConditionSQL(ManyToManyAssociationMetadata $association)
377
    {
378 18
        $targetClass       = $this->em->getClassMetadata($association->getTargetEntity());
379 18
        $owningAssociation = ! $association->isOwningSide()
380 3
            ? $targetClass->getProperty($association->getMappedBy())
381 18
            : $association;
382
383 18
        $joinTable   = $owningAssociation->getJoinTable();
384 18
        $joinColumns = $association->isOwningSide()
385 15
            ? $joinTable->getInverseJoinColumns()
386 18
            : $joinTable->getJoinColumns()
387
        ;
388
389 18
        $conditions = [];
390
391 18
        foreach ($joinColumns as $joinColumn) {
392 18
            $quotedColumnName           = $this->platform->quoteIdentifier($joinColumn->getColumnName());
393 18
            $quotedReferencedColumnName = $this->platform->quoteIdentifier($joinColumn->getReferencedColumnName());
394
395 18
            $conditions[] = ' t.' . $quotedColumnName . ' = te.' . $quotedReferencedColumnName;
396
        }
397
398 18
        return $conditions;
399
    }
400
401
    /**
402
     * {@inheritdoc}
403
     *
404
     * @override
405
     */
406 17
    protected function getDeleteSQL(PersistentCollection $collection)
407
    {
408 17
        $association   = $collection->getMapping();
409 17
        $joinTable     = $association->getJoinTable();
410 17
        $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
411 17
        $columns       = [];
412
413 17
        foreach ($joinTable->getJoinColumns() as $joinColumn) {
414 17
            $columns[] = $this->platform->quoteIdentifier($joinColumn->getColumnName());
415
        }
416
417 17
        return 'DELETE FROM ' . $joinTableName . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?';
418
    }
419
420
    /**
421
     * {@inheritdoc}
422
     *
423
     * Internal note: Order of the parameters must be the same as the order of the columns in getDeleteSql.
424
     * @override
425
     */
426 17
    protected function getDeleteSQLParameters(PersistentCollection $collection)
427
    {
428 17
        $association = $collection->getMapping();
429 17
        $identifier  = $this->uow->getEntityIdentifier($collection->getOwner());
430 17
        $joinTable   = $association->getJoinTable();
431 17
        $joinColumns = $joinTable->getJoinColumns();
432
433
        // Optimization for single column identifier
434 17
        if (count($joinColumns) === 1) {
435 15
            return [reset($identifier)];
436
        }
437
438
        // Composite identifier
439 2
        $sourceClass = $this->em->getClassMetadata($association->getSourceEntity());
440 2
        $params      = [];
441
442 2
        foreach ($joinColumns as $joinColumn) {
443 2
            $params[] = $identifier[$sourceClass->fieldNames[$joinColumn->getReferencedColumnName()]];
444
        }
445
446 2
        return $params;
447
    }
448
449
    /**
450
     * Gets the SQL statement used for deleting a row from the collection.
451
     *
452
     * @return string[]|string[][] ordered tuple containing the SQL to be executed and an array
453
     *                             of types for bound parameters
454
     */
455 325
    protected function getDeleteRowSQL(PersistentCollection $collection)
456
    {
457 325
        $association = $collection->getMapping();
458 325
        $class       = $this->em->getClassMetadata($association->getSourceEntity());
459 325
        $targetClass = $this->em->getClassMetadata($association->getTargetEntity());
460 325
        $columns     = [];
461 325
        $types       = [];
462
463 325
        $joinTable     = $association->getJoinTable();
464 325
        $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
465
466 325
        foreach ($joinTable->getJoinColumns() as $joinColumn) {
467
            /** @var JoinColumnMetadata $joinColumn */
468 325
            $quotedColumnName     = $this->platform->quoteIdentifier($joinColumn->getColumnName());
469 325
            $referencedColumnName = $joinColumn->getReferencedColumnName();
470
471 325
            if (! $joinColumn->getType()) {
472 34
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $class, $this->em));
473
            }
474
475 325
            $columns[] = $quotedColumnName;
476 325
            $types[]   = $joinColumn->getType();
477
        }
478
479 325
        foreach ($joinTable->getInverseJoinColumns() as $joinColumn) {
480
            /** @var JoinColumnMetadata $joinColumn */
481 325
            $quotedColumnName     = $this->platform->quoteIdentifier($joinColumn->getColumnName());
482 325
            $referencedColumnName = $joinColumn->getReferencedColumnName();
483
484 325
            if (! $joinColumn->getType()) {
485 34
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
486
            }
487
488 325
            $columns[] = $quotedColumnName;
489 325
            $types[]   = $joinColumn->getType();
490
        }
491
492
        return [
493 325
            sprintf('DELETE FROM %s WHERE %s = ?', $joinTableName, implode(' = ? AND ', $columns)),
494 325
            $types,
495
        ];
496
    }
497
498
    /**
499
     * Gets the SQL parameters for the corresponding SQL statement to delete the given
500
     * element from the given collection.
501
     *
502
     * Internal note: Order of the parameters must be the same as the order of the columns in getDeleteRowSql.
503
     *
504
     * @param mixed $element
505
     *
506
     * @return mixed[]
507
     */
508 10
    protected function getDeleteRowSQLParameters(PersistentCollection $collection, $element)
509
    {
510 10
        return $this->collectJoinTableColumnParameters($collection, $element);
511
    }
512
513
    /**
514
     * Gets the SQL statement used for inserting a row in the collection.
515
     *
516
     * @return string[]|string[][] ordered tuple containing the SQL to be executed and an array
517
     *                             of types for bound parameters
518
     */
519 325
    protected function getInsertRowSQL(PersistentCollection $collection)
520
    {
521 325
        $association = $collection->getMapping();
522 325
        $class       = $this->em->getClassMetadata($association->getSourceEntity());
523 325
        $targetClass = $this->em->getClassMetadata($association->getTargetEntity());
524 325
        $columns     = [];
525 325
        $types       = [];
526
527 325
        $joinTable     = $association->getJoinTable();
528 325
        $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
529
530 325
        foreach ($joinTable->getJoinColumns() as $joinColumn) {
531
            /** @var JoinColumnMetadata $joinColumn */
532 325
            $quotedColumnName     = $this->platform->quoteIdentifier($joinColumn->getColumnName());
533 325
            $referencedColumnName = $joinColumn->getReferencedColumnName();
534
535 325
            if (! $joinColumn->getType()) {
536
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $class, $this->em));
537
            }
538
539 325
            $columns[] = $quotedColumnName;
540 325
            $types[]   = $joinColumn->getType();
541
        }
542
543 325
        foreach ($joinTable->getInverseJoinColumns() as $joinColumn) {
544
            /** @var JoinColumnMetadata $joinColumn */
545 325
            $quotedColumnName     = $this->platform->quoteIdentifier($joinColumn->getColumnName());
546 325
            $referencedColumnName = $joinColumn->getReferencedColumnName();
547
548 325
            if (! $joinColumn->getType()) {
549
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
550
            }
551
552 325
            $columns[] = $quotedColumnName;
553 325
            $types[]   = $joinColumn->getType();
554
        }
555
556 325
        $columnNamesAsString  = implode(', ', $columns);
557 325
        $columnValuesAsString = implode(', ', array_fill(0, count($columns), '?'));
558
559
        return [
560 325
            sprintf('INSERT INTO %s (%s) VALUES (%s)', $joinTableName, $columnNamesAsString, $columnValuesAsString),
561 325
            $types,
562
        ];
563
    }
564
565
    /**
566
     * Gets the SQL parameters for the corresponding SQL statement to insert the given
567
     * element of the given collection into the database.
568
     *
569
     * Internal note: Order of the parameters must be the same as the order of the columns in getInsertRowSql.
570
     *
571
     * @param mixed $element
572
     *
573
     * @return mixed[]
574
     */
575 325
    protected function getInsertRowSQLParameters(PersistentCollection $collection, $element)
576
    {
577 325
        return $this->collectJoinTableColumnParameters($collection, $element);
578
    }
579
580
    /**
581
     * Collects the parameters for inserting/deleting on the join table in the order
582
     * of the join table columns.
583
     *
584
     * @param object $element
585
     *
586
     * @return mixed[]
587
     */
588 325
    private function collectJoinTableColumnParameters(PersistentCollection $collection, $element)
589
    {
590 325
        $params           = [];
591 325
        $association      = $collection->getMapping();
592 325
        $owningClass      = $this->em->getClassMetadata(get_class($collection->getOwner()));
593 325
        $targetClass      = $collection->getTypeClass();
594 325
        $owningIdentifier = $this->uow->getEntityIdentifier($collection->getOwner());
595 325
        $targetIdentifier = $this->uow->getEntityIdentifier($element);
596 325
        $joinTable        = $association->getJoinTable();
597
598 325
        foreach ($joinTable->getJoinColumns() as $joinColumn) {
599 325
            $fieldName = $owningClass->fieldNames[$joinColumn->getReferencedColumnName()];
600
601 325
            $params[] = $owningIdentifier[$fieldName];
602
        }
603
604 325
        foreach ($joinTable->getInverseJoinColumns() as $joinColumn) {
605 325
            $fieldName = $targetClass->fieldNames[$joinColumn->getReferencedColumnName()];
606
607 325
            $params[] = $targetIdentifier[$fieldName];
608
        }
609
610 325
        return $params;
611
    }
612
613
    /**
614
     * @param string $key
615
     * @param bool   $addFilters Whether the filter SQL should be included or not.
616
     *
617
     * @return mixed[] ordered vector:
618
     *                - quoted join table name
619
     *                - where clauses to be added for filtering
620
     *                - parameters to be bound for filtering
621
     *                - types of the parameters to be bound for filtering
622
     */
623 7
    private function getJoinTableRestrictionsWithKey(PersistentCollection $collection, $key, $addFilters)
624
    {
625 7
        $association       = $collection->getMapping();
626 7
        $owningAssociation = $association;
627 7
        $indexBy           = $owningAssociation->getIndexedBy();
628 7
        $identifier        = $this->uow->getEntityIdentifier($collection->getOwner());
629 7
        $sourceClass       = $this->em->getClassMetadata($owningAssociation->getSourceEntity());
630 7
        $targetClass       = $this->em->getClassMetadata($owningAssociation->getTargetEntity());
631
632 7
        if (! $owningAssociation->isOwningSide()) {
633 3
            $owningAssociation  = $targetClass->getProperty($owningAssociation->getMappedBy());
634 3
            $joinTable          = $owningAssociation->getJoinTable();
635 3
            $joinColumns        = $joinTable->getJoinColumns();
636 3
            $inverseJoinColumns = $joinTable->getInverseJoinColumns();
637
        } else {
638 4
            $joinTable          = $owningAssociation->getJoinTable();
639 4
            $joinColumns        = $joinTable->getInverseJoinColumns();
640 4
            $inverseJoinColumns = $joinTable->getJoinColumns();
641
        }
642
643 7
        $joinTableName   = $joinTable->getQuotedQualifiedName($this->platform);
644 7
        $quotedJoinTable = $joinTableName . ' t';
645 7
        $whereClauses    = [];
646 7
        $params          = [];
647 7
        $types           = [];
648 7
        $joinNeeded      = ! in_array($indexBy, $targetClass->identifier, true);
649
650 7
        if ($joinNeeded) { // extra join needed if indexBy is not a @id
651 3
            $joinConditions = [];
652
653 3
            foreach ($joinColumns as $joinColumn) {
654
                /** @var JoinColumnMetadata $joinColumn */
655 3
                $quotedColumnName           = $this->platform->quoteIdentifier($joinColumn->getColumnName());
656 3
                $quotedReferencedColumnName = $this->platform->quoteIdentifier($joinColumn->getReferencedColumnName());
657
658 3
                $joinConditions[] = ' t.' . $quotedColumnName . ' = tr.' . $quotedReferencedColumnName;
659
            }
660
661 3
            $tableName        = $targetClass->table->getQuotedQualifiedName($this->platform);
662 3
            $quotedJoinTable .= ' JOIN ' . $tableName . ' tr ON ' . implode(' AND ', $joinConditions);
663 3
            $indexByProperty  = $targetClass->getProperty($indexBy);
664
665
            switch (true) {
666 3
                case ($indexByProperty instanceof FieldMetadata):
667 3
                    $quotedColumnName = $this->platform->quoteIdentifier($indexByProperty->getColumnName());
668
669 3
                    $whereClauses[] = sprintf('tr.%s = ?', $quotedColumnName);
670 3
                    $params[]       = $key;
671 3
                    $types[]        = $indexByProperty->getType();
672 3
                    break;
673
674
                case ($indexByProperty instanceof ToOneAssociationMetadata && $indexByProperty->isOwningSide()):
675
                    // Cannot be supported because PHP does not accept objects as keys. =(
676
                    break;
677
            }
678
        }
679
680 7
        foreach ($inverseJoinColumns as $joinColumn) {
681
            /** @var JoinColumnMetadata $joinColumn */
682 7
            $quotedColumnName     = $this->platform->quoteIdentifier($joinColumn->getColumnName());
683 7
            $referencedColumnName = $joinColumn->getReferencedColumnName();
684
685 7
            if (! $joinColumn->getType()) {
686
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $sourceClass, $this->em));
687
            }
688
689 7
            $whereClauses[] = sprintf('t.%s = ?', $quotedColumnName);
690 7
            $params[]       = $identifier[$sourceClass->fieldNames[$joinColumn->getReferencedColumnName()]];
691 7
            $types[]        = $joinColumn->getType();
692
        }
693
694 7
        if (! $joinNeeded) {
695 4
            foreach ($joinColumns as $joinColumn) {
696
                /** @var JoinColumnMetadata $joinColumn */
697 4
                $quotedColumnName     = $this->platform->quoteIdentifier($joinColumn->getColumnName());
698 4
                $referencedColumnName = $joinColumn->getReferencedColumnName();
699
700 4
                if (! $joinColumn->getType()) {
701
                    $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
702
                }
703
704 4
                $whereClauses[] = sprintf('t.%s = ?', $quotedColumnName);
705 4
                $params[]       = $key;
706 4
                $types[]        = $joinColumn->getType();
707
            }
708
        }
709
710 7
        if ($addFilters) {
711 7
            list($joinTargetEntitySQL, $filterSql) = $this->getFilterSql($association);
712
713 7
            if ($filterSql) {
714
                $quotedJoinTable .= ' ' . $joinTargetEntitySQL;
715
                $whereClauses[]   = $filterSql;
716
            }
717
        }
718
719 7
        return [$quotedJoinTable, $whereClauses, $params, $types];
720
    }
721
722
    /**
723
     * @param object $element
724
     * @param bool   $addFilters Whether the filter SQL should be included or not.
725
     *
726
     * @return mixed[] ordered vector:
727
     *                - quoted join table name
728
     *                - where clauses to be added for filtering
729
     *                - parameters to be bound for filtering
730
     *                - types of the parameters to be bound for filtering
731
     */
732 9
    private function getJoinTableRestrictions(PersistentCollection $collection, $element, $addFilters)
733
    {
734 9
        $association       = $collection->getMapping();
735 9
        $owningAssociation = $association;
736
737 9
        if (! $association->isOwningSide()) {
738 4
            $sourceClass      = $this->em->getClassMetadata($association->getTargetEntity());
739 4
            $targetClass      = $this->em->getClassMetadata($association->getSourceEntity());
740 4
            $sourceIdentifier = $this->uow->getEntityIdentifier($element);
741 4
            $targetIdentifier = $this->uow->getEntityIdentifier($collection->getOwner());
742
743 4
            $owningAssociation = $sourceClass->getProperty($association->getMappedBy());
744
        } else {
745 5
            $sourceClass      = $this->em->getClassMetadata($association->getSourceEntity());
746 5
            $targetClass      = $this->em->getClassMetadata($association->getTargetEntity());
747 5
            $sourceIdentifier = $this->uow->getEntityIdentifier($collection->getOwner());
748 5
            $targetIdentifier = $this->uow->getEntityIdentifier($element);
749
        }
750
751 9
        $joinTable       = $owningAssociation->getJoinTable();
752 9
        $joinTableName   = $joinTable->getQuotedQualifiedName($this->platform);
753 9
        $quotedJoinTable = $joinTableName;
754 9
        $whereClauses    = [];
755 9
        $params          = [];
756 9
        $types           = [];
757
758 9
        foreach ($joinTable->getJoinColumns() as $joinColumn) {
759
            /** @var JoinColumnMetadata $joinColumn */
760 9
            $quotedColumnName     = $this->platform->quoteIdentifier($joinColumn->getColumnName());
761 9
            $referencedColumnName = $joinColumn->getReferencedColumnName();
762
763 9
            if (! $joinColumn->getType()) {
764
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $sourceClass, $this->em));
765
            }
766
767 9
            $whereClauses[] = ($addFilters ? 't.' : '') . $quotedColumnName . ' = ?';
768 9
            $params[]       = $sourceIdentifier[$sourceClass->fieldNames[$referencedColumnName]];
769 9
            $types[]        = $joinColumn->getType();
770
        }
771
772 9
        foreach ($joinTable->getInverseJoinColumns() as $joinColumn) {
773
            /** @var JoinColumnMetadata $joinColumn */
774 9
            $quotedColumnName     = $this->platform->quoteIdentifier($joinColumn->getColumnName());
775 9
            $referencedColumnName = $joinColumn->getReferencedColumnName();
776
777 9
            if (! $joinColumn->getType()) {
778
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
779
            }
780
781 9
            $whereClauses[] = ($addFilters ? 't.' : '') . $quotedColumnName . ' = ?';
782 9
            $params[]       = $targetIdentifier[$targetClass->fieldNames[$referencedColumnName]];
783 9
            $types[]        = $joinColumn->getType();
784
        }
785
786 9
        if ($addFilters) {
787 7
            $quotedJoinTable .= ' t';
788
789 7
            list($joinTargetEntitySQL, $filterSql) = $this->getFilterSql($association);
790
791 7
            if ($filterSql) {
792 3
                $quotedJoinTable .= ' ' . $joinTargetEntitySQL;
793 3
                $whereClauses[]   = $filterSql;
794
            }
795
        }
796
797 9
        return [$quotedJoinTable, $whereClauses, $params, $types];
798
    }
799
800
    /**
801
     * Expands Criteria Parameters by walking the expressions and grabbing all
802
     * parameters and types from it.
803
     *
804
     * @return mixed[]
805
     */
806 12
    private function expandCriteriaParameters(Criteria $criteria)
807
    {
808 12
        $expression = $criteria->getWhereExpression();
809
810 12
        if ($expression === null) {
811 5
            return [];
812
        }
813
814 7
        $valueVisitor = new SqlValueVisitor();
815
816 7
        $valueVisitor->dispatch($expression);
817
818 7
        list(, $types) = $valueVisitor->getParamsAndTypes();
819
820 7
        return $types;
821
    }
822
823
    /**
824
     * @return string
825
     */
826 12
    private function getOrderingSql(Criteria $criteria, ClassMetadata $targetClass)
827
    {
828 12
        $orderings = $criteria->getOrderings();
829
830 12
        if ($orderings) {
831 3
            $orderBy = [];
832
833 3
            foreach ($orderings as $name => $direction) {
834 3
                $property   = $targetClass->getProperty($name);
835 3
                $columnName = $this->platform->quoteIdentifier($property->getColumnName());
836
837 3
                $orderBy[] = $columnName . ' ' . $direction;
838
            }
839
840 3
            return ' ORDER BY ' . implode(', ', $orderBy);
841
        }
842 9
        return '';
843
    }
844
845
    /**
846
     * @return string
847
     * @throws \Doctrine\DBAL\DBALException
848
     */
849 12
    private function getLimitSql(Criteria $criteria)
850
    {
851 12
        $limit  = $criteria->getMaxResults();
852 12
        $offset = $criteria->getFirstResult();
853 12
        if ($limit !== null || $offset !== null) {
854 3
            return $this->platform->modifyLimitQuery('', $limit, $offset);
855
        }
856 9
        return '';
857
    }
858
}
859