Completed
Push — 2.6 ( ffb7d4...87ee40 )
by Marco
13s
created

BasicEntityPersister::updateTable()   D

Complexity

Conditions 15
Paths 185

Size

Total Lines 92
Code Lines 60

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 58
CRAP Score 15.001

Importance

Changes 0
Metric Value
cc 15
eloc 60
nc 185
nop 4
dl 0
loc 92
ccs 58
cts 59
cp 0.9831
crap 15.001
rs 4.597
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/*
3
 * 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\Entity;
21
22
use Doctrine\Common\Collections\Criteria;
23
use Doctrine\Common\Collections\Expr\Comparison;
24
use Doctrine\Common\Util\ClassUtils;
25
use Doctrine\DBAL\Connection;
26
use Doctrine\DBAL\LockMode;
27
use Doctrine\DBAL\Types\Type;
28
use Doctrine\ORM\EntityManagerInterface;
29
use Doctrine\ORM\Mapping\ClassMetadata;
30
use Doctrine\ORM\Mapping\MappingException;
31
use Doctrine\ORM\OptimisticLockException;
32
use Doctrine\ORM\ORMException;
33
use Doctrine\ORM\PersistentCollection;
34
use Doctrine\ORM\Persisters\SqlExpressionVisitor;
35
use Doctrine\ORM\Persisters\SqlValueVisitor;
36
use Doctrine\ORM\Query;
37
use Doctrine\ORM\UnitOfWork;
38
use Doctrine\ORM\Utility\IdentifierFlattener;
39
use Doctrine\ORM\Utility\PersisterHelper;
40
use function array_merge;
41
use function reset;
42
43
/**
44
 * A BasicEntityPersister maps an entity to a single table in a relational database.
45
 *
46
 * A persister is always responsible for a single entity type.
47
 *
48
 * EntityPersisters are used during a UnitOfWork to apply any changes to the persistent
49
 * state of entities onto a relational database when the UnitOfWork is committed,
50
 * as well as for basic querying of entities and their associations (not DQL).
51
 *
52
 * The persisting operations that are invoked during a commit of a UnitOfWork to
53
 * persist the persistent entity state are:
54
 *
55
 *   - {@link addInsert} : To schedule an entity for insertion.
56
 *   - {@link executeInserts} : To execute all scheduled insertions.
57
 *   - {@link update} : To update the persistent state of an entity.
58
 *   - {@link delete} : To delete the persistent state of an entity.
59
 *
60
 * As can be seen from the above list, insertions are batched and executed all at once
61
 * for increased efficiency.
62
 *
63
 * The querying operations invoked during a UnitOfWork, either through direct find
64
 * requests or lazy-loading, are the following:
65
 *
66
 *   - {@link load} : Loads (the state of) a single, managed entity.
67
 *   - {@link loadAll} : Loads multiple, managed entities.
68
 *   - {@link loadOneToOneEntity} : Loads a one/many-to-one entity association (lazy-loading).
69
 *   - {@link loadOneToManyCollection} : Loads a one-to-many entity association (lazy-loading).
70
 *   - {@link loadManyToManyCollection} : Loads a many-to-many entity association (lazy-loading).
71
 *
72
 * The BasicEntityPersister implementation provides the default behavior for
73
 * persisting and querying entities that are mapped to a single database table.
74
 *
75
 * Subclasses can be created to provide custom persisting and querying strategies,
76
 * i.e. spanning multiple tables.
77
 *
78
 * @author Roman Borschel <[email protected]>
79
 * @author Giorgio Sironi <[email protected]>
80
 * @author Benjamin Eberlei <[email protected]>
81
 * @author Alexander <[email protected]>
82
 * @author Fabio B. Silva <[email protected]>
83
 * @author Rob Caiger <[email protected]>
84
 * @since 2.0
85
 */
