Passed
Pull Request — 2.6 (#7336)
by
unknown
08:40
created

ManyToManyPersister::getDeleteSQL()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 8
nc 2
nop 1
dl 0
loc 13
rs 10
c 0
b 0
f 0
ccs 9
cts 9
cp 1
crap 2
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 18
    public function delete(PersistentCollection $collection)
43
    {
44 18
        $mapping = $collection->getMapping();
45
46 18
        if ( ! $mapping['isOwningSide']) {
47
            return; // ignore inverse side
48
        }
49
50 18
        $types = [];
51 18
        $class = $this->em->getClassMetadata($mapping['sourceEntity']);
52
53 18
        foreach ($mapping['joinTable']['joinColumns'] as $joinColumn) {
54 18
            $types[] = PersisterHelper::getTypeOfColumn($joinColumn['referencedColumnName'], $class, $this->em);
55
        }
56
57 18
        $this->conn->executeUpdate($this->getDeleteSQL($collection), $this->getDeleteSQLParameters($collection), $types);
58 18
    }
59
60
    /**
61
     * {@inheritdoc}
62
     */
63 335
    public function update(PersistentCollection $collection)
64
    {
65 335
        $mapping = $collection->getMapping();
66
67 335
        if ( ! $mapping['isOwningSide']) {
68 237
            return; // ignore inverse side
69
        }
70
71 334
        list($deleteSql, $deleteTypes) = $this->getDeleteRowSQL($collection);
72 334
        list($insertSql, $insertTypes) = $this->getInsertRowSQL($collection);
73
74 334
        foreach ($collection->getDeleteDiff() as $element) {
75 12
            $this->conn->executeUpdate(
76 12
                $deleteSql,
77 12
                $this->getDeleteRowSQLParameters($collection, $element),
78 12
                $deleteTypes
79
            );
80
        }
81
82 334
        foreach ($collection->getInsertDiff() as $element) {
83 334
            $this->conn->executeUpdate(
84 334
                $insertSql,
85 334
                $this->getInsertRowSQLParameters($collection, $element),
86 334
                $insertTypes
87
            );
88
        }
89 334
    }
90
91
    /**
92
     * {@inheritdoc}
93
     */
94 3
    public function get(PersistentCollection $collection, $index)
95
    {
96 3
        $mapping = $collection->getMapping();
97
98 3
        if ( ! isset($mapping['indexBy'])) {
99
            throw new \BadMethodCallException("Selecting a collection by index is only supported on indexed collections.");
100
        }
101
102 3
        $persister = $this->uow->getEntityPersister($mapping['targetEntity']);
103 3
        $mappedKey = $mapping['isOwningSide']
104 2
            ? $mapping['inversedBy']
105 3
            : $mapping['mappedBy'];
106
107 3
        return $persister->load([$mappedKey => $collection->getOwner(), $mapping['indexBy'] => $index], null, $mapping, [], 0, 1);
108
    }
109
110
    /**
111
     * {@inheritdoc}
112
     */
113 18
    public function count(PersistentCollection $collection)
114
    {
115 18
        $conditions     = [];
116 18
        $params         = [];
117 18
        $types          = [];
118 18
        $mapping        = $collection->getMapping();
119 18
        $id             = $this->uow->getEntityIdentifier($collection->getOwner());
120 18
        $sourceClass    = $this->em->getClassMetadata($mapping['sourceEntity']);
121 18
        $targetClass    = $this->em->getClassMetadata($mapping['targetEntity']);
122 18
        $association    = ( ! $mapping['isOwningSide'])
123 4
            ? $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 18
            : $mapping;
125
126 18
        $joinTableName  = $this->quoteStrategy->getJoinTableName($association, $sourceClass, $this->platform);
127 18
        $joinColumns    = ( ! $mapping['isOwningSide'])
128 4
            ? $association['joinTable']['inverseJoinColumns']
129 18
            : $association['joinTable']['joinColumns'];
130
131 18
        foreach ($joinColumns as $joinColumn) {
132 18
            $columnName     = $this->quoteStrategy->getJoinColumnName($joinColumn, $sourceClass, $this->platform);
133 18
            $referencedName = $joinColumn['referencedColumnName'];
134 18
            $conditions[]   = 't.' . $columnName . ' = ?';
135 18
            $params[]       = $id[$sourceClass->getFieldForColumn($referencedName)];
136 18
            $types[]        = PersisterHelper::getTypeOfColumn($referencedName, $sourceClass, $this->em);
137
        }
138
139 18
        list($joinTargetEntitySQL, $filterSql) = $this->getFilterSql($mapping);
140
141 18
        if ($filterSql) {
142 3
            $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) {
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...
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 18
            . ' FROM ' . $joinTableName . ' t'
164 18
            . $joinTargetEntitySQL
165 18
            . ' WHERE ' . implode(' AND ', $conditions);
166
167 18
        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) returns the type string|boolean which is incompatible with the return type mandated by Doctrine\ORM\Persisters\...ctionPersister::count() of integer.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
168
    }
169
170
    /**
171
     * {@inheritDoc}
172
     */
173 8
    public function slice(PersistentCollection $collection, $offset, $length = null)
174
    {
175 8
        $mapping   = $collection->getMapping();
176 8
        $persister = $this->uow->getEntityPersister($mapping['targetEntity']);
177
178 8
        return $persister->getManyToManyCollection($mapping, $collection->getOwner(), $offset, $length);
179
    }
180
    /**
181
     * {@inheritdoc}
182
     */
183 7
    public function containsKey(PersistentCollection $collection, $key)
184
    {
185 7
        $mapping = $collection->getMapping();
186
187 7
        if ( ! isset($mapping['indexBy'])) {
188
            throw new \BadMethodCallException("Selecting a collection by index is only supported on indexed collections.");
189
        }
190
191 7
        list($quotedJoinTable, $whereClauses, $params, $types) = $this->getJoinTableRestrictionsWithKey($collection, $key, true);
192
193 7
        $sql = 'SELECT 1 FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses);
194
195 7
        return (bool) $this->conn->fetchColumn($sql, $params, 0, $types);
196
    }
197
198
    /**
199
     * {@inheritDoc}
200
     */
201 7
    public function contains(PersistentCollection $collection, $element)
202
    {
203 7
        if ( ! $this->isValidEntityState($element)) {
204 2
            return false;
205
        }
206
207 7
        list($quotedJoinTable, $whereClauses, $params, $types) = $this->getJoinTableRestrictions($collection, $element, true);
208
209 7
        $sql = 'SELECT 1 FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses);
210
211 7
        return (bool) $this->conn->fetchColumn($sql, $params, 0, $types);
212
    }
213
214
    /**
215
     * {@inheritDoc}
216
     */
217 2
    public function removeElement(PersistentCollection $collection, $element)
218
    {
219 2
        if ( ! $this->isValidEntityState($element)) {
220 2
            return false;
221
        }
222
223 2
        list($quotedJoinTable, $whereClauses, $params, $types) = $this->getJoinTableRestrictions($collection, $element, false);
224
225 2
        $sql = 'DELETE FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses);
226
227 2
        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 32
    public function getFilterSql($mapping)
309
    {
310 32
        $targetClass = $this->em->getClassMetadata($mapping['targetEntity']);
311 32
        $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 32
        $filterSql   = $this->generateFilterConditionSQL($rootClass, 'te');
313
314 32
        if ('' === $filterSql) {
315 32
            return ['', ''];
316
        }
317
318
        // A join is needed if there is filtering on the target entity
319 6
        $tableName = $this->quoteStrategy->getTableName($rootClass, $this->platform);
320 6
        $joinSql   = ' JOIN ' . $tableName . ' te'
321 6
            . ' ON' . implode(' AND ', $this->getOnConditionSQL($mapping));
322
323 6
        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 32
    protected function generateFilterConditionSQL(ClassMetadata $targetEntity, $targetTableAlias)
335
    {
336 32
        $filterClauses = [];
337
338 32
        foreach ($this->em->getFilters()->getEnabledFilters() as $filter) {
339 6
            if ($filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias)) {
340 6
                $filterClauses[] = '(' . $filterExpr . ')';
341
            }
342
        }
343
344 32
        return $filterClauses
345 6
            ? '(' . implode(' AND ', $filterClauses) . ')'
346 32
            : '';
347
    }
348
349
    /**
350
     * Generate ON condition
351
     *
352
     * @param  array $mapping
353
     *
354
     * @return array
355
     */
356 18
    protected function getOnConditionSQL($mapping)
357
    {
358 18
        $targetClass = $this->em->getClassMetadata($mapping['targetEntity']);
359 18
        $association = ( ! $mapping['isOwningSide'])
360 3
            ? $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 18
            : $mapping;
362
363 18
        $joinColumns = $mapping['isOwningSide']
364 15
            ? $association['joinTable']['inverseJoinColumns']
365 18
            : $association['joinTable']['joinColumns'];
366
367 18
        $conditions = [];
368
369 18
        foreach ($joinColumns as $joinColumn) {
370 18
            $joinColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform);
371 18
            $refColumnName  = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform);
372
373 18
            $conditions[] = ' t.' . $joinColumnName . ' = ' . 'te.' . $refColumnName;
374
        }
375
376 18
        return $conditions;
377
    }
