Failed Conditions
Pull Request — 2.6 (#7882)
by
unknown
06:45
created

ManyToManyPersister::get()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
eloc 8
c 0
b 0
f 0
dl 0
loc 14
ccs 0
cts 9
cp 0
rs 10
cc 3
nc 3
nop 2
crap 12
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ORM\Persisters\Collection;
21
22
use Doctrine\Common\Collections\Criteria;
23
use Doctrine\ORM\Mapping\ClassMetadata;
24
use Doctrine\ORM\Persisters\SqlValueVisitor;
25
use Doctrine\ORM\PersistentCollection;
26
use Doctrine\ORM\Query;
27
use Doctrine\ORM\Utility\PersisterHelper;
28
29
/**
30
 * Persister for many-to-many collections.
31
 *
32
 * @author  Roman Borschel <[email protected]>
33
 * @author  Guilherme Blanco <[email protected]>
34
 * @author  Alexander <[email protected]>
35
 * @since   2.0
36
 */
37
class ManyToManyPersister extends AbstractCollectionPersister
38
{
39
    /**
40
     * {@inheritdoc}
41
     */
42 14
    public function delete(PersistentCollection $collection)
43
    {
44 14
        $mapping = $collection->getMapping();
45
46 14
        if ( ! $mapping['isOwningSide']) {
47
            return; // ignore inverse side
48
        }
49
50 14
        $types = [];
51 14
        $class = $this->em->getClassMetadata($mapping['sourceEntity']);
52
53 14
        foreach ($mapping['joinTable']['joinColumns'] as $joinColumn) {
54 14
            $types[] = PersisterHelper::getTypeOfColumn($joinColumn['referencedColumnName'], $class, $this->em);
55
        }
56
57 14
        $this->conn->executeUpdate($this->getDeleteSQL($collection), $this->getDeleteSQLParameters($collection), $types);
58 14
    }
59
60
    /**
61
     * {@inheritdoc}
62
     */
63 99
    public function update(PersistentCollection $collection)
64
    {
65 99
        $mapping = $collection->getMapping();
66
67 99
        if ( ! $mapping['isOwningSide']) {
68 63
            return; // ignore inverse side
69
        }
70
71 98
        list($deleteSql, $deleteTypes) = $this->getDeleteRowSQL($collection);
72 98
        list($insertSql, $insertTypes) = $this->getInsertRowSQL($collection);
73
74 98
        foreach ($collection->getDeleteDiff() as $element) {
75 10
            $this->conn->executeUpdate(
76 10
                $deleteSql,
77 10
                $this->getDeleteRowSQLParameters($collection, $element),
78 10
                $deleteTypes
79
            );
80
        }
81
82 98
        foreach ($collection->getInsertDiff() as $element) {
83 98
            $this->conn->executeUpdate(
84 98
                $insertSql,
85 98
                $this->getInsertRowSQLParameters($collection, $element),
86 98
                $insertTypes
87
            );
88
        }
89 98
    }
90
91
    /**
92
     * {@inheritdoc}
93
     */
94
    public function get(PersistentCollection $collection, $index)
95
    {
96
        $mapping = $collection->getMapping();
97
98
        if ( ! isset($mapping['indexBy'])) {
99
            throw new \BadMethodCallException("Selecting a collection by index is only supported on indexed collections.");
100
        }
101
102
        $persister = $this->uow->getEntityPersister($mapping['targetEntity']);
103
        $mappedKey = $mapping['isOwningSide']
104
            ? $mapping['inversedBy']
105
            : $mapping['mappedBy'];
106
107
        return $persister->load([$mappedKey => $collection->getOwner(), $mapping['indexBy'] => $index], null, $mapping, [], 0, 1);
108
    }
109
110
    /**
111
     * {@inheritdoc}
112
     */
113 9
    public function count(PersistentCollection $collection)
114
    {
115 9
        $conditions     = [];
116 9
        $params         = [];
117 9
        $types          = [];
118 9
        $mapping        = $collection->getMapping();
119 9
        $id             = $this->uow->getEntityIdentifier($collection->getOwner());
120 9
        $sourceClass    = $this->em->getClassMetadata($mapping['sourceEntity']);
121 9
        $targetClass    = $this->em->getClassMetadata($mapping['targetEntity']);
122 9
        $association    = ( ! $mapping['isOwningSide'])
123 2
            ? $targetClass->associationMappings[$mapping['mappedBy']]
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
124 9
            : $mapping;
125
126 9
        $joinTableName  = $this->quoteStrategy->getJoinTableName($association, $sourceClass, $this->platform);
127 9
        $joinColumns    = ( ! $mapping['isOwningSide'])
128 2
            ? $association['joinTable']['inverseJoinColumns']
129 9
            : $association['joinTable']['joinColumns'];
130
131 9
        foreach ($joinColumns as $joinColumn) {
132 9
            $columnName     = $this->quoteStrategy->getJoinColumnName($joinColumn, $sourceClass, $this->platform);
133 9
            $referencedName = $joinColumn['referencedColumnName'];
134 9
            $conditions[]   = 't.' . $columnName . ' = ?';
135 9
            $params[]       = $id[$sourceClass->getFieldForColumn($referencedName)];
136 9
            $types[]        = PersisterHelper::getTypeOfColumn($referencedName, $sourceClass, $this->em);
137
        }
138
139 9
        list($joinTargetEntitySQL, $filterSql) = $this->getFilterSql($mapping);
140
141 9
        if ($filterSql) {
142
            $conditions[] = $filterSql;
143
        }
144
145
        // If there is a provided criteria, make part of conditions
146
        // @todo Fix this. Current SQL returns something like:
147
        //
148
        /*if ($criteria && ($expression = $criteria->getWhereExpression()) !== null) {
149
            // A join is needed on the target entity
150
            $targetTableName = $this->quoteStrategy->getTableName($targetClass, $this->platform);
151
            $targetJoinSql   = ' JOIN ' . $targetTableName . ' te'
152
                . ' ON' . implode(' AND ', $this->getOnConditionSQL($association));
153
154
            // And criteria conditions needs to be added
155
            $persister    = $this->uow->getEntityPersister($targetClass->name);
156
            $visitor      = new SqlExpressionVisitor($persister, $targetClass);
157
            $conditions[] = $visitor->dispatch($expression);
158
159
            $joinTargetEntitySQL = $targetJoinSql . $joinTargetEntitySQL;
160
        }*/
161
162
        $sql = 'SELECT COUNT(*)'
163 9
            . ' FROM ' . $joinTableName . ' t'
164 9
            . $joinTargetEntitySQL
165 9
            . ' WHERE ' . implode(' AND ', $conditions);
166
167 9
        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...
168
    }
169
170
    /**
171
     * {@inheritDoc}
172
     */
173 2
    public function slice(PersistentCollection $collection, $offset, $length = null)
174
    {
175 2
        $mapping   = $collection->getMapping();
176 2
        $persister = $this->uow->getEntityPersister($mapping['targetEntity']);
177
178 2
        return $persister->getManyToManyCollection($mapping, $collection->getOwner(), $offset, $length);
179
    }
180
    /**
181
     * {@inheritdoc}
182
     */
183 2
    public function containsKey(PersistentCollection $collection, $key)
184
    {
185 2
        $mapping = $collection->getMapping();
186
187 2
        if ( ! isset($mapping['indexBy'])) {
188
            throw new \BadMethodCallException("Selecting a collection by index is only supported on indexed collections.");
189
        }
190
191 2
        list($quotedJoinTable, $whereClauses, $params, $types) = $this->getJoinTableRestrictionsWithKey($collection, $key, true);
192
193 2
        $sql = 'SELECT 1 FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses);
194
195 2
        return (bool) $this->conn->fetchColumn($sql, $params, 0, $types);
196
    }
197
198
    /**
199
     * {@inheritDoc}
200
     */
201 2
    public function contains(PersistentCollection $collection, $element)
202
    {
203 2
        if ( ! $this->isValidEntityState($element)) {
204
            return false;
205
        }
206
207 2
        list($quotedJoinTable, $whereClauses, $params, $types) = $this->getJoinTableRestrictions($collection, $element, true);
208
209 2
        $sql = 'SELECT 1 FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses);
210
211 2
        return (bool) $this->conn->fetchColumn($sql, $params, 0, $types);
212
    }
213
214
    /**
215
     * {@inheritDoc}
216
     */
217
    public function removeElement(PersistentCollection $collection, $element)
218
    {
219
        if ( ! $this->isValidEntityState($element)) {
220
            return false;
221
        }
222
223
        list($quotedJoinTable, $whereClauses, $params, $types) = $this->getJoinTableRestrictions($collection, $element, false);
224
225
        $sql = 'DELETE FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses);
226
227
        return (bool) $this->conn->executeUpdate($sql, $params, $types);
228
    }
229
230
    /**
231
     * {@inheritDoc}
232
     */
233 12
    public function loadCriteria(PersistentCollection $collection, Criteria $criteria)
234
    {
235 12
        $mapping       = $collection->getMapping();
236 12
        $owner         = $collection->getOwner();
237 12
        $ownerMetadata = $this->em->getClassMetadata(get_class($owner));
238 12
        $id            = $this->uow->getEntityIdentifier($owner);
239 12
        $targetClass   = $this->em->getClassMetadata($mapping['targetEntity']);
240 12
        $onConditions  = $this->getOnConditionSQL($mapping);
241 12
        $whereClauses  = $params = [];
242
243 12
        if ( ! $mapping['isOwningSide']) {
244 1
            $associationSourceClass = $targetClass;
245 1
            $mapping = $targetClass->associationMappings[$mapping['mappedBy']];
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
246 1
            $sourceRelationMode = 'relationToTargetKeyColumns';
247
        } else {
248 11
            $associationSourceClass = $ownerMetadata;
249 11
            $sourceRelationMode = 'relationToSourceKeyColumns';
250
        }
251
252 12
        foreach ($mapping[$sourceRelationMode] as $key => $value) {
253 12
            $whereClauses[] = sprintf('t.%s = ?', $key);
254 12
            $params[] = $ownerMetadata->containsForeignIdentifier
0 ignored issues
show
Bug introduced by
Accessing containsForeignIdentifier on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
255
                ? $id[$ownerMetadata->getFieldForColumn($value)]
256 12
                : $id[$ownerMetadata->fieldNames[$value]];
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...
257
        }
258
259 12
        $parameters = $this->expandCriteriaParameters($criteria);
260
261 12
        foreach ($parameters as $parameter) {
262 7
            [$name, $value, $operator] = $parameter;
263
264 7
            $field          = $this->quoteStrategy->getColumnName($name, $targetClass, $this->platform);
265 7
            $whereClauses[] = sprintf('te.%s %s ?', $field, $operator);
266 7
            $params[]       = $value;
267
        }
268
269 12
        $tableName    = $this->quoteStrategy->getTableName($targetClass, $this->platform);
270 12
        $joinTable    = $this->quoteStrategy->getJoinTableName($mapping, $associationSourceClass, $this->platform);
271
272 12
        $rsm = new Query\ResultSetMappingBuilder($this->em);
273 12
        $rsm->addRootEntityFromClassMetadata($targetClass->name, 'te');
0 ignored issues
show
Bug introduced by
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
274
275 12
        $sql = 'SELECT ' . $rsm->generateSelectClause()
276 12
            . ' FROM ' . $tableName . ' te'
277 12
            . ' JOIN ' . $joinTable  . ' t ON'
278 12
            . implode(' AND ', $onConditions)
279 12
            . ' WHERE ' . implode(' AND ', $whereClauses);
280
281 12
        $sql .= $this->getOrderingSql($criteria, $targetClass);
282
283 12
        $sql .= $this->getLimitSql($criteria);
284
285 12
        $stmt = $this->conn->executeQuery($sql, $params);
286
287
        return $this
288 12
            ->em
289 12
            ->newHydrator(Query::HYDRATE_OBJECT)
290 12
            ->hydrateAll($stmt, $rsm);
291
    }
292
293
    /**
294
     * Generates the filter SQL for a given mapping.
295
     *
296
     * This method is not used for actually grabbing the related entities
297
     * but when the extra-lazy collection methods are called on a filtered
298
     * association. This is why besides the many to many table we also
299
     * have to join in the actual entities table leading to additional
300
     * JOIN.
301
     *
302
     * @param array $mapping Array containing mapping information.
303
     *
304
     * @return string[] ordered tuple:
305
     *                   - JOIN condition to add to the SQL
306
     *                   - WHERE condition to add to the SQL
307
     */
308 13
    public function getFilterSql($mapping)
309
    {
310 13
        $targetClass = $this->em->getClassMetadata($mapping['targetEntity']);
311 13
        $rootClass   = $this->em->getClassMetadata($targetClass->rootEntityName);
0 ignored issues
show
Bug introduced by
Accessing rootEntityName on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
312 13
        $filterSql   = $this->generateFilterConditionSQL($rootClass, 'te');
313
314 13
        if ('' === $filterSql) {
315 13
            return ['', ''];
316
        }
317
318
        // A join is needed if there is filtering on the target entity
319
        $tableName = $this->quoteStrategy->getTableName($rootClass, $this->platform);
320
        $joinSql   = ' JOIN ' . $tableName . ' te'
321
            . ' ON' . implode(' AND ', $this->getOnConditionSQL($mapping));
322
323
        return [$joinSql, $filterSql];
324
    }
325
326
    /**
327
     * Generates the filter SQL for a given entity and table alias.
328
     *
329
     * @param ClassMetadata $targetEntity     Metadata of the target entity.
330
     * @param string        $targetTableAlias The table alias of the joined/selected table.
331
     *
332
     * @return string The SQL query part to add to a query.
333
     */
334 13
    protected function generateFilterConditionSQL(ClassMetadata $targetEntity, $targetTableAlias)
335
    {
336 13
        $filterClauses = [];
337
338 13
        foreach ($this->em->getFilters()->getEnabledFilters() as $filter) {
339
            if ($filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias)) {
340
                $filterClauses[] = '(' . $filterExpr . ')';
341
            }
342
        }
343
344 13
        return $filterClauses
345
            ? '(' . implode(' AND ', $filterClauses) . ')'
346 13
            : '';
347
    }
348
349
    /**
350
     * Generate ON condition
351
     *
352
     * @param  array $mapping
353
     *
354
     * @return array
355
     */
356 12
    protected function getOnConditionSQL($mapping)
357
    {
358 12
        $targetClass = $this->em->getClassMetadata($mapping['targetEntity']);
359 12
        $association = ( ! $mapping['isOwningSide'])
360 1
            ? $targetClass->associationMappings[$mapping['mappedBy']]
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
361 12
            : $mapping;
362
363 12
        $joinColumns = $mapping['isOwningSide']
364 11
            ? $association['joinTable']['inverseJoinColumns']
365 12
            : $association['joinTable']['joinColumns'];
366
367 12
        $conditions = [];
368
369 12
        foreach ($joinColumns as $joinColumn) {
370 12
            $joinColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform);
371 12
            $refColumnName  = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform);
372
373 12
            $conditions[] = ' t.' . $joinColumnName . ' = ' . 'te.' . $refColumnName;
374
        }