86
class BasicEntityPersister implements EntityPersister
87
{
88
    /**
89
     * @var array
90
     */
91
    static private $comparisonMap = [
92
        Comparison::EQ          => '= %s',
93
        Comparison::IS          => '= %s',
94
        Comparison::NEQ         => '!= %s',
95
        Comparison::GT          => '> %s',
96
        Comparison::GTE         => '>= %s',
97
        Comparison::LT          => '< %s',
98
        Comparison::LTE         => '<= %s',
99
        Comparison::IN          => 'IN (%s)',
100
        Comparison::NIN         => 'NOT IN (%s)',
101
        Comparison::CONTAINS    => 'LIKE %s',
102
        Comparison::STARTS_WITH => 'LIKE %s',
103
        Comparison::ENDS_WITH   => 'LIKE %s',
104
    ];
105
106
    /**
107
     * Metadata object that describes the mapping of the mapped entity class.
108
     *
109
     * @var \Doctrine\ORM\Mapping\ClassMetadata
110
     */
111
    protected $class;
112
113
    /**
114
     * The underlying DBAL Connection of the used EntityManager.
115
     *
116
     * @var \Doctrine\DBAL\Connection $conn
117
     */
118
    protected $conn;
119
120
    /**
121
     * The database platform.
122
     *
123
     * @var \Doctrine\DBAL\Platforms\AbstractPlatform
124
     */
125
    protected $platform;
126
127
    /**
128
     * The EntityManager instance.
129
     *
130
     * @var EntityManagerInterface
131
     */
132
    protected $em;
133
134
    /**
135
     * Queued inserts.
136
     *
137
     * @var array
138
     */
139
    protected $queuedInserts = [];
140
141
    /**
142
     * The map of column names to DBAL mapping types of all prepared columns used
143
     * when INSERTing or UPDATEing an entity.
144
     *
145
     * @var array
146
     *
147
     * @see prepareInsertData($entity)
148
     * @see prepareUpdateData($entity)
149
     */
150
    protected $columnTypes = [];
151
152
    /**
153
     * The map of quoted column names.
154
     *
155
     * @var array
156
     *
157
     * @see prepareInsertData($entity)
158
     * @see prepareUpdateData($entity)
159
     */
160
    protected $quotedColumns = [];
161
162
    /**
163
     * The INSERT SQL statement used for entities handled by this persister.
164
     * This SQL is only generated once per request, if at all.
165
     *
166
     * @var string
167
     */
168
    private $insertSql;
169
170
    /**
171
     * The quote strategy.
172
     *
173
     * @var \Doctrine\ORM\Mapping\QuoteStrategy
174
     */
175
    protected $quoteStrategy;
176
177
    /**
178
     * The IdentifierFlattener used for manipulating identifiers
179
     *
180
     * @var \Doctrine\ORM\Utility\IdentifierFlattener
181
     */
182
    private $identifierFlattener;
183
184
    /**
185
     * @var CachedPersisterContext
186
     */
187
    protected $currentPersisterContext;
188
189
    /**
190
     * @var CachedPersisterContext
191
     */
192
    private $limitsHandlingContext;
193
194
    /**
195
     * @var CachedPersisterContext
196
     */
197
    private $noLimitsContext;
198
199
    /**
200
     * Initializes a new <tt>BasicEntityPersister</tt> that uses the given EntityManager
201
     * and persists instances of the class described by the given ClassMetadata descriptor.
202
     *
203
     * @param EntityManagerInterface $em
204
     * @param ClassMetadata          $class
205
     */
206 1178
    public function __construct(EntityManagerInterface $em, ClassMetadata $class)
207
    {
208 1178
        $this->em                    = $em;
209 1178
        $this->class                 = $class;
210 1178
        $this->conn                  = $em->getConnection();
211 1178
        $this->platform              = $this->conn->getDatabasePlatform();
212 1178
        $this->quoteStrategy         = $em->getConfiguration()->getQuoteStrategy();
213 1178
        $this->identifierFlattener   = new IdentifierFlattener($em->getUnitOfWork(), $em->getMetadataFactory());
214 1178
        $this->noLimitsContext       = $this->currentPersisterContext = new CachedPersisterContext(
215 1178
            $class,
216 1178
            new Query\ResultSetMapping(),
217 1178
            false
218
        );
219 1178
        $this->limitsHandlingContext = new CachedPersisterContext(
220 1178
            $class,
221 1178
            new Query\ResultSetMapping(),
222 1178
            true
223
        );
224 1178
    }
225
226
    /**
227
     * {@inheritdoc}
228
     */
229 18
    public function getClassMetadata()
230
    {
231 18
        return $this->class;
232
    }
233
234
    /**
235
     * {@inheritdoc}
236
     */
237 11
    public function getResultSetMapping()
238
    {
239 11
        return $this->currentPersisterContext->rsm;
240
    }
241
242
    /**
243
     * {@inheritdoc}
244
     */
245 1060
    public function addInsert($entity)
246
    {
247 1060
        $this->queuedInserts[spl_object_hash($entity)] = $entity;
248 1060
    }
249
250
    /**
251
     * {@inheritdoc}
252
     */
253 95
    public function getInserts()
254
    {
255 95
        return $this->queuedInserts;
256
    }
257
258
    /**
259
     * {@inheritdoc}
260
     */
261 1031
    public function executeInserts()
262
    {
263 1031
        if ( ! $this->queuedInserts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->queuedInserts of type array 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...
264 658
            return [];
265
        }
266
267 971
        $postInsertIds  = [];
268 971
        $idGenerator    = $this->class->idGenerator;
269 971
        $isPostInsertId = $idGenerator->isPostInsertGenerator();
270
271 971
        $stmt       = $this->conn->prepare($this->getInsertSQL());
272 971
        $tableName  = $this->class->getTableName();
273
274 971
        foreach ($this->queuedInserts as $entity) {
275 971
            $insertData = $this->prepareInsertData($entity);
276
277 971
            if (isset($insertData[$tableName])) {
278 944
                $paramIndex = 1;
279
280 944
                foreach ($insertData[$tableName] as $column => $value) {
281 944
                    $stmt->bindValue($paramIndex++, $value, $this->columnTypes[$column]);
282
                }
283
            }
284
285 971
            $stmt->execute();
286
287 970
            if ($isPostInsertId) {
288 878
                $generatedId = $idGenerator->generate($this->em, $entity);
289
                $id = [
290 878
                    $this->class->identifier[0] => $generatedId
291
                ];
292 878
                $postInsertIds[] = [
293 878
                    'generatedId' => $generatedId,
294 878
                    'entity' => $entity,
295
                ];
296
            } else {
297 269
                $id = $this->class->getIdentifierValues($entity);
298
            }
299
300 970
            if ($this->class->isVersioned) {
301 970
                $this->assignDefaultVersionValue($entity, $id);
302
            }
303
        }
304
305 970
        $stmt->closeCursor();
306 970
        $this->queuedInserts = [];
307
308 970
        return $postInsertIds;
309
    }
310
311
    /**
312
     * Retrieves the default version value which was created
313
     * by the preceding INSERT statement and assigns it back in to the
314
     * entities version field.
315
     *
316
     * @param object $entity
317
     * @param array  $id
318
     *
319
     * @return void
320
     */
321 201
    protected function assignDefaultVersionValue($entity, array $id)
322
    {
323 201
        $value = $this->fetchVersionValue($this->class, $id);
324
325 201
        $this->class->setFieldValue($entity, $this->class->versionField, $value);
326 201
    }
327
328
    /**
329
     * Fetches the current version value of a versioned entity.
330
     *
331
     * @param \Doctrine\ORM\Mapping\ClassMetadata $versionedClass
332
     * @param array                               $id
333
     *
334
     * @return mixed
335
     */
336 210
    protected function fetchVersionValue($versionedClass, array $id)
337
    {
338 210
        $versionField = $versionedClass->versionField;
339 210
        $fieldMapping = $versionedClass->fieldMappings[$versionField];
340 210
        $tableName    = $this->quoteStrategy->getTableName($versionedClass, $this->platform);
341 210
        $identifier   = $this->quoteStrategy->getIdentifierColumnNames($versionedClass, $this->platform);
342 210
        $columnName   = $this->quoteStrategy->getColumnName($versionField, $versionedClass, $this->platform);
343
344
        // FIXME: Order with composite keys might not be correct
345 210
        $sql = 'SELECT ' . $columnName
346 210
             . ' FROM '  . $tableName
347 210
             . ' WHERE ' . implode(' = ? AND ', $identifier) . ' = ?';
348
349
350 210
        $flatId = $this->identifierFlattener->flattenIdentifier($versionedClass, $id);
351
352 210
        $value = $this->conn->fetchColumn(
353 210
            $sql,
354 210
            array_values($flatId),
355 210
            0,
356 210
            $this->extractIdentifierTypes($id, $versionedClass)
357
        );
358
359 210
        return Type::getType($fieldMapping['type'])->convertToPHPValue($value, $this->platform);
360
    }
361
362 210
    private function extractIdentifierTypes(array $id, ClassMetadata $versionedClass) : array
363
    {
364 210
        $types = [];
365
366 210
        foreach ($id as $field => $value) {
367 210
            $types = array_merge($types, $this->getTypes($field, $value, $versionedClass));
368
        }
369
370 210
        return $types;
371
    }
372
373
    /**
374
     * {@inheritdoc}
375
     */
376 100
    public function update($entity)
377
    {
378 100
        $tableName  = $this->class->getTableName();
379 100
        $updateData = $this->prepareUpdateData($entity);
380
381 100
        if ( ! isset($updateData[$tableName]) || ! ($data = $updateData[$tableName])) {
382 8
            return;
383
        }
384
385 92
        $isVersioned     = $this->class->isVersioned;
386 92
        $quotedTableName = $this->quoteStrategy->getTableName($this->class, $this->platform);
387
388 92
        $this->updateTable($entity, $quotedTableName, $data, $isVersioned);
389
390 90
        if ($isVersioned) {
391 13
            $id = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
392
393 13
            $this->assignDefaultVersionValue($entity, $id);
394
        }
395 90
    }
396
397
    /**
398
     * Performs an UPDATE statement for an entity on a specific table.
399
     * The UPDATE can optionally be versioned, which requires the entity to have a version field.
400
     *
401
     * @param object  $entity          The entity object being updated.
402
     * @param string  $quotedTableName The quoted name of the table to apply the UPDATE on.
403
     * @param array   $updateData      The map of columns to update (column => value).
404
     * @param boolean $versioned       Whether the UPDATE should be versioned.
405
     *
406
     * @return void
407
     *
408
     * @throws \Doctrine\ORM\ORMException
409
     * @throws \Doctrine\ORM\OptimisticLockException
410
     */
411 123
    protected final function updateTable($entity, $quotedTableName, array $updateData, $versioned = false)
0 ignored issues
show
Coding Style introduced by
As per PSR2, final should precede the visibility keyword.
Loading history...
412
    {
413 123
        $set    = [];
414 123
        $types  = [];
415 123
        $params = [];
416
417 123
        foreach ($updateData as $columnName => $value) {
418 123
            $placeholder = '?';
419 123
            $column      = $columnName;
420
421
            switch (true) {
422 123
                case isset($this->class->fieldNames[$columnName]):
423 63
                    $fieldName  = $this->class->fieldNames[$columnName];
424 63
                    $column     = $this->quoteStrategy->getColumnName($fieldName, $this->class, $this->platform);
425
426 63
                    if (isset($this->class->fieldMappings[$fieldName]['requireSQLConversion'])) {
427 3
                        $type        = Type::getType($this->columnTypes[$columnName]);
428 3
                        $placeholder = $type->convertToDatabaseValueSQL('?', $this->platform);
429
                    }
430
431 63
                    break;
432
433 62
                case isset($this->quotedColumns[$columnName]):
434 62
                    $column = $this->quotedColumns[$columnName];
435
436 62
                    break;
437
            }
438
439 123
            $params[]   = $value;
440 123
            $set[]      = $column . ' = ' . $placeholder;
441 123
            $types[]    = $this->columnTypes[$columnName];
442
        }
443
444 123
        $where      = [];
445 123
        $identifier = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
446
447 123
        foreach ($this->class->identifier as $idField) {
448 123
            if ( ! isset($this->class->associationMappings[$idField])) {
449 118
                $params[]   = $identifier[$idField];
450 118
                $types[]    = $this->class->fieldMappings[$idField]['type'];
451 118
                $where[]    = $this->quoteStrategy->getColumnName($idField, $this->class, $this->platform);
452
453 118
                continue;
454
            }
455
456 6
            $params[] = $identifier[$idField];
457 6
            $where[]  = $this->quoteStrategy->getJoinColumnName(
458 6
                $this->class->associationMappings[$idField]['joinColumns'][0],
459 6
                $this->class,
460 6
                $this->platform
461
            );
462
463 6
            $targetMapping = $this->em->getClassMetadata($this->class->associationMappings[$idField]['targetEntity']);
464 6
            $targetType    = PersisterHelper::getTypeOfField($targetMapping->identifier[0], $targetMapping, $this->em);
465
466 6
            if ($targetType === []) {
467
                throw ORMException::unrecognizedField($targetMapping->identifier[0]);
468
            }
469
470 6
            $types[] = reset($targetType);
471
        }
472
473 123
        if ($versioned) {
474 19
            $versionField       = $this->class->versionField;
475 19
            $versionFieldType   = $this->class->fieldMappings[$versionField]['type'];
476 19
            $versionColumn      = $this->quoteStrategy->getColumnName($versionField, $this->class, $this->platform);
477
478 19
            $where[]    = $versionColumn;
479 19
            $types[]    = $this->class->fieldMappings[$versionField]['type'];
480 19
            $params[]   = $this->class->reflFields[$versionField]->getValue($entity);
481
482
            switch ($versionFieldType) {
483 19
                case Type::SMALLINT:
484 19
                case Type::INTEGER:
485 2
                case Type::BIGINT:
486 17
                    $set[] = $versionColumn . ' = ' . $versionColumn . ' + 1';
487 17
                    break;
488
489 2
                case Type::DATETIME:
490 2
                    $set[] = $versionColumn . ' = CURRENT_TIMESTAMP';
491 2
                    break;
492
            }
493
        }
494
495 123
        $sql = 'UPDATE ' . $quotedTableName
496 123
             . ' SET ' . implode(', ', $set)
497 123
             . ' WHERE ' . implode(' = ? AND ', $where) . ' = ?';
498
499 123
        $result = $this->conn->executeUpdate($sql, $params, $types);
500
501 123
        if ($versioned && ! $result) {
502 4
            throw OptimisticLockException::lockFailed($entity);
503
        }
504 120
    }
505
506
    /**
507
     * @todo Add check for platform if it supports foreign keys/cascading.
508
     *
509
     * @param array $identifier
510
     *
511
     * @return void
512
     */
513 66
    protected function deleteJoinTableRecords($identifier)
514
    {
515 66
        foreach ($this->class->associationMappings as $mapping) {
516 49
            if ($mapping['type'] !== ClassMetadata::MANY_TO_MANY) {
517 48
                continue;
518
            }
519
520
            // @Todo this only covers scenarios with no inheritance or of the same level. Is there something
521
            // like self-referential relationship between different levels of an inheritance hierarchy? I hope not!
522 24
            $selfReferential = ($mapping['targetEntity'] == $mapping['sourceEntity']);
523 24
            $class           = $this->class;
524 24
            $association     = $mapping;
525 24
            $otherColumns    = [];
526 24
            $otherKeys       = [];
527 24
            $keys            = [];
528
529 24
            if ( ! $mapping['isOwningSide']) {
530 6
                $class       = $this->em->getClassMetadata($mapping['targetEntity']);
531 6
                $association = $class->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...
532
            }
533
534 24
            $joinColumns = $mapping['isOwningSide']
535 20
                ? $association['joinTable']['joinColumns']
536 24
                : $association['joinTable']['inverseJoinColumns'];
537
538
539 24
            if ($selfReferential) {
540 1
                $otherColumns = (! $mapping['isOwningSide'])
541
                    ? $association['joinTable']['joinColumns']
542 1
                    : $association['joinTable']['inverseJoinColumns'];
543
            }
544
545 24
            foreach ($joinColumns as $joinColumn) {
546 24
                $keys[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
547
            }
548
549 24
            foreach ($otherColumns as $joinColumn) {
550 1
                $otherKeys[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
551
            }
552
553 24
            if (isset($mapping['isOnDeleteCascade'])) {
554 5
                continue;
555
            }
556
557 20
            $joinTableName = $this->quoteStrategy->getJoinTableName($association, $this->class, $this->platform);
558
559 20
            $this->conn->delete($joinTableName, array_combine($keys, $identifier));
560
561 20
            if ($selfReferential) {
562 20
                $this->conn->delete($joinTableName, array_combine($otherKeys, $identifier));
563
            }
564
        }
565 66
    }
566
567
    /**
568
     * {@inheritdoc}
569
     */
570 62
    public function delete($entity)
571
    {
572 62
        $self       = $this;
573 62
        $class      = $this->class;
574 62
        $identifier = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
575 62
        $tableName  = $this->quoteStrategy->getTableName($class, $this->platform);
576 62
        $idColumns  = $this->quoteStrategy->getIdentifierColumnNames($class, $this->platform);
577 62
        $id         = array_combine($idColumns, $identifier);
578 62
        $types      = array_map(function ($identifier) use ($class, $self) {
579 62
            if (isset($class->fieldMappings[$identifier])) {
580 60
                return $class->fieldMappings[$identifier]['type'];
581
            }
582
583 5
            $targetMapping = $self->em->getClassMetadata($class->associationMappings[$identifier]['targetEntity']);
584
585 5
            if (isset($targetMapping->fieldMappings[$targetMapping->identifier[0]])) {
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...
Bug introduced by
Accessing fieldMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
586 4
                return $targetMapping->fieldMappings[$targetMapping->identifier[0]]['type'];
587
            }
588
589 1
            if (isset($targetMapping->associationMappings[$targetMapping->identifier[0]])) {
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...
590 1
                return $targetMapping->associationMappings[$targetMapping->identifier[0]]['type'];
591
            }
592
593
            throw ORMException::unrecognizedField($targetMapping->identifier[0]);
594 62
        }, $class->identifier);
595
596 62
        $this->deleteJoinTableRecords($identifier);
597
598 62
        return (bool) $this->conn->delete($tableName, $id, $types);
599
    }
600
601
    /**
602
     * Prepares the changeset of an entity for database insertion (UPDATE).
603
     *
604
     * The changeset is obtained from the currently running UnitOfWork.
605
     *
606
     * During this preparation the array that is passed as the second parameter is filled with
607
     * <columnName> => <value> pairs, grouped by table name.
608
     *
609
     * Example:
610
     * <code>
611
     * array(
612
     *    'foo_table' => array('column1' => 'value1', 'column2' => 'value2', ...),
613
     *    'bar_table' => array('columnX' => 'valueX', 'columnY' => 'valueY', ...),
614
     *    ...
615
     * )
616
     * </code>
617
     *
618
     * @param object $entity The entity for which to prepare the data.
619
     *
620
     * @return array The prepared data.
621
     */
622 1065
    protected function prepareUpdateData($entity)
623
    {
624 1065
        $versionField = null;
625 1065
        $result       = [];
626 1065
        $uow          = $this->em->getUnitOfWork();
627
628 1065
        if (($versioned = $this->class->isVersioned) != false) {
0 ignored issues
show
Unused Code introduced by
The assignment to $versioned is dead and can be removed.
Loading history...
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison !== instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
629 214
            $versionField = $this->class->versionField;
630
        }
631
632 1065
        foreach ($uow->getEntityChangeSet($entity) as $field => $change) {
633 1029
            if (isset($versionField) && $versionField == $field) {
634
                continue;
635
            }
636
637 1029
            if (isset($this->class->embeddedClasses[$field])) {
638 9
                continue;
639
            }
640
641 1029
            $newVal = $change[1];
642
643 1029
            if ( ! isset($this->class->associationMappings[$field])) {
644 990
                $fieldMapping = $this->class->fieldMappings[$field];
645 990
                $columnName   = $fieldMapping['columnName'];
646
647 990
                $this->columnTypes[$columnName] = $fieldMapping['type'];
648
649 990
                $result[$this->getOwningTable($field)][$columnName] = $newVal;
650
651 990
                continue;
652
            }
653
654 876
            $assoc = $this->class->associationMappings[$field];
655
656
            // Only owning side of x-1 associations can have a FK column.
657 876
            if ( ! $assoc['isOwningSide'] || ! ($assoc['type'] & ClassMetadata::TO_ONE)) {
658 8
                continue;
659
            }
660
661 876
            if ($newVal !== null) {
662 638
                $oid = spl_object_hash($newVal);
663
664 638
                if (isset($this->queuedInserts[$oid]) || $uow->isScheduledForInsert($newVal)) {
665
                    // The associated entity $newVal is not yet persisted, so we must
666
                    // set $newVal = null, in order to insert a null value and schedule an
667
                    // extra update on the UnitOfWork.
668 43
                    $uow->scheduleExtraUpdate($entity, [$field => [null, $newVal]]);
669
670 43
                    $newVal = null;
671
                }
672
            }
673
674 876
            $newValId = null;
675
676 876
            if ($newVal !== null) {
677 638
                $newValId = $uow->getEntityIdentifier($newVal);
678
            }
679
680 876
            $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
681 876
            $owningTable = $this->getOwningTable($field);
682
683 876
            foreach ($assoc['joinColumns'] as $joinColumn) {
684 876
                $sourceColumn = $joinColumn['name'];
685 876
                $targetColumn = $joinColumn['referencedColumnName'];
686 876
                $quotedColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
687
688 876
                $this->quotedColumns[$sourceColumn]  = $quotedColumn;
689 876
                $this->columnTypes[$sourceColumn]    = PersisterHelper::getTypeOfColumn($targetColumn, $targetClass, $this->em);
690 876
                $result[$owningTable][$sourceColumn] = $newValId
691 638
                    ? $newValId[$targetClass->getFieldForColumn($targetColumn)]
0 ignored issues
show
Bug introduced by
The method getFieldForColumn() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. It seems like you code against a sub-type of Doctrine\Common\Persistence\Mapping\ClassMetadata such as Doctrine\ORM\Mapping\ClassMetadataInfo. ( Ignorable by Annotation )

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

691
                    ? $newValId[$targetClass->/** @scrutinizer ignore-call */ getFieldForColumn($targetColumn)]
Loading history...
692 876
                    : null;
693
            }
694
        }
695
696 1065
        return $result;
697
    }
698
699
    /**
700
     * Prepares the data changeset of a managed entity for database insertion (initial INSERT).
701
     * The changeset of the entity is obtained from the currently running UnitOfWork.
702
     *
703
     * The default insert data preparation is the same as for updates.
704
     *
705
     * @param object $entity The entity for which to prepare the data.
706
     *
707
     * @return array The prepared data for the tables to update.
708
     *
709
     * @see prepareUpdateData
710
     */
711 1060
    protected function prepareInsertData($entity)
712
    {
713 1060
        return $this->prepareUpdateData($entity);
714
    }
715
716
    /**
717
     * {@inheritdoc}
718
     */
719 943
    public function getOwningTable($fieldName)
720
    {
721 943
        return $this->class->getTableName();
722
    }
723
724
    /**
725
     * {@inheritdoc}
726
     */
727 517
    public function load(array $criteria, $entity = null, $assoc = null, array $hints = [], $lockMode = null, $limit = null, array $orderBy = null)
728
    {
729 517
        $this->switchPersisterContext(null, $limit);
730
731 517
        $sql = $this->getSelectSQL($criteria, $assoc, $lockMode, $limit, null, $orderBy);
732 516
        list($params, $types) = $this->expandParameters($criteria);
733 516
        $stmt = $this->conn->executeQuery($sql, $params, $types);
734
735 516
        if ($entity !== null) {
736 72
            $hints[Query::HINT_REFRESH]         = true;
737 72
            $hints[Query::HINT_REFRESH_ENTITY]  = $entity;
738
        }
739
740 516
        $hydrator = $this->em->newHydrator($this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
741 516
        $entities = $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, $hints);
742
743 516
        return $entities ? $entities[0] : null;
744
    }
745
746
    /**
747
     * {@inheritdoc}
748
     */
749 440
    public function loadById(array $identifier, $entity = null)
750
    {
751 440
        return $this->load($identifier, $entity);
752
    }
753
754
    /**
755
     * {@inheritdoc}
756
     */
757 97
    public function loadOneToOneEntity(array $assoc, $sourceEntity, array $identifier = [])
758
    {
759 97
        if (($foundEntity = $this->em->getUnitOfWork()->tryGetById($identifier, $assoc['targetEntity'])) != false) {
760
            return $foundEntity;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $foundEntity also could return the type true which is incompatible with the return type mandated by Doctrine\ORM\Persisters\...r::loadOneToOneEntity() of object.
Loading history...
761
        }
762
763 97
        $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
764
765 97
        if ($assoc['isOwningSide']) {
766 32
            $isInverseSingleValued = $assoc['inversedBy'] && ! $targetClass->isCollectionValuedAssociation($assoc['inversedBy']);
767
768
            // Mark inverse side as fetched in the hints, otherwise the UoW would
769
            // try to load it in a separate query (remember: to-one inverse sides can not be lazy).
770 32
            $hints = [];
771
772 32
            if ($isInverseSingleValued) {
773
                $hints['fetched']["r"][$assoc['inversedBy']] = true;
774
            }
775
776
            /* cascade read-only status
0 ignored issues
show
Unused Code Comprehensibility introduced by
49% 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...
777
            if ($this->em->getUnitOfWork()->isReadOnly($sourceEntity)) {
778
                $hints[Query::HINT_READ_ONLY] = true;
779
            }
780
            */
781
782 32
            $targetEntity = $this->load($identifier, null, $assoc, $hints);
783
784
            // Complete bidirectional association, if necessary
785 32
            if ($targetEntity !== null && $isInverseSingleValued) {
786
                $targetClass->reflFields[$assoc['inversedBy']]->setValue($targetEntity, $sourceEntity);
0 ignored issues
show
Bug introduced by
Accessing reflFields on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
787
            }
788
789 32
            return $targetEntity;
790
        }
791
792 65
        $sourceClass = $this->em->getClassMetadata($assoc['sourceEntity']);
793 65
        $owningAssoc = $targetClass->getAssociationMapping($assoc['mappedBy']);
0 ignored issues
show
Bug introduced by
The method getAssociationMapping() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. Did you maybe mean getAssociationNames()? ( Ignorable by Annotation )

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

793
        /** @scrutinizer ignore-call */ 
794
        $owningAssoc = $targetClass->getAssociationMapping($assoc['mappedBy']);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
794
795 65
        $computedIdentifier = [];
796
797
        // TRICKY: since the association is specular source and target are flipped
798 65
        foreach ($owningAssoc['targetToSourceKeyColumns'] as $sourceKeyColumn => $targetKeyColumn) {
799 65
            if ( ! isset($sourceClass->fieldNames[$sourceKeyColumn])) {
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...
800
                throw MappingException::joinColumnMustPointToMappedField(
801
                    $sourceClass->name, $sourceKeyColumn
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...
802
                );
803
            }
804
805 65
            $computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
806 65
                $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
807
        }
808
809 65
        $targetEntity = $this->load($computedIdentifier, null, $assoc);
810
811 65
        if ($targetEntity !== null) {
812 17
            $targetClass->setFieldValue($targetEntity, $assoc['mappedBy'], $sourceEntity);
0 ignored issues
show
Bug introduced by
The method setFieldValue() does not exist on Doctrine\Common\Persistence\Mapping\ClassMetadata. It seems like you code against a sub-type of Doctrine\Common\Persistence\Mapping\ClassMetadata such as Doctrine\ORM\Mapping\ClassMetadataInfo. ( Ignorable by Annotation )

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

812
            $targetClass->/** @scrutinizer ignore-call */ 
813
                          setFieldValue($targetEntity, $assoc['mappedBy'], $sourceEntity);
Loading history...
813
        }
814
815 65
        return $targetEntity;
816
    }
817
818
    /**
819
     * {@inheritdoc}
820
     */
821 17
    public function refresh(array $id, $entity, $lockMode = null)
822
    {
823 17
        $sql = $this->getSelectSQL($id, null, $lockMode);
824 17
        list($params, $types) = $this->expandParameters($id);
825 17
        $stmt = $this->conn->executeQuery($sql, $params, $types);
826
827 17
        $hydrator = $this->em->newHydrator(Query::HYDRATE_OBJECT);
828 17
        $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [Query::HINT_REFRESH => true]);
829 17
    }
830
831
    /**
832
     * {@inheritDoc}
833
     */
834 46
    public function count($criteria = [])
835
    {
836 46
        $sql = $this->getCountSQL($criteria);
837
838 46
        list($params, $types) = ($criteria instanceof Criteria)
839 43
            ? $this->expandCriteriaParameters($criteria)
840 46
            : $this->expandParameters($criteria);
841
842 46
        return (int) $this->conn->executeQuery($sql, $params, $types)->fetchColumn();
843
    }
844
845
    /**
846
     * {@inheritdoc}
847
     */
848 9
    public function loadCriteria(Criteria $criteria)
849
    {
850 9
        $orderBy = $criteria->getOrderings();
851 9
        $limit   = $criteria->getMaxResults();
852 9
        $offset  = $criteria->getFirstResult();
853 9
        $query   = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy);
854
855 7
        list($params, $types) = $this->expandCriteriaParameters($criteria);
856
857 7
        $stmt       = $this->conn->executeQuery($query, $params, $types);
858 7
        $hydrator   = $this->em->newHydrator(($this->currentPersisterContext->selectJoinSql) ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
859
860 7
        return $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [UnitOfWork::HINT_DEFEREAGERLOAD => true]
861
        );
862
    }
863
864
    /**
865
     * {@inheritdoc}
866
     */
867 56
    public function expandCriteriaParameters(Criteria $criteria)
868
    {
869 56
        $expression = $criteria->getWhereExpression();
870 56
        $sqlParams  = [];
871 56
        $sqlTypes   = [];
872
873 56
        if ($expression === null) {
874 2
            return [$sqlParams, $sqlTypes];
875
        }
876
877 55
        $valueVisitor = new SqlValueVisitor();
878
879 55
        $valueVisitor->dispatch($expression);
880
881 55
        list($params, $types) = $valueVisitor->getParamsAndTypes();
882
883 55
        foreach ($params as $param) {
884 51
            $sqlParams = array_merge($sqlParams, $this->getValues($param));
885
        }
886
887 55
        foreach ($types as $type) {
888 51
            list ($field, $value) = $type;
889 51
            $sqlTypes = array_merge($sqlTypes, $this->getTypes($field, $value, $this->class));
890
        }
891
892 55
        return [$sqlParams, $sqlTypes];
893
    }
894
895
    /**
896
     * {@inheritdoc}
897
     */
898 71
    public function loadAll(array $criteria = [], array $orderBy = null, $limit = null, $offset = null)
899
    {
900 71
        $this->switchPersisterContext($offset, $limit);
901
902 71
        $sql = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy);
903 67
        list($params, $types) = $this->expandParameters($criteria);
904 67
        $stmt = $this->conn->executeQuery($sql, $params, $types);
905
906 67
        $hydrator = $this->em->newHydrator(($this->currentPersisterContext->selectJoinSql) ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
907
908 67
        return $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [UnitOfWork::HINT_DEFEREAGERLOAD => true]
909
        );
910
    }