378
379
    /**
380
     * {@inheritdoc}
381
     *
382
     * @override
383
     */
384 18
    protected function getDeleteSQL(PersistentCollection $collection)
385
    {
386 18
        $columns    = [];
387 18
        $mapping    = $collection->getMapping();
388 18
        $class      = $this->em->getClassMetadata(get_class($collection->getOwner()));
389 18
        $joinTable  = $this->quoteStrategy->getJoinTableName($mapping, $class, $this->platform);
390
391 18
        foreach ($mapping['joinTable']['joinColumns'] as $joinColumn) {
392 18
            $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
393
        }
394
395 18
        return 'DELETE FROM ' . $joinTable
396 18
            . ' 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 18
    protected function getDeleteSQLParameters(PersistentCollection $collection)
406
    {
407 18
        $mapping    = $collection->getMapping();
408 18
        $identifier = $this->uow->getEntityIdentifier($collection->getOwner());
409
410
        // Optimization for single column identifier
411 18
        if (count($mapping['relationToSourceKeyColumns']) === 1) {
412 15
            return [reset($identifier)];
413
        }
414
415
        // Composite identifier
416 3
        $sourceClass = $this->em->getClassMetadata($mapping['sourceEntity']);
417 3
        $params      = [];
418
419 3
        foreach ($mapping['relationToSourceKeyColumns'] as $columnName => $refColumnName) {
420 3
            $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 2
                ? $identifier[$sourceClass->fieldNames[$refColumnName]]
422 3
                : $identifier[$sourceClass->getFieldForColumn($refColumnName)];
423
        }
424
425 3
        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 334
    protected function getDeleteRowSQL(PersistentCollection $collection)
437
    {
438 334
        $mapping     = $collection->getMapping();
439 334
        $class       = $this->em->getClassMetadata($mapping['sourceEntity']);
440 334
        $targetClass = $this->em->getClassMetadata($mapping['targetEntity']);
441 334
        $columns     = [];
442 334
        $types       = [];
443
444 334
        foreach ($mapping['joinTable']['joinColumns'] as $joinColumn) {
445 334
            $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
446 334
            $types[]   = PersisterHelper::getTypeOfColumn($joinColumn['referencedColumnName'], $class, $this->em);
447
        }
448
449 334
        foreach ($mapping['joinTable']['inverseJoinColumns'] as $joinColumn) {
450 334
            $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform);
451 334
            $types[]   = PersisterHelper::getTypeOfColumn($joinColumn['referencedColumnName'], $targetClass, $this->em);
452
        }
453
454
        return [
455 334
            'DELETE FROM ' . $this->quoteStrategy->getJoinTableName($mapping, $class, $this->platform)
456 334
            . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?',
457 334
            $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 12
    protected function getDeleteRowSQLParameters(PersistentCollection $collection, $element)
473
    {
474 12
        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 334
    protected function getInsertRowSQL(PersistentCollection $collection)
486
    {
487 334
        $columns     = [];
488 334
        $types       = [];
489 334
        $mapping     = $collection->getMapping();
490 334
        $class       = $this->em->getClassMetadata($mapping['sourceEntity']);
491 334
        $targetClass = $this->em->getClassMetadata($mapping['targetEntity']);
492
493 334
        foreach ($mapping['joinTable']['joinColumns'] as $joinColumn) {
494 334
            $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform);
495 334
            $types[]   = PersisterHelper::getTypeOfColumn($joinColumn['referencedColumnName'], $class, $this->em);
496
        }
497
498 334
        foreach ($mapping['joinTable']['inverseJoinColumns'] as $joinColumn) {
499 334
            $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform);
500 334
            $types[]   = PersisterHelper::getTypeOfColumn($joinColumn['referencedColumnName'], $targetClass, $this->em);
501
        }
502
503
        return [
504 334
            'INSERT INTO ' . $this->quoteStrategy->getJoinTableName($mapping, $class, $this->platform)
505 334
            . ' (' . implode(', ', $columns) . ')'
506 334
            . ' VALUES'
507 334
            . ' (' . implode(', ', array_fill(0, count($columns), '?')) . ')',
508 334
            $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 334
    protected function getInsertRowSQLParameters(PersistentCollection $collection, $element)
524
    {
525 334
        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 334
    private function collectJoinTableColumnParameters(PersistentCollection $collection, $element)
538
    {
539 334
        $params      = [];
540 334
        $mapping     = $collection->getMapping();
541
542 334
        $identifier1 = $this->uow->getEntityIdentifier($collection->getOwner());
543 334
        $identifier2 = $this->uow->getEntityIdentifier($element);
544
545 334
        $class1 = $this->em->getClassMetadata(get_class($collection->getOwner()));
546 334
        $class2 = $collection->getTypeClass();
547
548
549 334
        foreach ($mapping['joinTableColumns'] as $joinTableColumn) {
550 334
            $isRelationToSource = isset($mapping['relationToSourceKeyColumns'][$joinTableColumn]);
551
552 334
            if ($isRelationToSource) {
553 334
                $params[] = $identifier1[$class1->getFieldForColumn($mapping['relationToSourceKeyColumns'][$joinTableColumn])];
554
555 334
                continue;
556
            }
557
558 334
            $params[] = $identifier2[$class2->getFieldForColumn($mapping['relationToTargetKeyColumns'][$joinTableColumn])];
559
        }
560
561 334
        return $params;
562
    }
563
564
    /**
565
     * @param \Doctrine\ORM\PersistentCollection $collection
566
     * @param string                             $key
567
     * @param boolean                            $addFilters Whether the filter SQL should be included or not.
568
     *
569
     * @return array ordered vector:
570
     *                - quoted join table name
571
     *                - where clauses to be added for filtering
572
     *                - parameters to be bound for filtering
573
     *                - types of the parameters to be bound for filtering
574
     */
575 7
    private function getJoinTableRestrictionsWithKey(PersistentCollection $collection, $key, $addFilters)
576
    {
577 7
        $filterMapping = $collection->getMapping();
578 7
        $mapping       = $filterMapping;
579 7
        $indexBy       = $mapping['indexBy'];
580 7
        $id            = $this->uow->getEntityIdentifier($collection->getOwner());
581 7
        $sourceClass   = $this->em->getClassMetadata($mapping['sourceEntity']);
582 7
        $targetClass   = $this->em->getClassMetadata($mapping['targetEntity']);
583
584 7
        if (! $mapping['isOwningSide']) {
585 3
            $associationSourceClass = $this->em->getClassMetadata($mapping['targetEntity']);
586 3
            $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...
587 3
            $joinColumns            = $mapping['joinTable']['joinColumns'];
588 3
            $sourceRelationMode     = 'relationToTargetKeyColumns';
589 3
            $targetRelationMode     = 'relationToSourceKeyColumns';
590
        } else {
591 4
            $associationSourceClass = $this->em->getClassMetadata($mapping['sourceEntity']);
592 4
            $joinColumns            = $mapping['joinTable']['inverseJoinColumns'];
593 4
            $sourceRelationMode     = 'relationToSourceKeyColumns';
594 4
            $targetRelationMode     = 'relationToTargetKeyColumns';
595
        }
596
597 7
        $quotedJoinTable = $this->quoteStrategy->getJoinTableName($mapping, $associationSourceClass, $this->platform). ' t';
598 7
        $whereClauses    = [];
599 7
        $params          = [];
600 7
        $types           = [];
601
602 7
        $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...
603
604 7
        if ($joinNeeded) { // extra join needed if indexBy is not a @id
605 3
            $joinConditions = [];
606
607 3
            foreach ($joinColumns as $joinTableColumn) {
608 3
                $joinConditions[] = 't.' . $joinTableColumn['name'] . ' = tr.' . $joinTableColumn['referencedColumnName'];
609
            }
610
611 3
            $tableName        = $this->quoteStrategy->getTableName($targetClass, $this->platform);
612 3
            $quotedJoinTable .= ' JOIN ' . $tableName . ' tr ON ' . implode(' AND ', $joinConditions);
613 3
            $columnName       = $targetClass->getColumnName($indexBy);
614
615 3
            $whereClauses[] = 'tr.' . $columnName . ' = ?';
616 3
            $params[]       = $key;
617 3
            $types[]        = PersisterHelper::getTypeOfColumn($columnName, $targetClass, $this->em);
618
        }
619
620 7
        foreach ($mapping['joinTableColumns'] as $joinTableColumn) {
621 7
            if (isset($mapping[$sourceRelationMode][$joinTableColumn])) {
622 7
                $column         = $mapping[$sourceRelationMode][$joinTableColumn];
623 7
                $whereClauses[] = 't.' . $joinTableColumn . ' = ?';
624 7
                $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...
625
                    ? $id[$sourceClass->getFieldForColumn($column)]
626 7
                    : $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...
627 7
                $types[]        = PersisterHelper::getTypeOfColumn($column, $sourceClass, $this->em);
628 7
            } elseif ( ! $joinNeeded) {
629 4
                $column = $mapping[$targetRelationMode][$joinTableColumn];
630
631 4
                $whereClauses[] = 't.' . $joinTableColumn . ' = ?';
632 4
                $params[]       = $key;
633 7
                $types[]        = PersisterHelper::getTypeOfColumn($column, $targetClass, $this->em);
634
            }
635
        }
636
637 7
        if ($addFilters) {
638 7
            list($joinTargetEntitySQL, $filterSql) = $this->getFilterSql($filterMapping);
639
640 7
            if ($filterSql) {
641
                $quotedJoinTable .= ' ' . $joinTargetEntitySQL;
642
                $whereClauses[] = $filterSql;
643
            }
644
        }
645
646 7
        return [$quotedJoinTable, $whereClauses, $params, $types];
647
    }
648
649
    /**
650
     * @param \Doctrine\ORM\PersistentCollection $collection
651
     * @param object                             $element
652
     * @param boolean                            $addFilters Whether the filter SQL should be included or not.
653
     *
654
     * @return array ordered vector:
655
     *                - quoted join table name
656
     *                - where clauses to be added for filtering
657
     *                - parameters to be bound for filtering
658
     *                - types of the parameters to be bound for filtering
659
     */
660 9
    private function getJoinTableRestrictions(PersistentCollection $collection, $element, $addFilters)
661
    {
662 9
        $filterMapping  = $collection->getMapping();
663 9
        $mapping        = $filterMapping;
664
665 9
        if ( ! $mapping['isOwningSide']) {
666 4
            $sourceClass = $this->em->getClassMetadata($mapping['targetEntity']);
667 4
            $targetClass = $this->em->getClassMetadata($mapping['sourceEntity']);
668 4
            $sourceId = $this->uow->getEntityIdentifier($element);
669 4
            $targetId = $this->uow->getEntityIdentifier($collection->getOwner());
670
671 4
            $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...
672
        } else {
673 5
            $sourceClass = $this->em->getClassMetadata($mapping['sourceEntity']);
674 5
            $targetClass = $this->em->getClassMetadata($mapping['targetEntity']);
675 5
            $sourceId = $this->uow->getEntityIdentifier($collection->getOwner());
676 5
            $targetId = $this->uow->getEntityIdentifier($element);
677
        }
678
679 9
        $quotedJoinTable = $this->quoteStrategy->getJoinTableName($mapping, $sourceClass, $this->platform);
680 9
        $whereClauses    = [];
681 9
        $params          = [];
682 9
        $types           = [];
683
684 9
        foreach ($mapping['joinTableColumns'] as $joinTableColumn) {
685 9
            $whereClauses[] = ($addFilters ? 't.' : '') . $joinTableColumn . ' = ?';
686
687 9
            if (isset($mapping['relationToTargetKeyColumns'][$joinTableColumn])) {
688 9
                $targetColumn = $mapping['relationToTargetKeyColumns'][$joinTableColumn];
689 9
                $params[]     = $targetId[$targetClass->getFieldForColumn($targetColumn)];
690 9
                $types[]      = PersisterHelper::getTypeOfColumn($targetColumn, $targetClass, $this->em);
691
692 9
                continue;
693
            }
694
695
            // relationToSourceKeyColumns
696 9
            $targetColumn = $mapping['relationToSourceKeyColumns'][$joinTableColumn];
697 9
            $params[]     = $sourceId[$sourceClass->getFieldForColumn($targetColumn)];
698 9
            $types[]      = PersisterHelper::getTypeOfColumn($targetColumn, $sourceClass, $this->em);
699
        }
700
701 9
        if ($addFilters) {
702 7
            $quotedJoinTable .= ' t';
703
704 7
            list($joinTargetEntitySQL, $filterSql) = $this->getFilterSql($filterMapping);
705
706 7
            if ($filterSql) {
707 3
                $quotedJoinTable .= ' ' . $joinTargetEntitySQL;
708 3
                $whereClauses[] = $filterSql;
709
            }
710
        }
711
712 9
        return [$quotedJoinTable, $whereClauses, $params, $types];
713
    }
714
715
    /**
716
     * Expands Criteria Parameters by walking the expressions and grabbing all
717
     * parameters and types from it.
718
     *
719
     * @param \Doctrine\Common\Collections\Criteria $criteria
720
     *
721
     * @return array
722
     */
723 12
    private function expandCriteriaParameters(Criteria $criteria)
724
    {
725 12
        $expression = $criteria->getWhereExpression();
726
727 12
        if ($expression === null) {
728 5
            return [];
729
        }
730
731 7
        $valueVisitor = new SqlValueVisitor();
732
733 7
        $valueVisitor->dispatch($expression);
734
735 7
        list(, $types) = $valueVisitor->getParamsAndTypes();
736
737 7
        return $types;
738
    }
739
740
    /**
741
     * @param Criteria $criteria
742
     * @param ClassMetadata $targetClass
743
     * @return string
744
     */
745 12
    private function getOrderingSql(Criteria $criteria, ClassMetadata $targetClass)
746
    {
747 12
        $orderings = $criteria->getOrderings();
748 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...
749 2
            $orderBy = [];
750 2
            foreach ($orderings as $name => $direction) {
751 2
                $field = $this->quoteStrategy->getColumnName(
752 2
                    $name,
753 2
                    $targetClass,
754 2
                    $this->platform
755
                );
756 2
                $orderBy[] = $field . ' ' . $direction;
757
            }
758
759 2
            return ' ORDER BY ' . implode(', ', $orderBy);
760
        }
761 10
        return '';
762
    }
763
764
    /**
765
     * @param Criteria $criteria
766
     * @return string
767
     * @throws \Doctrine\DBAL\DBALException
768
     */
769 12
    private function getLimitSql(Criteria $criteria)
770
    {
771 12
        $limit  = $criteria->getMaxResults();
772 12
        $offset = $criteria->getFirstResult();
773 12
        if ($limit !== null || $offset !== null) {
774 3
            return $this->platform->modifyLimitQuery('', $limit, $offset);
775
        }
776 9
        return '';
777
    }
778
}
779