375
376 12
        return $conditions;
377
    }
378
379
    /**
380
     * {@inheritdoc}
381
     *
382
     * @override
383
     */
384 14
    protected function getDeleteSQL(PersistentCollection $collection)
385
    {
386 14
        $columns    = [];
387 14
        $mapping    = $collection->getMapping();
388 14
        $class      = $this->em->getClassMetadata(get_class($collection->getOwner()));
389 14
        $joinTable  = $this->quoteStrategy->getJoinTableName($mapping, $class, $this->platform);
390
391 14
        foreach ($mapping['joinTable']['joinColumns'] as $joinColumn) {
392 14
            $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
393
        }
394
395 14
        return 'DELETE FROM ' . $joinTable
396 14
            . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?';
397
    }
398
399
    /**
400
     * {@inheritdoc}
401
     *
402
     * Internal note: Order of the parameters must be the same as the order of the columns in getDeleteSql.
403
     * @override
404
     */
405 14
    protected function getDeleteSQLParameters(PersistentCollection $collection)
406
    {
407 14
        $mapping    = $collection->getMapping();
408 14
        $identifier = $this->uow->getEntityIdentifier($collection->getOwner());
409
410
        // Optimization for single column identifier
411 14
        if (count($mapping['relationToSourceKeyColumns']) === 1) {
412 14
            return [reset($identifier)];
413
        }
414
415
        // Composite identifier
416
        $sourceClass = $this->em->getClassMetadata($mapping['sourceEntity']);
417
        $params      = [];
418
419
        foreach ($mapping['relationToSourceKeyColumns'] as $columnName => $refColumnName) {
420
            $params[] = isset($sourceClass->fieldNames[$refColumnName])
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...
421
                ? $identifier[$sourceClass->fieldNames[$refColumnName]]
422
                : $identifier[$sourceClass->getFieldForColumn($refColumnName)];
423
        }
424
425
        return $params;
426
    }