911
912
    /**
913
     * {@inheritdoc}
914
     */
915 8
    public function getManyToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null)
916
    {
917 8
        $this->switchPersisterContext($offset, $limit);
918
919 8
        $stmt = $this->getManyToManyStatement($assoc, $sourceEntity, $offset, $limit);
920
921 8
        return $this->loadArrayFromStatement($assoc, $stmt);
922
    }
923
924
    /**
925
     * Loads an array of entities from a given DBAL statement.
926
     *
927
     * @param array                    $assoc
928
     * @param \Doctrine\DBAL\Statement $stmt
929
     *
930
     * @return array
931
     */
932 13
    private function loadArrayFromStatement($assoc, $stmt)
933
    {
934 13
        $rsm    = $this->currentPersisterContext->rsm;
935 13
        $hints  = [UnitOfWork::HINT_DEFEREAGERLOAD => true];
936
937 13
        if (isset($assoc['indexBy'])) {
938 7
            $rsm = clone ($this->currentPersisterContext->rsm); // this is necessary because the "default rsm" should be changed.
939 7
            $rsm->addIndexBy('r', $assoc['indexBy']);
940
        }
941
942 13
        return $this->em->newHydrator(Query::HYDRATE_OBJECT)->hydrateAll($stmt, $rsm, $hints);
943
    }