427
428
    /**
429
     * Gets the SQL statement used for deleting a row from the collection.
430
     *
431
     * @param \Doctrine\ORM\PersistentCollection $collection
432
     *
433
     * @return string[]|string[][] ordered tuple containing the SQL to be executed and an array
434
     *                             of types for bound parameters
435
     */
436 98
    protected function getDeleteRowSQL(PersistentCollection $collection)
437
    {
438 98
        $mapping     = $collection->getMapping();
439 98
        $class       = $this->em->getClassMetadata($mapping['sourceEntity']);
440 98
        $targetClass = $this->em->getClassMetadata($mapping['targetEntity']);
441 98
        $columns     = [];
442 98
        $types       = [];
443
444 98
        foreach ($mapping['joinTable']['joinColumns'] as $joinColumn) {
445 98
            $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
446 98
            $types[]   = PersisterHelper::getTypeOfColumn($joinColumn['referencedColumnName'], $class, $this->em);
447
        }
448
449 98
        foreach ($mapping['joinTable']['inverseJoinColumns'] as $joinColumn) {
450 98
            $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform);
451 98
            $types[]   = PersisterHelper::getTypeOfColumn($joinColumn['referencedColumnName'], $targetClass, $this->em);
452
        }
453
454
        return [
455 98
            'DELETE FROM ' . $this->quoteStrategy->getJoinTableName($mapping, $class, $this->platform)
456 98
            . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?',
457 98
            $types,
458
        ];