944
945
    /**
946
     * Hydrates a collection from a given DBAL statement.
947
     *
948
     * @param array                    $assoc
949
     * @param \Doctrine\DBAL\Statement $stmt
950
     * @param PersistentCollection     $coll
951
     *
952
     * @return array
953
     */
954 145
    private function loadCollectionFromStatement($assoc, $stmt, $coll)
955
    {
956 145
        $rsm   = $this->currentPersisterContext->rsm;
957
        $hints = [
958 145
            UnitOfWork::HINT_DEFEREAGERLOAD => true,
959 145
            'collection' => $coll
960
        ];
961
962 145
        if (isset($assoc['indexBy'])) {
963 10
            $rsm = clone ($this->currentPersisterContext->rsm); // this is necessary because the "default rsm" should be changed.
964 10
            $rsm->addIndexBy('r', $assoc['indexBy']);
965
        }
966
967 145
        return $this->em->newHydrator(Query::HYDRATE_OBJECT)->hydrateAll($stmt, $rsm, $hints);
968
    }
969
970
    /**
971
     * {@inheritdoc}
972
     */
973 82
    public function loadManyToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll)
974
    {
975 82
        $stmt = $this->getManyToManyStatement($assoc, $sourceEntity);
976
977 82
        return $this->loadCollectionFromStatement($assoc, $stmt, $coll);
978
    }
979
980
    /**
981
     * @param array    $assoc
982
     * @param object   $sourceEntity
983
     * @param int|null $offset
984
     * @param int|null $limit
985
     *
986
     * @return \Doctrine\DBAL\Driver\Statement
987
     *
988
     * @throws \Doctrine\ORM\Mapping\MappingException
989
     */
990 89
    private function getManyToManyStatement(array $assoc, $sourceEntity, $offset = null, $limit = null)