459
    }
460
461
    /**
462
     * Gets the SQL parameters for the corresponding SQL statement to delete the given
463
     * element from the given collection.
464
     *
465
     * Internal note: Order of the parameters must be the same as the order of the columns in getDeleteRowSql.
466
     *
467
     * @param \Doctrine\ORM\PersistentCollection $collection
468
     * @param mixed                              $element
469
     *
470
     * @return array
471
     */
472 10
    protected function getDeleteRowSQLParameters(PersistentCollection $collection, $element)
473
    {
474 10
        return $this->collectJoinTableColumnParameters($collection, $element);
475
    }
476
477
    /**
478
     * Gets the SQL statement used for inserting a row in the collection.
479
     *
480
     * @param \Doctrine\ORM\PersistentCollection $collection
481
     *
482
     * @return string[]|string[][] ordered tuple containing the SQL to be executed and an array
483
     *                             of types for bound parameters
484
     */
485 98
    protected function getInsertRowSQL(PersistentCollection $collection)
486
    {
487 98
        $columns     = [];
488 98
        $types       = [];
489 98
        $mapping     = $collection->getMapping();
490 98
        $class       = $this->em->getClassMetadata($mapping['sourceEntity']);
491 98
        $targetClass = $this->em->getClassMetadata($mapping['targetEntity']);
492
493 98
        foreach ($mapping['joinTable']['joinColumns'] as $joinColumn) {
494 98
            $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform);
495 98
            $types[]   = PersisterHelper::getTypeOfColumn($joinColumn['referencedColumnName'], $class, $this->em);
496
        }
497
498 98
        foreach ($mapping['joinTable']['inverseJoinColumns'] as $joinColumn) {
499 98
            $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform);
500 98
            $types[]   = PersisterHelper::getTypeOfColumn($joinColumn['referencedColumnName'], $targetClass, $this->em);
501
        }
502
503
        return [
504 98
            'INSERT INTO ' . $this->quoteStrategy->getJoinTableName($mapping, $class, $this->platform)
505 98
            . ' (' . implode(', ', $columns) . ')'
506 98
            . ' VALUES'
507 98
            . ' (' . implode(', ', array_fill(0, count($columns), '?')) . ')',
508 98
            $types,
509
        ];
510
    }
511
512
    /**
513
     * Gets the SQL parameters for the corresponding SQL statement to insert the given
514
     * element of the given collection into the database.
515
     *
516
     * Internal note: Order of the parameters must be the same as the order of the columns in getInsertRowSql.
517
     *
518
     * @param \Doctrine\ORM\PersistentCollection $collection
519
     * @param mixed                              $element
520
     *
521
     * @return array
522
     */
523 98
    protected function getInsertRowSQLParameters(PersistentCollection $collection, $element)
524
    {
525 98
        return $this->collectJoinTableColumnParameters($collection, $element);
526
    }
527
528
    /**
529
     * Collects the parameters for inserting/deleting on the join table in the order
530
     * of the join table columns as specified in ManyToManyMapping#joinTableColumns.
531
     *
532
     * @param \Doctrine\ORM\PersistentCollection $collection
533
     * @param object                             $element
534
     *
535
     * @return array
536
     */