991
    {
992 89
        $this->switchPersisterContext($offset, $limit);
993
994 89
        $sourceClass    = $this->em->getClassMetadata($assoc['sourceEntity']);
995 89
        $class          = $sourceClass;
996 89
        $association    = $assoc;
997 89
        $criteria       = [];
998 89
        $parameters     = [];
999
1000 89
        if ( ! $assoc['isOwningSide']) {
1001 12
            $class       = $this->em->getClassMetadata($assoc['targetEntity']);
1002 12
            $association = $class->associationMappings[$assoc['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...
1003
        }
1004
1005 89
        $joinColumns = $assoc['isOwningSide']
1006 82
            ? $association['joinTable']['joinColumns']
1007 89
            : $association['joinTable']['inverseJoinColumns'];
1008
1009 89
        $quotedJoinTable = $this->quoteStrategy->getJoinTableName($association, $class, $this->platform);
1010
1011 89
        foreach ($joinColumns as $joinColumn) {
1012 89
            $sourceKeyColumn    = $joinColumn['referencedColumnName'];
1013 89
            $quotedKeyColumn    = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
1014
1015
            switch (true) {
1016 89
                case $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...
1017 4
                    $field = $sourceClass->getFieldForColumn($sourceKeyColumn);
1018 4
                    $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
0 ignored issues
show
Bug introduced by
Accessing reflFields on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1019
1020 4
                    if (isset($sourceClass->associationMappings[$field])) {
1021 4
                        $value = $this->em->getUnitOfWork()->getEntityIdentifier($value);
1022 4
                        $value = $value[$this->em->getClassMetadata($sourceClass->associationMappings[$field]['targetEntity'])->identifier[0]];
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...
1023
                    }
1024
1025 4
                    break;
1026
1027 87
                case isset($sourceClass->fieldNames[$sourceKeyColumn]):
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...
1028 87
                    $field = $sourceClass->fieldNames[$sourceKeyColumn];
1029 87
                    $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
1030
1031 87
                    break;
1032
1033
                default:
1034
                    throw MappingException::joinColumnMustPointToMappedField(
1035
                        $sourceClass->name, $sourceKeyColumn
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...
1036
                    );
1037
            }
1038
1039 89
            $criteria[$quotedJoinTable . '.' . $quotedKeyColumn] = $value;
1040 89
            $parameters[] = [
1041 89
                'value' => $value,
1042 89
                'field' => $field,
1043 89
                'class' => $sourceClass,
1044
            ];
1045
        }
1046
1047 89
        $sql = $this->getSelectSQL($criteria, $assoc, null, $limit, $offset);
1048 89
        list($params, $types) = $this->expandToManyParameters($parameters);
1049
1050 89
        return $this->conn->executeQuery($sql, $params, $types);
1051
    }
1052
1053
    /**
1054
     * {@inheritdoc}
1055
     */
1056 569
    public function getSelectSQL($criteria, $assoc = null, $lockMode = null, $limit = null, $offset = null, array $orderBy = null)
1057
    {
1058 569
        $this->switchPersisterContext($offset, $limit);
1059
1060 569
        $lockSql    = '';
1061 569
        $joinSql    = '';
1062 569
        $orderBySql = '';
1063
1064 569
        if ($assoc != null && $assoc['type'] == ClassMetadata::MANY_TO_MANY) {
1065 90
            $joinSql = $this->getSelectManyToManyJoinSQL($assoc);
1066
        }
1067
1068 569
        if (isset($assoc['orderBy'])) {
1069 5
            $orderBy = $assoc['orderBy'];
1070
        }
1071
1072 569
        if ($orderBy) {
1073 11
            $orderBySql = $this->getOrderBySQL($orderBy, $this->getSQLTableAlias($this->class->name));
1074
        }
1075
1076 567
        $conditionSql = ($criteria instanceof Criteria)
1077 9
            ? $this->getSelectConditionCriteriaSQL($criteria)
1078 565
            : $this->getSelectConditionSQL($criteria, $assoc);
1079
1080
        switch ($lockMode) {
1081 562
            case LockMode::PESSIMISTIC_READ:
1082
                $lockSql = ' ' . $this->platform->getReadLockSQL();
1083
                break;
1084
1085 562
            case LockMode::PESSIMISTIC_WRITE:
1086
                $lockSql = ' ' . $this->platform->getWriteLockSQL();
1087
                break;
1088
        }
1089
1090 562
        $columnList = $this->getSelectColumnsSQL();
1091 562
        $tableAlias = $this->getSQLTableAlias($this->class->name);
1092 562
        $filterSql  = $this->generateFilterConditionSQL($this->class, $tableAlias);
1093 562
        $tableName  = $this->quoteStrategy->getTableName($this->class, $this->platform);
1094
1095 562
        if ('' !== $filterSql) {
1096 12
            $conditionSql = $conditionSql
1097 11
                ? $conditionSql . ' AND ' . $filterSql
1098 12
                : $filterSql;
1099
        }
1100
1101 562
        $select = 'SELECT ' . $columnList;
1102 562
        $from   = ' FROM ' . $tableName . ' '. $tableAlias;
1103 562
        $join   = $this->currentPersisterContext->selectJoinSql . $joinSql;
1104 562
        $where  = ($conditionSql ? ' WHERE ' . $conditionSql : '');
1105 562
        $lock   = $this->platform->appendLockHint($from, $lockMode);
1106
        $query  = $select
1107 562
            . $lock
1108 562
            . $join
1109 562
            . $where
1110 562
            . $orderBySql;
1111
1112 562
        return $this->platform->modifyLimitQuery($query, $limit, $offset) . $lockSql;
1113
    }
1114
1115
    /**
1116
     * {@inheritDoc}
1117
     */
1118 41
    public function getCountSQL($criteria = [])
1119
    {
1120 41
        $tableName  = $this->quoteStrategy->getTableName($this->class, $this->platform);
1121 41
        $tableAlias = $this->getSQLTableAlias($this->class->name);
1122
1123 41
        $conditionSql = ($criteria instanceof Criteria)
1124 38
            ? $this->getSelectConditionCriteriaSQL($criteria)
1125 41
            : $this->getSelectConditionSQL($criteria);
1126
1127 41
        $filterSql = $this->generateFilterConditionSQL($this->class, $tableAlias);
1128
1129 41
        if ('' !== $filterSql) {
1130 2
            $conditionSql = $conditionSql
1131 2
                ? $conditionSql . ' AND ' . $filterSql
1132 2
                : $filterSql;
1133
        }
1134
1135
        $sql = 'SELECT COUNT(*) '
1136 41
            . 'FROM ' . $tableName . ' ' . $tableAlias
1137 41
            . (empty($conditionSql) ? '' : ' WHERE ' . $conditionSql);
1138
1139 41
        return $sql;
1140
    }
1141
1142
    /**
1143
     * Gets the ORDER BY SQL snippet for ordered collections.
1144
     *
1145
     * @param array  $orderBy
1146
     * @param string $baseTableAlias
1147
     *
1148
     * @return string
1149
     *
1150
     * @throws \Doctrine\ORM\ORMException
1151
     */
1152 12
    protected final function getOrderBySQL(array $orderBy, $baseTableAlias)
0 ignored issues
show
Coding Style introduced by
As per PSR2, final should precede the visibility keyword.
Loading history...
1153
    {
1154 12
        $orderByList = [];
1155
1156 12
        foreach ($orderBy as $fieldName => $orientation) {
1157
1158 12
            $orientation = strtoupper(trim($orientation));
1159
1160 12
            if ($orientation != 'ASC' && $orientation != 'DESC') {
1161 1
                throw ORMException::invalidOrientation($this->class->name, $fieldName);
1162
            }
1163
1164 11
            if (isset($this->class->fieldMappings[$fieldName])) {
1165 9
                $tableAlias = isset($this->class->fieldMappings[$fieldName]['inherited'])
1166
                    ? $this->getSQLTableAlias($this->class->fieldMappings[$fieldName]['inherited'])
1167 9
                    : $baseTableAlias;
1168
1169 9
                $columnName    = $this->quoteStrategy->getColumnName($fieldName, $this->class, $this->platform);
1170 9
                $orderByList[] = $tableAlias . '.' . $columnName . ' ' . $orientation;
1171
1172 9
                continue;
1173
            }
1174
1175 2
            if (isset($this->class->associationMappings[$fieldName])) {
1176
1177 2
                if ( ! $this->class->associationMappings[$fieldName]['isOwningSide']) {
1178 1
                    throw ORMException::invalidFindByInverseAssociation($this->class->name, $fieldName);
1179
                }
1180
1181 1
                $tableAlias = isset($this->class->associationMappings[$fieldName]['inherited'])
1182
                    ? $this->getSQLTableAlias($this->class->associationMappings[$fieldName]['inherited'])
1183 1
                    : $baseTableAlias;
1184
1185 1
                foreach ($this->class->associationMappings[$fieldName]['joinColumns'] as $joinColumn) {
1186 1
                    $columnName    = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
1187 1
                    $orderByList[] = $tableAlias . '.' . $columnName . ' ' . $orientation;
1188
                }
1189
1190 1
                continue;
1191
            }
1192
1193
            throw ORMException::unrecognizedField($fieldName);
1194
        }
1195
1196 10
        return ' ORDER BY ' . implode(', ', $orderByList);
1197
    }
1198
1199
    /**
1200
     * Gets the SQL fragment with the list of columns to select when querying for
1201
     * an entity in this persister.
1202
     *
1203
     * Subclasses should override this method to alter or change the select column
1204
     * list SQL fragment. Note that in the implementation of BasicEntityPersister
1205
     * the resulting SQL fragment is generated only once and cached in {@link selectColumnListSql}.
1206
     * Subclasses may or may not do the same.
1207
     *
1208
     * @return string The SQL fragment.
1209
     */
1210 563
    protected function getSelectColumnsSQL()
1211
    {
1212 563
        if ($this->currentPersisterContext->selectColumnListSql !== null) {
1213 147
            return $this->currentPersisterContext->selectColumnListSql;
1214
        }
1215
1216 563
        $columnList = [];
1217 563
        $this->currentPersisterContext->rsm->addEntityResult($this->class->name, 'r'); // r for root
1218
1219
        // Add regular columns to select list
1220 563
        foreach ($this->class->fieldNames as $field) {
1221 561
            $columnList[] = $this->getSelectColumnSQL($field, $this->class);
1222
        }
1223
1224 563
        $this->currentPersisterContext->selectJoinSql    = '';
1225 563
        $eagerAliasCounter      = 0;
1226
1227 563
        foreach ($this->class->associationMappings as $assocField => $assoc) {
1228 500
            $assocColumnSQL = $this->getSelectColumnAssociationSQL($assocField, $assoc, $this->class);
1229
1230 500
            if ($assocColumnSQL) {
1231 422
                $columnList[] = $assocColumnSQL;
1232
            }
1233
1234 500
            $isAssocToOneInverseSide = $assoc['type'] & ClassMetadata::TO_ONE && ! $assoc['isOwningSide'];
1235 500
            $isAssocFromOneEager     = $assoc['type'] !== ClassMetadata::MANY_TO_MANY && $assoc['fetch'] === ClassMetadata::FETCH_EAGER;
1236
1237 500
            if ( ! ($isAssocFromOneEager || $isAssocToOneInverseSide)) {
1238 478
                continue;
1239
            }
1240
1241 192
            if ((($assoc['type'] & ClassMetadata::TO_MANY) > 0) && $this->currentPersisterContext->handlesLimits) {
1242 3
                continue;
1243
            }
1244
1245 189
            $eagerEntity = $this->em->getClassMetadata($assoc['targetEntity']);
1246
1247 189
            if ($eagerEntity->inheritanceType != ClassMetadata::INHERITANCE_TYPE_NONE) {
0 ignored issues
show
Bug introduced by
Accessing inheritanceType on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1248 5
                continue; // now this is why you shouldn't use inheritance
1249
            }
1250
1251 184
            $assocAlias = 'e' . ($eagerAliasCounter++);
1252 184
            $this->currentPersisterContext->rsm->addJoinedEntityResult($assoc['targetEntity'], $assocAlias, 'r', $assocField);
1253
1254 184
            foreach ($eagerEntity->fieldNames as $field) {
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...
1255 181
                $columnList[] = $this->getSelectColumnSQL($field, $eagerEntity, $assocAlias);
1256
            }
1257
1258 184
            foreach ($eagerEntity->associationMappings as $eagerAssocField => $eagerAssoc) {
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...
1259 181
                $eagerAssocColumnSQL = $this->getSelectColumnAssociationSQL(
1260 181
                    $eagerAssocField, $eagerAssoc, $eagerEntity, $assocAlias
1261
                );
1262
1263 181
                if ($eagerAssocColumnSQL) {
1264 181
                    $columnList[] = $eagerAssocColumnSQL;
1265
                }
1266
            }
1267
1268 184
            $association    = $assoc;
1269 184
            $joinCondition  = [];
1270
1271 184
            if (isset($assoc['indexBy'])) {
1272 1
                $this->currentPersisterContext->rsm->addIndexBy($assocAlias, $assoc['indexBy']);
1273
            }
1274
1275 184
            if ( ! $assoc['isOwningSide']) {
1276 177
                $eagerEntity = $this->em->getClassMetadata($assoc['targetEntity']);
1277 177
                $association = $eagerEntity->getAssociationMapping($assoc['mappedBy']);
1278
            }
1279
1280 184
            $joinTableAlias = $this->getSQLTableAlias($eagerEntity->name, $assocAlias);
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...
1281 184
            $joinTableName  = $this->quoteStrategy->getTableName($eagerEntity, $this->platform);
1282
1283 184
            if ($assoc['isOwningSide']) {
1284 13
                $tableAlias           = $this->getSQLTableAlias($association['targetEntity'], $assocAlias);
1285 13
                $this->currentPersisterContext->selectJoinSql .= ' ' . $this->getJoinSQLForJoinColumns($association['joinColumns']);
1286
1287 13
                foreach ($association['joinColumns'] as $joinColumn) {
1288 13
                    $sourceCol       = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
1289 13
                    $targetCol       = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform);
1290 13
                    $joinCondition[] = $this->getSQLTableAlias($association['sourceEntity'])
1291 13
                                        . '.' . $sourceCol . ' = ' . $tableAlias . '.' . $targetCol;
1292
                }
1293
1294
                // Add filter SQL
1295 13
                if ($filterSql = $this->generateFilterConditionSQL($eagerEntity, $tableAlias)) {
1296 13
                    $joinCondition[] = $filterSql;
1297
                }
1298
1299
            } else {
1300
1301 177
                $this->currentPersisterContext->selectJoinSql .= ' LEFT JOIN';
1302
1303 177
                foreach ($association['joinColumns'] as $joinColumn) {
1304 177
                    $sourceCol       = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
1305 177
                    $targetCol       = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform);
1306
1307 177
                    $joinCondition[] = $this->getSQLTableAlias($association['sourceEntity'], $assocAlias) . '.' . $sourceCol . ' = '
1308 177
                        . $this->getSQLTableAlias($association['targetEntity']) . '.' . $targetCol;
1309
                }
1310
            }
1311
1312 184
            $this->currentPersisterContext->selectJoinSql .= ' ' . $joinTableName . ' ' . $joinTableAlias . ' ON ';
1313 184
            $this->currentPersisterContext->selectJoinSql .= implode(' AND ', $joinCondition);
1314
        }
1315
1316 563
        $this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList);