537 98
    private function collectJoinTableColumnParameters(PersistentCollection $collection, $element)
538
    {
539 98
        $params      = [];
540 98
        $mapping     = $collection->getMapping();
541 98
        $isComposite = count($mapping['joinTableColumns']) > 2;
542
543 98
        $identifier1 = $this->uow->getEntityIdentifier($collection->getOwner());
544 98
        $identifier2 = $this->uow->getEntityIdentifier($element);
545
546 98
        $class1 = $class2 = null;
547 98
        if ($isComposite) {
548 6
            $class1 = $this->em->getClassMetadata(get_class($collection->getOwner()));
549 6
            $class2 = $collection->getTypeClass();
550
        }
551
552 98
        foreach ($mapping['joinTableColumns'] as $joinTableColumn) {
553 98
            $isRelationToSource = isset($mapping['relationToSourceKeyColumns'][$joinTableColumn]);
554
555 98
            if ( ! $isComposite) {
556 92
                $params[] = $isRelationToSource ? array_pop($identifier1) : array_pop($identifier2);
557
558 92
                continue;
559
            }
560
561 6
            if ($isRelationToSource) {
562 6
                $params[] = $identifier1[$class1->getFieldForColumn($mapping['relationToSourceKeyColumns'][$joinTableColumn])];
563
564 6
                continue;
565
            }
566
567 6
            $params[] = $identifier2[$class2->getFieldForColumn($mapping['relationToTargetKeyColumns'][$joinTableColumn])];
568
        }
569
570 98
        return $params;
571
    }
572
573
    /**
574
     * @param \Doctrine\ORM\PersistentCollection $collection
575
     * @param string                             $key
576
     * @param boolean                            $addFilters Whether the filter SQL should be included or not.
577
     *
578
     * @return array ordered vector:
579
     *                - quoted join table name
580
     *                - where clauses to be added for filtering
581
     *                - parameters to be bound for filtering
582
     *                - types of the parameters to be bound for filtering
583
     */
584 2
    private function getJoinTableRestrictionsWithKey(PersistentCollection $collection, $key, $addFilters)
585
    {
586 2
        $filterMapping = $collection->getMapping();
587 2
        $mapping       = $filterMapping;
588 2
        $indexBy       = $mapping['indexBy'];
589 2
        $id            = $this->uow->getEntityIdentifier($collection->getOwner());
590 2
        $sourceClass   = $this->em->getClassMetadata($mapping['sourceEntity']);
591 2
        $targetClass   = $this->em->getClassMetadata($mapping['targetEntity']);
592
593 2
        if (! $mapping['isOwningSide']) {
594 1
            $associationSourceClass = $this->em->getClassMetadata($mapping['targetEntity']);
595 1
            $mapping                = $associationSourceClass->associationMappings[$mapping['mappedBy']];
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
596 1
            $joinColumns            = $mapping['joinTable']['joinColumns'];
597 1
            $sourceRelationMode     = 'relationToTargetKeyColumns';
598 1
            $targetRelationMode     = 'relationToSourceKeyColumns';
599
        } else {
600 1
            $associationSourceClass = $this->em->getClassMetadata($mapping['sourceEntity']);
601 1
            $joinColumns            = $mapping['joinTable']['inverseJoinColumns'];
602 1
            $sourceRelationMode     = 'relationToSourceKeyColumns';
603 1
            $targetRelationMode     = 'relationToTargetKeyColumns';
604
        }
605
606 2
        $quotedJoinTable = $this->quoteStrategy->getJoinTableName($mapping, $associationSourceClass, $this->platform). ' t';
607 2
        $whereClauses    = [];
608 2
        $params          = [];
609 2
        $types           = [];
610
611 2
        $joinNeeded = ! in_array($indexBy, $targetClass->identifier);
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...
612
613 2
        if ($joinNeeded) { // extra join needed if indexBy is not a @id
614
            $joinConditions = [];
615
616
            foreach ($joinColumns as $joinTableColumn) {
617
                $joinConditions[] = 't.' . $joinTableColumn['name'] . ' = tr.' . $joinTableColumn['referencedColumnName'];
618
            }
619
620
            $tableName        = $this->quoteStrategy->getTableName($targetClass, $this->platform);
621
            $quotedJoinTable .= ' JOIN ' . $tableName . ' tr ON ' . implode(' AND ', $joinConditions);
622
            $columnName       = $targetClass->getColumnName($indexBy);
623
624
            $whereClauses[] = 'tr.' . $columnName . ' = ?';
625
            $params[]       = $key;
626
            $types[]        = PersisterHelper::getTypeOfColumn($columnName, $targetClass, $this->em);
627
        }
628
629 2
        foreach ($mapping['joinTableColumns'] as $joinTableColumn) {
630 2
            if (isset($mapping[$sourceRelationMode][$joinTableColumn])) {
631 2
                $column         = $mapping[$sourceRelationMode][$joinTableColumn];
632 2
                $whereClauses[] = 't.' . $joinTableColumn . ' = ?';
633 2
                $params[]       = $sourceClass->containsForeignIdentifier
0 ignored issues
show
Bug introduced by
Accessing containsForeignIdentifier on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
634
                    ? $id[$sourceClass->getFieldForColumn($column)]
635 2
                    : $id[$sourceClass->fieldNames[$column]];
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...
636 2
                $types[]        = PersisterHelper::getTypeOfColumn($column, $sourceClass, $this->em);
637 2
            } elseif ( ! $joinNeeded) {
638 2
                $column = $mapping[$targetRelationMode][$joinTableColumn];
639
640 2
                $whereClauses[] = 't.' . $joinTableColumn . ' = ?';
641 2
                $params[]       = $key;
642 2
                $types[]        = PersisterHelper::getTypeOfColumn($column, $targetClass, $this->em);
643
            }
644
        }
645
646 2
        if ($addFilters) {
647 2
            list($joinTargetEntitySQL, $filterSql) = $this->getFilterSql($filterMapping);
648
649 2
            if ($filterSql) {
650
                $quotedJoinTable .= ' ' . $joinTargetEntitySQL;
651
                $whereClauses[] = $filterSql;
652
            }
653
        }
654
655 2
        return [$quotedJoinTable, $whereClauses, $params, $types];
656
    }
657
658
    /**
659
     * @param \Doctrine\ORM\PersistentCollection $collection
660
     * @param object                             $element
661
     * @param boolean                            $addFilters Whether the filter SQL should be included or not.
662
     *
663
     * @return array ordered vector:
664
     *                - quoted join table name
665
     *                - where clauses to be added for filtering
666
     *                - parameters to be bound for filtering
667
     *                - types of the parameters to be bound for filtering
668
     */
669 2
    private function getJoinTableRestrictions(PersistentCollection $collection, $element, $addFilters)