1317
1318 563
        return $this->currentPersisterContext->selectColumnListSql;
1319
    }
1320
1321
    /**
1322
     * Gets the SQL join fragment used when selecting entities from an association.
1323
     *
1324
     * @param string        $field
1325
     * @param array         $assoc
1326
     * @param ClassMetadata $class
1327
     * @param string        $alias
1328
     *
1329
     * @return string
1330
     */
1331 500
    protected function getSelectColumnAssociationSQL($field, $assoc, ClassMetadata $class, $alias = 'r')
0 ignored issues
show
Unused Code introduced by
The parameter $field is not used and could be removed. ( Ignorable by Annotation )

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

1331
    protected function getSelectColumnAssociationSQL(/** @scrutinizer ignore-unused */ $field, $assoc, ClassMetadata $class, $alias = 'r')

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1332
    {
1333 500
        if ( ! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) ) {
1334 400
            return '';
1335
        }
1336
1337 439
        $columnList    = [];
1338 439
        $targetClass   = $this->em->getClassMetadata($assoc['targetEntity']);
1339 439
        $isIdentifier  = isset($assoc['id']) && $assoc['id'] === true;
1340 439
        $sqlTableAlias = $this->getSQLTableAlias($class->name, ($alias == 'r' ? '' : $alias));
1341
1342 439
        foreach ($assoc['joinColumns'] as $joinColumn) {
1343 439
            $quotedColumn     = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
1344 439
            $resultColumnName = $this->getSQLColumnAlias($joinColumn['name']);
1345 439
            $type             = PersisterHelper::getTypeOfColumn($joinColumn['referencedColumnName'], $targetClass, $this->em);
1346
1347 439
            $this->currentPersisterContext->rsm->addMetaResult($alias, $resultColumnName, $joinColumn['name'], $isIdentifier, $type);
1348
1349 439
            $columnList[] = sprintf('%s.%s AS %s', $sqlTableAlias, $quotedColumn, $resultColumnName);
1350
        }
1351
1352 439
        return implode(', ', $columnList);
1353
    }
1354
1355
    /**
1356
     * Gets the SQL join fragment used when selecting entities from a
1357
     * many-to-many association.
1358
     *
1359
     * @param array $manyToMany
1360
     *
1361
     * @return string
1362
     */
1363 92
    protected function getSelectManyToManyJoinSQL(array $manyToMany)
1364
    {
1365 92
        $conditions         = [];
1366 92
        $association        = $manyToMany;
1367 92
        $sourceTableAlias   = $this->getSQLTableAlias($this->class->name);
1368
1369 92
        if ( ! $manyToMany['isOwningSide']) {
1370 13
            $targetEntity   = $this->em->getClassMetadata($manyToMany['targetEntity']);
1371 13
            $association    = $targetEntity->associationMappings[$manyToMany['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...
1372
        }
1373
1374 92
        $joinTableName  = $this->quoteStrategy->getJoinTableName($association, $this->class, $this->platform);
1375 92
        $joinColumns    = ($manyToMany['isOwningSide'])
1376 84
            ? $association['joinTable']['inverseJoinColumns']
1377 92
            : $association['joinTable']['joinColumns'];
1378
1379 92
        foreach ($joinColumns as $joinColumn) {
1380 92
            $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
1381 92
            $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform);
1382 92
            $conditions[]       = $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableName . '.' . $quotedSourceColumn;
1383
        }
1384
1385 92
        return ' INNER JOIN ' . $joinTableName . ' ON ' . implode(' AND ', $conditions);
1386
    }
1387
1388
    /**
1389
     * {@inheritdoc}
1390
     */
1391 1061
    public function getInsertSQL()
1392
    {
1393 1061
        if ($this->insertSql !== null) {
1394 89
            return $this->insertSql;
1395
        }
1396
1397 1061
        $columns   = $this->getInsertColumnList();
1398 1061
        $tableName = $this->quoteStrategy->getTableName($this->class, $this->platform);
1399
1400 1061
        if (empty($columns)) {
1401 115
            $identityColumn  = $this->quoteStrategy->getColumnName($this->class->identifier[0], $this->class, $this->platform);
1402 115
            $this->insertSql = $this->platform->getEmptyIdentityInsertSQL($tableName, $identityColumn);
1403
1404 115
            return $this->insertSql;
1405
        }
1406
1407 1036
        $values  = [];
1408 1036
        $columns = array_unique($columns);
1409
1410 1036
        foreach ($columns as $column) {
1411 1036
            $placeholder = '?';
1412
1413 1036
            if (isset($this->class->fieldNames[$column])
1414 1036
                && isset($this->columnTypes[$this->class->fieldNames[$column]])
1415 1036
                && isset($this->class->fieldMappings[$this->class->fieldNames[$column]]['requireSQLConversion'])) {
1416 6
                $type        = Type::getType($this->columnTypes[$this->class->fieldNames[$column]]);
1417 6
                $placeholder = $type->convertToDatabaseValueSQL('?', $this->platform);
1418
            }
1419
1420 1036
            $values[] = $placeholder;
1421
        }
1422
1423 1036
        $columns = implode(', ', $columns);
1424 1036
        $values  = implode(', ', $values);
1425
1426 1036
        $this->insertSql = sprintf('INSERT INTO %s (%s) VALUES (%s)', $tableName, $columns, $values);
1427
1428 1036
        return $this->insertSql;
1429
    }
1430
1431
    /**
1432
     * Gets the list of columns to put in the INSERT SQL statement.
1433
     *
1434
     * Subclasses should override this method to alter or change the list of
1435
     * columns placed in the INSERT statements used by the persister.
1436
     *
1437
     * @return array The list of columns.
1438
     */
1439 972
    protected function getInsertColumnList()
1440
    {
1441 972
        $columns = [];
1442
1443 972
        foreach ($this->class->reflFields as $name => $field) {
1444 972
            if ($this->class->isVersioned && $this->class->versionField == $name) {
1445 201
                continue;
1446
            }
1447
1448 972
            if (isset($this->class->embeddedClasses[$name])) {
1449 8
                continue;
1450
            }
1451
1452 972
            if (isset($this->class->associationMappings[$name])) {
1453 852
                $assoc = $this->class->associationMappings[$name];
1454
1455 852
                if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
1456 802
                    foreach ($assoc['joinColumns'] as $joinColumn) {
1457 802
                        $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
1458
                    }
1459
                }
1460
1461 852
                continue;
1462
            }
1463
1464 972
            if (! $this->class->isIdGeneratorIdentity() || $this->class->identifier[0] != $name) {
1465 901
                $columns[]                = $this->quoteStrategy->getColumnName($name, $this->class, $this->platform);
1466 972
                $this->columnTypes[$name] = $this->class->fieldMappings[$name]['type'];
1467
            }
1468
        }
1469
1470 972
        return $columns;
1471
    }
1472
1473
    /**
1474
     * Gets the SQL snippet of a qualified column name for the given field name.
1475
     *
1476
     * @param string        $field The field name.
1477
     * @param ClassMetadata $class The class that declares this field. The table this class is
1478
     *                             mapped to must own the column for the given field.
1479
     * @param string        $alias
1480
     *
1481
     * @return string
1482
     */
1483 526
    protected function getSelectColumnSQL($field, ClassMetadata $class, $alias = 'r')
1484
    {
1485 526
        $root         = $alias == 'r' ? '' : $alias ;
1486 526
        $tableAlias   = $this->getSQLTableAlias($class->name, $root);
1487 526
        $fieldMapping = $class->fieldMappings[$field];
1488 526
        $sql          = sprintf('%s.%s', $tableAlias, $this->quoteStrategy->getColumnName($field, $class, $this->platform));
1489 526
        $columnAlias  = $this->getSQLColumnAlias($fieldMapping['columnName']);
1490
1491 526
        $this->currentPersisterContext->rsm->addFieldResult($alias, $columnAlias, $field);
1492
1493 526
        if (isset($fieldMapping['requireSQLConversion'])) {
1494 3
            $type = Type::getType($fieldMapping['type']);
1495 3
            $sql  = $type->convertToPHPValueSQL($sql, $this->platform);
1496
        }
1497
1498 526
        return $sql . ' AS ' . $columnAlias;
1499
    }
1500
1501
    /**
1502
     * Gets the SQL table alias for the given class name.
1503
     *
1504
     * @param string $className
1505
     * @param string $assocName
1506
     *
1507
     * @return string The SQL table alias.
1508
     *
1509
     * @todo Reconsider. Binding table aliases to class names is not such a good idea.
1510
     */
1511 633
    protected function getSQLTableAlias($className, $assocName = '')
1512
    {
1513 633
        if ($assocName) {
1514 184
            $className .= '#' . $assocName;
1515
        }
1516
1517 633
        if (isset($this->currentPersisterContext->sqlTableAliases[$className])) {
1518 629
            return $this->currentPersisterContext->sqlTableAliases[$className];
1519
        }
1520
1521 633
        $tableAlias = 't' . $this->currentPersisterContext->sqlAliasCounter++;
1522
1523 633
        $this->currentPersisterContext->sqlTableAliases[$className] = $tableAlias;
1524
1525 633
        return $tableAlias;
1526
    }
1527
1528
    /**
1529
     * {@inheritdoc}
1530
     */
1531
    public function lock(array $criteria, $lockMode)
1532
    {
1533
        $lockSql      = '';
1534
        $conditionSql = $this->getSelectConditionSQL($criteria);
1535
1536
        switch ($lockMode) {
1537
            case LockMode::PESSIMISTIC_READ:
1538
                $lockSql = $this->platform->getReadLockSQL();
1539
1540
                break;
1541
            case LockMode::PESSIMISTIC_WRITE:
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

switch ($expr) {
case "A":
    doSomething(); //right
    break;
case "B":

    doSomethingElse(); //wrong
    break;

}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1542
1543
                $lockSql = $this->platform->getWriteLockSQL();
1544
                break;
1545
        }
1546
1547
        $lock  = $this->getLockTablesSql($lockMode);
1548
        $where = ($conditionSql ? ' WHERE ' . $conditionSql : '') . ' ';
1549
        $sql = 'SELECT 1 '
1550
             . $lock
1551
             . $where
1552
             . $lockSql;
1553
1554
        list($params, $types) = $this->expandParameters($criteria);
1555
1556
        $this->conn->executeQuery($sql, $params, $types);
1557
    }
1558
1559
    /**
1560
     * Gets the FROM and optionally JOIN conditions to lock the entity managed by this persister.
1561
     *
1562
     * @param integer $lockMode One of the Doctrine\DBAL\LockMode::* constants.
1563
     *
1564
     * @return string
1565
     */
1566 13
    protected function getLockTablesSql($lockMode)
1567
    {
1568 13
        return $this->platform->appendLockHint(
1569
            'FROM '
1570 13
            . $this->quoteStrategy->getTableName($this->class, $this->platform) . ' '
1571 13
            . $this->getSQLTableAlias($this->class->name),
1572 13
            $lockMode
1573
        );
1574
    }
1575
1576
    /**
1577
     * Gets the Select Where Condition from a Criteria object.
1578
     *
1579
     * @param \Doctrine\Common\Collections\Criteria $criteria
1580
     *
1581
     * @return string
1582
     */
1583 58
    protected function getSelectConditionCriteriaSQL(Criteria $criteria)
1584
    {
1585 58
        $expression = $criteria->getWhereExpression();
1586
1587 58
        if ($expression === null) {
1588 2
            return '';
1589
        }
1590
1591 57
        $visitor = new SqlExpressionVisitor($this, $this->class);
1592
1593 57
        return $visitor->dispatch($expression);
1594
    }
1595
1596
    /**
1597
     * {@inheritdoc}
1598
     */
1599 612
    public function getSelectConditionStatementSQL($field, $value, $assoc = null, $comparison = null)
1600
    {
1601 612
        $selectedColumns = [];
1602 612
        $columns         = $this->getSelectConditionStatementColumnSQL($field, $assoc);
1603
1604 608
        if (count($columns) > 1 && $comparison === Comparison::IN) {
1605
            /*
0 ignored issues
show
Unused Code Comprehensibility introduced by
37% 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...
1606
             *  @todo try to support multi-column IN expressions.
1607
             *  Example: (col1, col2) IN (('val1A', 'val2A'), ('val1B', 'val2B'))
1608
             */
1609 1
            throw ORMException::cantUseInOperatorOnCompositeKeys();
1610
        }
1611
1612 607
        foreach ($columns as $column) {
1613 607
            $placeholder = '?';
1614
1615 607
            if (isset($this->class->fieldMappings[$field]['requireSQLConversion'])) {
1616 1
                $type        = Type::getType($this->class->fieldMappings[$field]['type']);
1617 1
                $placeholder = $type->convertToDatabaseValueSQL($placeholder, $this->platform);
1618
            }
1619
1620 607
            if (null !== $comparison) {
1621
                // special case null value handling
1622 61
                if (($comparison === Comparison::EQ || $comparison === Comparison::IS) && null ===$value) {
1623 6
                    $selectedColumns[] = $column . ' IS NULL';
1624
1625 6
                    continue;
1626
                }
1627
1628 55
                if ($comparison === Comparison::NEQ && null === $value) {
1629 3
                    $selectedColumns[] = $column . ' IS NOT NULL';
1630
1631 3
                    continue;
1632
                }
1633
1634 52
                $selectedColumns[] = $column . ' ' . sprintf(self::$comparisonMap[$comparison], $placeholder);
1635
1636 52
                continue;
1637
            }
1638
1639 579
            if (is_array($value)) {
1640 14
                $in = sprintf('%s IN (%s)', $column, $placeholder);
1641
1642 14
                if (false !== array_search(null, $value, true)) {
1643 4
                    $selectedColumns[] = sprintf('(%s OR %s IS NULL)', $in, $column);
1644
1645 4
                    continue;
1646
                }
1647
1648 10
                $selectedColumns[] = $in;
1649
1650 10
                continue;
1651
            }
1652
1653 568
            if (null === $value) {
1654 9
                $selectedColumns[] = sprintf('%s IS NULL', $column);
1655
1656 9
                continue;
1657
            }
1658
1659 560
            $selectedColumns[] = sprintf('%s = %s', $column, $placeholder);
1660
        }
1661
1662 607
        return implode(' AND ', $selectedColumns);
1663
    }
1664
1665
    /**
1666
     * Builds the left-hand-side of a where condition statement.
1667
     *
1668
     * @param string     $field
1669
     * @param array|null $assoc
1670
     *
1671
     * @return string[]
1672
     *
1673
     * @throws \Doctrine\ORM\ORMException
1674
     */
1675 612
    private function getSelectConditionStatementColumnSQL($field, $assoc = null)
1676
    {
1677 612
        if (isset($this->class->fieldMappings[$field])) {
1678 515
            $className = (isset($this->class->fieldMappings[$field]['inherited']))
1679 54
                ? $this->class->fieldMappings[$field]['inherited']
1680 515
                : $this->class->name;
1681
1682 515
            return [$this->getSQLTableAlias($className) . '.' . $this->quoteStrategy->getColumnName($field, $this->class, $this->platform)];
1683
        }
1684
1685 297
        if (isset($this->class->associationMappings[$field])) {
1686 150
            $association = $this->class->associationMappings[$field];
1687
            // Many-To-Many requires join table check for joinColumn
1688 150
            $columns = [];
1689 150
            $class   = $this->class;
1690
1691 150
            if ($association['type'] === ClassMetadata::MANY_TO_MANY) {
1692 3
                if ( ! $association['isOwningSide']) {
1693 2
                    $association = $assoc;
1694
                }
1695
1696 3
                $joinTableName = $this->quoteStrategy->getJoinTableName($association, $class, $this->platform);
1697 3
                $joinColumns   = $assoc['isOwningSide']
1698 2
                    ? $association['joinTable']['joinColumns']
1699 3
                    : $association['joinTable']['inverseJoinColumns'];
1700
1701
1702 3
                foreach ($joinColumns as $joinColumn) {
1703 3
                    $columns[] = $joinTableName . '.' . $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
1704
                }
1705
1706
            } else {
1707 148
                if ( ! $association['isOwningSide']) {
1708 1
                    throw ORMException::invalidFindByInverseAssociation($this->class->name, $field);
1709
                }
1710
1711 147
                $className  = (isset($association['inherited']))
1712 11
                    ? $association['inherited']
1713 147
                    : $this->class->name;
1714
1715 147
                foreach ($association['joinColumns'] as $joinColumn) {
1716 147
                    $columns[] = $this->getSQLTableAlias($className) . '.' . $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
1717
                }
1718
            }
1719 149
            return $columns;
1720
        }
1721
1722 164
        if ($assoc !== null && strpos($field, " ") === false && strpos($field, "(") === false) {
1723
            // very careless developers could potentially open up this normally hidden api for userland attacks,
1724
            // therefore checking for spaces and function calls which are not allowed.
1725
1726
            // found a join column condition, not really a "field"
1727 161
            return [$field];
1728
        }
1729
1730 3
        throw ORMException::unrecognizedField($field);
1731
    }
1732
1733
    /**
1734
     * Gets the conditional SQL fragment used in the WHERE clause when selecting
1735
     * entities in this persister.
1736
     *
1737
     * Subclasses are supposed to override this method if they intend to change
1738
     * or alter the criteria by which entities are selected.
1739
     *
1740
     * @param array      $criteria
1741
     * @param array|null $assoc
1742
     *
1743
     * @return string
1744
     */
1745 606
    protected function getSelectConditionSQL(array $criteria, $assoc = null)
1746
    {
1747 606
        $conditions = [];
1748
1749 606
        foreach ($criteria as $field => $value) {
1750 581
            $conditions[] = $this->getSelectConditionStatementSQL($field, $value, $assoc);
1751
        }
1752
1753 603
        return implode(' AND ', $conditions);
1754
    }
1755
1756
    /**
1757
     * {@inheritdoc}
1758
     */
1759 5
    public function getOneToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null)
1760
    {
1761 5
        $this->switchPersisterContext($offset, $limit);
1762
1763 5
        $stmt = $this->getOneToManyStatement($assoc, $sourceEntity, $offset, $limit);
1764
1765 5
        return $this->loadArrayFromStatement($assoc, $stmt);
1766
    }
1767
1768
    /**
1769
     * {@inheritdoc}
1770
     */
1771 77
    public function loadOneToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll)
1772
    {
1773 77
        $stmt = $this->getOneToManyStatement($assoc, $sourceEntity);
1774
1775 77
        return $this->loadCollectionFromStatement($assoc, $stmt, $coll);
1776
    }
1777
1778
    /**
1779
     * Builds criteria and execute SQL statement to fetch the one to many entities from.
1780
     *
1781
     * @param array    $assoc
1782
     * @param object   $sourceEntity
1783
     * @param int|null $offset
1784
     * @param int|null $limit
1785
     *
1786
     * @return \Doctrine\DBAL\Statement
1787
     */
1788 82
    private function getOneToManyStatement(array $assoc, $sourceEntity, $offset = null, $limit = null)