670
    {
671 2
        $filterMapping  = $collection->getMapping();
672 2
        $mapping        = $filterMapping;
673
674 2
        if ( ! $mapping['isOwningSide']) {
675 1
            $sourceClass = $this->em->getClassMetadata($mapping['targetEntity']);
676 1
            $targetClass = $this->em->getClassMetadata($mapping['sourceEntity']);
677 1
            $sourceId = $this->uow->getEntityIdentifier($element);
678 1
            $targetId = $this->uow->getEntityIdentifier($collection->getOwner());
679
680 1
            $mapping = $sourceClass->associationMappings[$mapping['mappedBy']];
0 ignored issues
show
Bug introduced by
Accessing associationMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
681
        } else {
682 1
            $sourceClass = $this->em->getClassMetadata($mapping['sourceEntity']);
683 1
            $targetClass = $this->em->getClassMetadata($mapping['targetEntity']);
684 1
            $sourceId = $this->uow->getEntityIdentifier($collection->getOwner());
685 1
            $targetId = $this->uow->getEntityIdentifier($element);
686
        }
687
688 2
        $quotedJoinTable = $this->quoteStrategy->getJoinTableName($mapping, $sourceClass, $this->platform);
689 2
        $whereClauses    = [];
690 2
        $params          = [];
691 2
        $types           = [];
692
693 2
        foreach ($mapping['joinTableColumns'] as $joinTableColumn) {
694 2
            $whereClauses[] = ($addFilters ? 't.' : '') . $joinTableColumn . ' = ?';
695
696 2
            if (isset($mapping['relationToTargetKeyColumns'][$joinTableColumn])) {
697 2
                $targetColumn = $mapping['relationToTargetKeyColumns'][$joinTableColumn];
698 2
                $params[]     = $targetId[$targetClass->getFieldForColumn($targetColumn)];
699 2
                $types[]      = PersisterHelper::getTypeOfColumn($targetColumn, $targetClass, $this->em);
700
701 2
                continue;
702
            }
703
704
            // relationToSourceKeyColumns
705 2
            $targetColumn = $mapping['relationToSourceKeyColumns'][$joinTableColumn];
706 2
            $params[]     = $sourceId[$sourceClass->getFieldForColumn($targetColumn)];
707 2
            $types[]      = PersisterHelper::getTypeOfColumn($targetColumn, $sourceClass, $this->em);
708
        }
709
710 2
        if ($addFilters) {
711 2
            $quotedJoinTable .= ' t';
712
713 2
            list($joinTargetEntitySQL, $filterSql) = $this->getFilterSql($filterMapping);
714
715 2
            if ($filterSql) {
716
                $quotedJoinTable .= ' ' . $joinTargetEntitySQL;
717
                $whereClauses[] = $filterSql;
718
            }
719
        }
720
721 2
        return [$quotedJoinTable, $whereClauses, $params, $types];
722
    }
723
724
    /**
725
     * Expands Criteria Parameters by walking the expressions and grabbing all
726
     * parameters and types from it.
727
     *
728
     * @param \Doctrine\Common\Collections\Criteria $criteria
729
     *
730
     * @return array
731
     */
732 12
    private function expandCriteriaParameters(Criteria $criteria)
733
    {
734 12
        $expression = $criteria->getWhereExpression();
735
736 12
        if ($expression === null) {
737 5
            return [];
738
        }
739
740 7
        $valueVisitor = new SqlValueVisitor();
741
742 7
        $valueVisitor->dispatch($expression);
743
744 7
        list(, $types) = $valueVisitor->getParamsAndTypes();
745
746 7
        return $types;
747
    }
748
749
    /**
750
     * @param Criteria $criteria
751
     * @param ClassMetadata $targetClass
752
     * @return string
753
     */
754 12
    private function getOrderingSql(Criteria $criteria, ClassMetadata $targetClass)
755
    {
756 12
        $orderings = $criteria->getOrderings();
757 12
        if ($orderings) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $orderings of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

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

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

Loading history...
758 2
            $orderBy = [];
759 2
            foreach ($orderings as $name => $direction) {
760 2
                $field = $this->quoteStrategy->getColumnName(
761 2
                    $name,
762 2
                    $targetClass,
763 2
                    $this->platform
764
                );
765 2
                $orderBy[] = $field . ' ' . $direction;
766
            }
767
768 2
            return ' ORDER BY ' . implode(', ', $orderBy);
769
        }
770 10
        return '';
771
    }
772
773
    /**
774
     * @param Criteria $criteria
775
     * @return string
776
     * @throws \Doctrine\DBAL\DBALException
777
     */
778 12
    private function getLimitSql(Criteria $criteria)
779
    {
780 12
        $limit  = $criteria->getMaxResults();
781 12
        $offset = $criteria->getFirstResult();
782 12
        if ($limit !== null || $offset !== null) {
783 3
            return $this->platform->modifyLimitQuery('', $limit, $offset);
784
        }
785 9
        return '';
786
    }
787
}
788