1789
    {
1790 82
        $this->switchPersisterContext($offset, $limit);
1791
1792 82
        $criteria    = [];
1793 82
        $parameters  = [];
1794 82
        $owningAssoc = $this->class->associationMappings[$assoc['mappedBy']];
1795 82
        $sourceClass = $this->em->getClassMetadata($assoc['sourceEntity']);
1796 82
        $tableAlias  = $this->getSQLTableAlias($owningAssoc['inherited'] ?? $this->class->name);
1797
1798 82
        foreach ($owningAssoc['targetToSourceKeyColumns'] as $sourceKeyColumn => $targetKeyColumn) {
1799 82
            if ($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...
1800 4
                $field = $sourceClass->getFieldForColumn($sourceKeyColumn);
1801 4
                $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
0 ignored issues
show
Bug introduced by
Accessing reflFields on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1802
1803 4
                if (isset($sourceClass->associationMappings[$field])) {
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...
1804 4
                    $value = $this->em->getUnitOfWork()->getEntityIdentifier($value);
1805 4
                    $value = $value[$this->em->getClassMetadata($sourceClass->associationMappings[$field]['targetEntity'])->identifier[0]];
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...
1806
                }
1807
1808 4
                $criteria[$tableAlias . "." . $targetKeyColumn] = $value;
1809 4
                $parameters[]                                   = [
1810 4
                    'value' => $value,
1811 4
                    'field' => $field,
1812 4
                    'class' => $sourceClass,
1813
                ];
1814
1815 4
                continue;
1816
            }
1817
1818 79
            $field = $sourceClass->fieldNames[$sourceKeyColumn];
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...
1819 79
            $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
1820
1821 79
            $criteria[$tableAlias . "." . $targetKeyColumn] = $value;
1822 79
            $parameters[] = [
1823 79
                'value' => $value,
1824 79
                'field' => $field,
1825 79
                'class' => $sourceClass,
1826
            ];
1827
1828
        }
1829
1830 82
        $sql                  = $this->getSelectSQL($criteria, $assoc, null, $limit, $offset);
1831 82
        list($params, $types) = $this->expandToManyParameters($parameters);
1832
1833 82
        return $this->conn->executeQuery($sql, $params, $types);
1834
    }
1835
1836
    /**
1837
     * {@inheritdoc}
1838
     */
1839 582
    public function expandParameters($criteria)
1840
    {
1841 582
        $params = [];
1842 582
        $types  = [];
1843
1844 582
        foreach ($criteria as $field => $value) {
1845 557
            if ($value === null) {
1846 3
                continue; // skip null values.
1847
            }
1848
1849 555
            $types  = array_merge($types, $this->getTypes($field, $value, $this->class));
1850 555
            $params = array_merge($params, $this->getValues($value));
1851
        }
1852
1853 582
        return [$params, $types];
1854
    }
1855
1856
    /**
1857
     * Expands the parameters from the given criteria and use the correct binding types if found,
1858
     * specialized for OneToMany or ManyToMany associations.
1859
     *
1860
     * @param mixed[][] $criteria an array of arrays containing following:
1861
     *                             - field to which each criterion will be bound
1862
     *                             - value to be bound
1863
     *                             - class to which the field belongs to
1864
     *
1865
     *
1866
     * @return array
1867
     */
1868 157
    private function expandToManyParameters($criteria)
1869
    {
1870 157
        $params = [];
1871 157
        $types  = [];
1872
1873 157
        foreach ($criteria as $criterion) {
1874 157
            if ($criterion['value'] === null) {
1875 6
                continue; // skip null values.
1876
            }
1877
1878 151
            $types  = array_merge($types, $this->getTypes($criterion['field'], $criterion['value'], $criterion['class']));
1879 151
            $params = array_merge($params, $this->getValues($criterion['value']));
1880
        }
1881
1882 157
        return [$params, $types];
1883
    }
1884
1885
    /**
1886
     * Infers field types to be used by parameter type casting.
1887
     *
1888
     * @param string        $field
1889
     * @param mixed         $value
1890
     * @param ClassMetadata $class
1891
     *
1892
     * @return array
1893
     *
1894
     * @throws \Doctrine\ORM\Query\QueryException
1895
     */
1896 715
    private function getTypes($field, $value, ClassMetadata $class)
1897
    {
1898 715
        $types = [];
1899
1900
        switch (true) {
1901 715
            case (isset($class->fieldMappings[$field])):
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1902 654
                $types = array_merge($types, [$class->fieldMappings[$field]['type']]);
1903 654
                break;
1904
1905 149
            case (isset($class->associationMappings[$field])):
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
1906 148
                $assoc = $class->associationMappings[$field];
1907 148
                $class = $this->em->getClassMetadata($assoc['targetEntity']);
1908
1909 148
                if (! $assoc['isOwningSide']) {
1910 2
                    $assoc = $class->associationMappings[$assoc['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...
1911 2
                    $class = $this->em->getClassMetadata($assoc['targetEntity']);
1912
                }
1913
1914 148
                $columns = $assoc['type'] === ClassMetadata::MANY_TO_MANY
1915 3
                    ? $assoc['relationToTargetKeyColumns']
1916 148
                    : $assoc['sourceToTargetKeyColumns'];
1917
1918 148
                foreach ($columns as $column){
1919 148
                    $types[] = PersisterHelper::getTypeOfColumn($column, $class, $this->em);
1920
                }
1921 148
                break;
1922
1923
            default:
1924 1
                $types[] = null;
1925 1
                break;
1926
        }
1927
1928 715
        if (is_array($value)) {
1929 16
            return array_map(function ($type) {
1930 16
                $type = Type::getType($type);
1931
1932 16
                return $type->getBindingType() + Connection::ARRAY_PARAM_OFFSET;
1933 16
            }, $types);
1934
        }
1935
1936 705
        return $types;
1937
    }
1938
1939
    /**
1940
     * Retrieves the parameters that identifies a value.
1941
     *
1942
     * @param mixed $value
1943
     *
1944
     * @return array
1945
     */
1946 589
    private function getValues($value)
1947
    {
1948 589
        if (is_array($value)) {
1949 16
            $newValue = [];
1950
1951 16
            foreach ($value as $itemValue) {
1952 16
                $newValue = array_merge($newValue, $this->getValues($itemValue));
1953
            }
1954
1955 16
            return [$newValue];
1956
        }
1957
1958 589
        if (is_object($value) && $this->em->getMetadataFactory()->hasMetadataFor(ClassUtils::getClass($value))) {
1959 44
            $class = $this->em->getClassMetadata(get_class($value));
1960 44
            if ($class->isIdentifierComposite) {
0 ignored issues
show
Bug introduced by
Accessing isIdentifierComposite on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1961 3
                $newValue = [];
1962
1963 3
                foreach ($class->getIdentifierValues($value) as $innerValue) {
1964 3
                    $newValue = array_merge($newValue, $this->getValues($innerValue));
1965
                }
1966
1967 3
                return $newValue;
1968
            }
1969
        }
1970
1971 589
        return [$this->getIndividualValue($value)];
1972
    }
1973
1974
    /**
1975
     * Retrieves an individual parameter value.
1976
     *
1977
     * @param mixed $value
1978
     *
1979
     * @return mixed
1980
     */
1981 589
    private function getIndividualValue($value)
1982
    {
1983 589
        if ( ! is_object($value) || ! $this->em->getMetadataFactory()->hasMetadataFor(ClassUtils::getClass($value))) {
1984 586
            return $value;
1985
        }
1986
1987 44
        return $this->em->getUnitOfWork()->getSingleIdentifierValue($value);
1988
    }
1989
1990
    /**
1991
     * {@inheritdoc}
1992
     */
1993 14
    public function exists($entity, Criteria $extraConditions = null)
1994
    {
1995 14
        $criteria = $this->class->getIdentifierValues($entity);
1996
1997 14
        if ( ! $criteria) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $criteria of type array 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...
1998 2
            return false;
1999
        }
2000
2001 13
        $alias = $this->getSQLTableAlias($this->class->name);
2002
2003
        $sql = 'SELECT 1 '
2004 13
             . $this->getLockTablesSql(null)
2005 13
             . ' WHERE ' . $this->getSelectConditionSQL($criteria);
2006
2007 13
        list($params, $types) = $this->expandParameters($criteria);
2008
2009 13
        if (null !== $extraConditions) {
2010 9
            $sql                                 .= ' AND ' . $this->getSelectConditionCriteriaSQL($extraConditions);
2011 9
            list($criteriaParams, $criteriaTypes) = $this->expandCriteriaParameters($extraConditions);
2012
2013 9
            $params = array_merge($params, $criteriaParams);
2014 9
            $types  = array_merge($types, $criteriaTypes);
2015
        }
2016
2017 13
        if ($filterSql = $this->generateFilterConditionSQL($this->class, $alias)) {
2018 3
            $sql .= ' AND ' . $filterSql;
2019
        }
2020
2021 13
        return (bool) $this->conn->fetchColumn($sql, $params, 0, $types);
2022
    }
2023
2024
    /**
2025
     * Generates the appropriate join SQL for the given join column.
2026
     *
2027
     * @param array $joinColumns The join columns definition of an association.
2028
     *
2029
     * @return string LEFT JOIN if one of the columns is nullable, INNER JOIN otherwise.
2030
     */
2031 13
    protected function getJoinSQLForJoinColumns($joinColumns)
2032
    {
2033
        // if one of the join columns is nullable, return left join
2034 13
        foreach ($joinColumns as $joinColumn) {
2035 13
             if ( ! isset($joinColumn['nullable']) || $joinColumn['nullable']) {
2036 13
                 return 'LEFT JOIN';
2037
             }
2038
        }
2039
2040 5
        return 'INNER JOIN';
2041
    }
2042
2043
    /**
2044
     * {@inheritdoc}
2045
     */
2046 600
    public function getSQLColumnAlias($columnName)
2047
    {
2048 600
        return $this->quoteStrategy->getColumnAlias($columnName, $this->currentPersisterContext->sqlAliasCounter++, $this->platform);
2049
    }
2050
2051
    /**
2052
     * Generates the filter SQL for a given entity and table alias.
2053
     *
2054
     * @param ClassMetadata $targetEntity     Metadata of the target entity.
2055
     * @param string        $targetTableAlias The table alias of the joined/selected table.
2056
     *
2057
     * @return string The SQL query part to add to a query.
2058
     */
2059 624
    protected function generateFilterConditionSQL(ClassMetadata $targetEntity, $targetTableAlias)
2060
    {
2061 624
        $filterClauses = [];
2062
2063 624
        foreach ($this->em->getFilters()->getEnabledFilters() as $filter) {
2064 22
            if ('' !== $filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias)) {
2065 22
                $filterClauses[] = '(' . $filterExpr . ')';
2066
            }
2067
        }
2068
2069 624
        $sql = implode(' AND ', $filterClauses);
2070
2071 624
        return $sql ? "(" . $sql . ")" : ""; // Wrap again to avoid "X or Y and FilterConditionSQL"
2072
    }
2073
2074
    /**
2075
     * Switches persister context according to current query offset/limits
2076
     *
2077
     * This is due to the fact that to-many associations cannot be fetch-joined when a limit is involved
2078
     *
2079
     * @param null|int $offset
2080
     * @param null|int $limit
2081
     */
2082 606
    protected function switchPersisterContext($offset, $limit)
2083
    {
2084 606
        if (null === $offset && null === $limit) {
2085 593
            $this->currentPersisterContext = $this->noLimitsContext;
2086
2087 593
            return;
2088
        }
2089
2090 42
        $this->currentPersisterContext = $this->limitsHandlingContext;
2091 42
    }
2092
}
2093