Failed Conditions
Push — master ( 8be1e3...e3936d )
by Marco
14s
created

ORM/Persisters/Entity/BasicEntityPersister.php (1 issue)

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM\Persisters\Entity;
6
7
use Doctrine\Common\Collections\Criteria;
8
use Doctrine\Common\Collections\Expr\Comparison;
9
use Doctrine\DBAL\Connection;
10
use Doctrine\DBAL\LockMode;
11
use Doctrine\DBAL\Types\Type;
12
use Doctrine\ORM\EntityManagerInterface;
13
use Doctrine\ORM\Mapping\AssociationMetadata;
14
use Doctrine\ORM\Mapping\ClassMetadata;
15
use Doctrine\ORM\Mapping\FetchMode;
16
use Doctrine\ORM\Mapping\FieldMetadata;
17
use Doctrine\ORM\Mapping\GeneratorType;
18
use Doctrine\ORM\Mapping\InheritanceType;
19
use Doctrine\ORM\Mapping\JoinColumnMetadata;
20
use Doctrine\ORM\Mapping\LocalColumnMetadata;
21
use Doctrine\ORM\Mapping\ManyToManyAssociationMetadata;
22
use Doctrine\ORM\Mapping\MappingException;
23
use Doctrine\ORM\Mapping\OneToManyAssociationMetadata;
24
use Doctrine\ORM\Mapping\ToManyAssociationMetadata;
25
use Doctrine\ORM\Mapping\ToOneAssociationMetadata;
26
use Doctrine\ORM\Mapping\VersionFieldMetadata;
27
use Doctrine\ORM\OptimisticLockException;
28
use Doctrine\ORM\ORMException;
29
use Doctrine\ORM\PersistentCollection;
30
use Doctrine\ORM\Persisters\SqlExpressionVisitor;
31
use Doctrine\ORM\Persisters\SqlValueVisitor;
32
use Doctrine\ORM\Query;
33
use Doctrine\ORM\UnitOfWork;
34
use Doctrine\ORM\Utility\PersisterHelper;
35
use Doctrine\ORM\Utility\StaticClassNameConverter;
36
37
/**
38
 * A BasicEntityPersister maps an entity to a single table in a relational database.
39
 *
40
 * A persister is always responsible for a single entity type.
41
 *
42
 * EntityPersisters are used during a UnitOfWork to apply any changes to the persistent
43
 * state of entities onto a relational database when the UnitOfWork is committed,
44
 * as well as for basic querying of entities and their associations (not DQL).
45
 *
46
 * The persisting operations that are invoked during a commit of a UnitOfWork to
47
 * persist the persistent entity state are:
48
 *
49
 *   - {@link insert} : To insert the persistent state of an entity.
50
 *   - {@link update} : To update the persistent state of an entity.
51
 *   - {@link delete} : To delete the persistent state of an entity.
52
 *
53
 * As can be seen from the above list, insertions are batched and executed all at once
54
 * for increased efficiency.
55
 *
56
 * The querying operations invoked during a UnitOfWork, either through direct find
57
 * requests or lazy-loading, are the following:
58
 *
59
 *   - {@link load} : Loads (the state of) a single, managed entity.
60
 *   - {@link loadAll} : Loads multiple, managed entities.
61
 *   - {@link loadToOneEntity} : Loads a one/many-to-one entity association (lazy-loading).
62
 *   - {@link loadOneToManyCollection} : Loads a one-to-many entity association (lazy-loading).
63
 *   - {@link loadManyToManyCollection} : Loads a many-to-many entity association (lazy-loading).
64
 *
65
 * The BasicEntityPersister implementation provides the default behavior for
66
 * persisting and querying entities that are mapped to a single database table.
67
 *
68
 * Subclasses can be created to provide custom persisting and querying strategies,
69
 * i.e. spanning multiple tables.
70
 */
71
class BasicEntityPersister implements EntityPersister
72
{
73
    /**
74
     * @var string[]
75
     */
76
    private static $comparisonMap = [
77
        Comparison::EQ          => '= %s',
78
        Comparison::IS          => '= %s',
79
        Comparison::NEQ         => '!= %s',
80
        Comparison::GT          => '> %s',
81
        Comparison::GTE         => '>= %s',
82
        Comparison::LT          => '< %s',
83
        Comparison::LTE         => '<= %s',
84
        Comparison::IN          => 'IN (%s)',
85
        Comparison::NIN         => 'NOT IN (%s)',
86
        Comparison::CONTAINS    => 'LIKE %s',
87
        Comparison::STARTS_WITH => 'LIKE %s',
88
        Comparison::ENDS_WITH   => 'LIKE %s',
89
    ];
90
91
    /**
92
     * Metadata object that describes the mapping of the mapped entity class.
93
     *
94
     * @var \Doctrine\ORM\Mapping\ClassMetadata
95
     */
96
    protected $class;
97
98
    /**
99
     * The underlying DBAL Connection of the used EntityManager.
100
     *
101
     * @var \Doctrine\DBAL\Connection $conn
102
     */
103
    protected $conn;
104
105
    /**
106
     * The database platform.
107
     *
108
     * @var \Doctrine\DBAL\Platforms\AbstractPlatform
109
     */
110
    protected $platform;
111
112
    /**
113
     * The EntityManager instance.
114
     *
115
     * @var EntityManagerInterface
116
     */
117
    protected $em;
118
119
    /**
120
     * The map of column names to DBAL columns used when INSERTing or UPDATEing an entity.
121
     *
122
     * @var array<ColumnMetadata>
123
     *
124
     * @see prepareInsertData($entity)
125
     * @see prepareUpdateData($entity)
126
     */
127
    protected $columns = [];
128
129
    /**
130
     * The INSERT SQL statement used for entities handled by this persister.
131
     * This SQL is only generated once per request, if at all.
132
     *
133
     * @var string
134
     */
135
    private $insertSql;
136
137
    /**
138
     * @var CachedPersisterContext
139
     */
140
    protected $currentPersisterContext;
141
142
    /**
143
     * @var CachedPersisterContext
144
     */
145
    private $limitsHandlingContext;
146
147
    /**
148
     * @var CachedPersisterContext
149
     */
150
    private $noLimitsContext;
151
152
    /**
153
     * Initializes a new <tt>BasicEntityPersister</tt> that uses the given EntityManager
154
     * and persists instances of the class described by the given ClassMetadata descriptor.
155
     */
156 1129
    public function __construct(EntityManagerInterface $em, ClassMetadata $class)
157
    {
158 1129
        $this->em                    = $em;
159 1129
        $this->class                 = $class;
160 1129
        $this->conn                  = $em->getConnection();
161 1129
        $this->platform              = $this->conn->getDatabasePlatform();
162 1129
        $this->noLimitsContext       = $this->currentPersisterContext = new CachedPersisterContext(
163 1129
            $class,
164 1129
            new Query\ResultSetMapping(),
165 1129
            false
166
        );
167 1129
        $this->limitsHandlingContext = new CachedPersisterContext(
168 1129
            $class,
169 1129
            new Query\ResultSetMapping(),
170 1129
            true
171
        );
172 1129
    }
173
174
    /**
175
     * {@inheritdoc}
176
     */
177 15
    public function getClassMetadata()
178
    {
179 15
        return $this->class;
180
    }
181
182
    /**
183
     * {@inheritdoc}
184
     */
185 11
    public function getResultSetMapping()
186
    {
187 11
        return $this->currentPersisterContext->rsm;
188
    }
189
190
    /**
191
     * {@inheritdoc}
192
     */
193 1049
    public function getIdentifier($entity) : array
194
    {
195 1049
        $id = [];
196
197 1049
        foreach ($this->class->getIdentifier() as $fieldName) {
198 1049
            $property = $this->class->getProperty($fieldName);
199 1049
            $value    = $property->getValue($entity);
200
201 1049
            if ($value !== null) {
202 1049
                $id[$fieldName] = $value;
203
            }
204
        }
205
206 1049
        return $id;
207
    }
208
209
    /**
210
     * Populates the entity identifier of an entity.
211
     *
212
     * @param object  $entity
213
     * @param mixed[] $id
214
     */
215 219
    public function setIdentifier($entity, array $id) : void
216
    {
217 219
        foreach ($id as $idField => $idValue) {
218 219
            $property = $this->class->getProperty($idField);
219
220 219
            $property->setValue($entity, $idValue);
221
        }
222 219
    }
223
224
    /**
225
     * {@inheritdoc}
226
     */
227 912
    public function insert($entity)
228
    {
229 912
        $stmt           = $this->conn->prepare($this->getInsertSQL());
230 912
        $tableName      = $this->class->getTableName();
231 912
        $insertData     = $this->prepareInsertData($entity);
232 912
        $generationPlan = $this->class->getValueGenerationPlan();
233
234 912
        if (isset($insertData[$tableName])) {
235 888
            $paramIndex = 1;
236
237 888
            foreach ($insertData[$tableName] as $columnName => $value) {
238 888
                $type = $this->columns[$columnName]->getType();
239
240 888
                $stmt->bindValue($paramIndex++, $value, $type);
241
            }
242
        }
243
244 912
        $stmt->execute();
245
246 911
        if ($generationPlan->containsDeferred()) {
247 826
            $generationPlan->executeDeferred($this->em, $entity);
248
        }
249
250 911
        if ($this->class->isVersioned()) {
251 196
            $this->assignDefaultVersionValue($entity, $this->getIdentifier($entity));
252
        }
253
254 911
        $stmt->closeCursor();
255 911
    }
256
257
    /**
258
     * Retrieves the default version value which was created
259
     * by the preceding INSERT statement and assigns it back in to the
260
     * entities version field.
261
     *
262
     * @param object  $entity
263
     * @param mixed[] $id
264
     */
265 205
    protected function assignDefaultVersionValue($entity, array $id)
266
    {
267 205
        $versionProperty = $this->class->versionProperty;
268 205
        $versionValue    = $this->fetchVersionValue($versionProperty, $id);
269
270 205
        $versionProperty->setValue($entity, $versionValue);
271 205
    }
272
273
    /**
274
     * Fetches the current version value of a versioned entity.
275
     *
276
     * @param mixed[] $id
277
     *
278
     * @return mixed
279
     */
280 205
    protected function fetchVersionValue(VersionFieldMetadata $versionProperty, array $id)
281
    {
282 205
        $versionedClass = $versionProperty->getDeclaringClass();
283 205
        $tableName      = $versionedClass->table->getQuotedQualifiedName($this->platform);
284 205
        $columnName     = $this->platform->quoteIdentifier($versionProperty->getColumnName());
285 205
        $identifier     = array_map(
286 205
            function ($columnName) {
287 205
                return $this->platform->quoteIdentifier($columnName);
288 205
            },
289 205
            array_keys($versionedClass->getIdentifierColumns($this->em))
290
        );
291
292
        // FIXME: Order with composite keys might not be correct
293 205
        $sql = 'SELECT ' . $columnName
294 205
             . ' FROM ' . $tableName
295 205
             . ' WHERE ' . implode(' = ? AND ', $identifier) . ' = ?';
296
297 205
        $flattenedId = $this->em->getIdentifierFlattener()->flattenIdentifier($versionedClass, $id);
298 205
        $versionType = $versionProperty->getType();
299
300 205
        $value = $this->conn->fetchColumn(
301 205
            $sql,
302 205
            array_values($flattenedId),
303 205
            0,
304 205
            $this->extractIdentifierTypes($id, $versionedClass)
305
        );
306
307 205
        return $versionType->convertToPHPValue($value, $this->platform);
308
    }
309
310
    /**
311
     * @param mixed[] $id
312
     *
313
     * @return mixed[]
314
     */
315 205
    private function extractIdentifierTypes(array $id, ClassMetadata $versionedClass) : array
316
    {
317 205
        $types = [];
318
319 205
        foreach ($id as $field => $value) {
320 205
            $types = array_merge($types, $this->getTypes($field, $value, $versionedClass));
321
        }
322
323 205
        return $types;
324
    }
325
326
    /**
327
     * {@inheritdoc}
328
     */
329 80
    public function update($entity)
330
    {
331 80
        $tableName  = $this->class->getTableName();
332 80
        $updateData = $this->prepareUpdateData($entity);
333
334 80
        if (! isset($updateData[$tableName])) {
335 8
            return;
336
        }
337
338 72
        $data = $updateData[$tableName];
339
340 72
        if (! $data) {
341
            return;
342
        }
343
344 72
        $isVersioned     = $this->class->isVersioned();
345 72
        $quotedTableName = $this->class->table->getQuotedQualifiedName($this->platform);
346
347 72
        $this->updateTable($entity, $quotedTableName, $data, $isVersioned);
348
349 70
        if ($isVersioned) {
350 11
            $id = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
351
352 11
            $this->assignDefaultVersionValue($entity, $id);
353
        }
354 70
    }
355
356
    /**
357
     * {@inheritdoc}
358
     */
359 58
    public function delete($entity)
360
    {
361 58
        $class      = $this->class;
362 58
        $unitOfWork = $this->em->getUnitOfWork();
363 58
        $identifier = $unitOfWork->getEntityIdentifier($entity);
364 58
        $tableName  = $class->table->getQuotedQualifiedName($this->platform);
365
366 58
        $types = [];
367 58
        $id    = [];
368
369 58
        foreach ($class->identifier as $field) {
370 58
            $property = $class->getProperty($field);
371
372 58
            if ($property instanceof FieldMetadata) {
373 56
                $columnName       = $property->getColumnName();
374 56
                $quotedColumnName = $this->platform->quoteIdentifier($columnName);
375
376 56
                $id[$quotedColumnName] = $identifier[$field];
377 56
                $types[]               = $property->getType();
378
379 56
                continue;
380
            }
381
382 5
            $targetClass = $this->em->getClassMetadata($property->getTargetEntity());
383 5
            $joinColumns = $property instanceof ManyToManyAssociationMetadata
384
                ? $property->getTable()->getJoinColumns()
385 5
                : $property->getJoinColumns()
386
            ;
387
388 5
            $associationValue = null;
389 5
            $value            = $identifier[$field];
390
391 5
            if ($value !== null) {
392
                // @todo guilhermeblanco Make sure we do not have flat association values.
393 5
                if (! is_array($value)) {
394 5
                    $value = [$targetClass->identifier[0] => $value];
395
                }
396
397 5
                $associationValue = $value;
398
            }
399
400 5
            foreach ($joinColumns as $joinColumn) {
401
                /** @var JoinColumnMetadata $joinColumn */
402 5
                $quotedColumnName     = $this->platform->quoteIdentifier($joinColumn->getColumnName());
403 5
                $referencedColumnName = $joinColumn->getReferencedColumnName();
404 5
                $targetField          = $targetClass->fieldNames[$referencedColumnName];
405
406 5
                if (! $joinColumn->getType()) {
407
                    $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
408
                }
409
410 5
                $id[$quotedColumnName] = $associationValue ? $associationValue[$targetField] : null;
411 5
                $types[]               = $joinColumn->getType();
412
            }
413
        }
414
415 58
        $this->deleteJoinTableRecords($identifier);
416
417 58
        return (bool) $this->conn->delete($tableName, $id, $types);
418
    }
419
420
    /**
421
     * Performs an UPDATE statement for an entity on a specific table.
422
     * The UPDATE can optionally be versioned, which requires the entity to have a version field.
423
     *
424
     * @param object  $entity          The entity object being updated.
425
     * @param string  $quotedTableName The quoted name of the table to apply the UPDATE on.
426
     * @param mixed[] $updateData      The map of columns to update (column => value).
427
     * @param bool    $versioned       Whether the UPDATE should be versioned.
428
     *
429
     * @throws \Doctrine\ORM\ORMException
430
     * @throws \Doctrine\ORM\OptimisticLockException
431
     */
432 102
    final protected function updateTable($entity, $quotedTableName, array $updateData, $versioned = false)
433
    {
434 102
        $set    = [];
435 102
        $types  = [];
436 102
        $params = [];
437
438 102
        foreach ($updateData as $columnName => $value) {
439 102
            $column           = $this->columns[$columnName];
440 102
            $quotedColumnName = $this->platform->quoteIdentifier($column->getColumnName());
441 102
            $type             = $column->getType();
442 102
            $placeholder      = $type->convertToDatabaseValueSQL('?', $this->platform);
443
444 102
            $set[]    = sprintf('%s = %s', $quotedColumnName, $placeholder);
445 102
            $params[] = $value;
446 102
            $types[]  = $column->getType();
447
        }
448
449
        // @todo guilhermeblanco Bring this back: $this->em->getUnitOfWork()->getEntityIdentifier($entity);
450 102
        $identifier = $this->getIdentifier($entity);
451 102
        $where      = [];
452
453 102
        foreach ($this->class->identifier as $idField) {
454 102
            $property = $this->class->getProperty($idField);
455
456
            switch (true) {
457 102
                case ($property instanceof FieldMetadata):
458 99
                    $where[]  = $this->platform->quoteIdentifier($property->getColumnName());
459 99
                    $params[] = $identifier[$idField];
460 99
                    $types[]  = $property->getType();
461 99
                    break;
462
463 4
                case ($property instanceof ToOneAssociationMetadata):
464 4
                    $targetClass     = $this->em->getClassMetadata($property->getTargetEntity());
465 4
                    $targetPersister = $this->em->getUnitOfWork()->getEntityPersister($property->getTargetEntity());
466
467 4
                    foreach ($property->getJoinColumns() as $joinColumn) {
468
                        /** @var JoinColumnMetadata $joinColumn */
469 4
                        $quotedColumnName     = $this->platform->quoteIdentifier($joinColumn->getColumnName());
470 4
                        $referencedColumnName = $joinColumn->getReferencedColumnName();
471
472 4
                        if (! $joinColumn->getType()) {
473
                            $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
474
                        }
475
476 4
                        $value = $targetPersister->getColumnValue($identifier[$idField], $referencedColumnName);
477
478 4
                        $where[]  = $quotedColumnName;
479 4
                        $params[] = $value;
480 4
                        $types[]  = $joinColumn->getType();
481
                    }
482 102
                    break;
483
            }
484
        }
485
486 102
        if ($versioned) {
487 19
            $versionProperty   = $this->class->versionProperty;
488 19
            $versionColumnType = $versionProperty->getType();
489 19
            $versionColumnName = $this->platform->quoteIdentifier($versionProperty->getColumnName());
490
491 19
            $where[]  = $versionColumnName;
492 19
            $types[]  = $versionColumnType;
493 19
            $params[] = $versionProperty->getValue($entity);
494
495 19
            switch ($versionColumnType->getName()) {
496
                case Type::SMALLINT:
497
                case Type::INTEGER:
498
                case Type::BIGINT:
499 18
                    $set[] = $versionColumnName . ' = ' . $versionColumnName . ' + 1';
500 18
                    break;
501
502
                case Type::DATETIME:
503 1
                    $set[] = $versionColumnName . ' = CURRENT_TIMESTAMP';
504 1
                    break;
505
            }
506
        }
507
508 102
        $sql = 'UPDATE ' . $quotedTableName
509 102
             . ' SET ' . implode(', ', $set)
510 102
             . ' WHERE ' . implode(' = ? AND ', $where) . ' = ?';
511
512 102
        $result = $this->conn->executeUpdate($sql, $params, $types);
513
514 102
        if ($versioned && ! $result) {
515 4
            throw OptimisticLockException::lockFailed($entity);
516
        }
517 99
    }
518
519
    /**
520
     * @todo Add check for platform if it supports foreign keys/cascading.
521
     *
522
     * @param mixed[] $identifier
523
     */
524 62
    protected function deleteJoinTableRecords($identifier)
525
    {
526 62
        foreach ($this->class->getDeclaredPropertiesIterator() as $association) {
527 62
            if (! ($association instanceof ManyToManyAssociationMetadata)) {
528 62
                continue;
529
            }
530
531
            // @Todo this only covers scenarios with no inheritance or of the same level. Is there something
532
            // like self-referential relationship between different levels of an inheritance hierarchy? I hope not!
533 23
            $selfReferential   = $association->getTargetEntity() === $association->getSourceEntity();
534 23
            $owningAssociation = $association;
535 23
            $otherColumns      = [];
536 23
            $otherKeys         = [];
537 23
            $keys              = [];
538
539 23
            if (! $owningAssociation->isOwningSide()) {
540 6
                $class             = $this->em->getClassMetadata($association->getTargetEntity());
541 6
                $owningAssociation = $class->getProperty($association->getMappedBy());
542
            }
543
544 23
            $joinTable     = $owningAssociation->getJoinTable();
545 23
            $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
546 23
            $joinColumns   = $association->isOwningSide()
547 19
                ? $joinTable->getJoinColumns()
548 23
                : $joinTable->getInverseJoinColumns()
549
            ;
550
551 23
            if ($selfReferential) {
552 1
                $otherColumns = ! $association->isOwningSide()
553
                    ? $joinTable->getJoinColumns()
554 1
                    : $joinTable->getInverseJoinColumns()
555
                ;
556
            }
557
558 23
            $isOnDeleteCascade = false;
559
560 23
            foreach ($joinColumns as $joinColumn) {
561 23
                $keys[] = $this->platform->quoteIdentifier($joinColumn->getColumnName());
562
563 23
                if ($joinColumn->isOnDeleteCascade()) {
564 23
                    $isOnDeleteCascade = true;
565
                }
566
            }
567
568 23
            foreach ($otherColumns as $joinColumn) {
569 1
                $otherKeys[] = $this->platform->quoteIdentifier($joinColumn->getColumnName());
570
571 1
                if ($joinColumn->isOnDeleteCascade()) {
572 1
                    $isOnDeleteCascade = true;
573
                }
574
            }
575
576 23
            if ($isOnDeleteCascade) {
577 5
                continue;
578
            }
579
580 19
            $this->conn->delete($joinTableName, array_combine($keys, $identifier));
581
582 19
            if ($selfReferential) {
583 19
                $this->conn->delete($joinTableName, array_combine($otherKeys, $identifier));
584
            }
585
        }
586 62
    }
587
588
    /**
589
     * Prepares the data changeset of a managed entity for database insertion (initial INSERT).
590
     * The changeset of the entity is obtained from the currently running UnitOfWork.
591
     *
592
     * The default insert data preparation is the same as for updates.
593
     *
594
     * @param object $entity The entity for which to prepare the data.
595
     *
596
     * @return mixed[] The prepared data for the tables to update.
597
     */
598 996
    protected function prepareInsertData($entity) : array
599
    {
600 996
        return $this->prepareUpdateData($entity);
601
    }
602
603
    /**
604
     * Prepares the changeset of an entity for database insertion (UPDATE).
605
     *
606
     * The changeset is obtained from the currently running UnitOfWork.
607
     *
608
     * During this preparation the array that is passed as the second parameter is filled with
609
     * <columnName> => <value> pairs, grouped by table name.
610
     *
611
     * Example:
612
     * <code>
613
     * array(
614
     *    'foo_table' => array('column1' => 'value1', 'column2' => 'value2', ...),
615
     *    'bar_table' => array('columnX' => 'valueX', 'columnY' => 'valueY', ...),
616
     *    ...
617
     * )
618
     * </code>
619
     *
620
     * @param object $entity The entity for which to prepare the data.
621
     *
622
     * @return mixed[] The prepared data.
623
     */
624 1001
    protected function prepareUpdateData($entity)
625
    {
626 1001
        $uow                 = $this->em->getUnitOfWork();
627 1001
        $result              = [];
628 1001
        $versionPropertyName = $this->class->isVersioned()
629 209
            ? $this->class->versionProperty->getName()
630 1001
            : null
631
        ;
632
633
        // @todo guilhermeblanco This should check column insertability/updateability instead of field changeset
634 1001
        foreach ($uow->getEntityChangeSet($entity) as $propertyName => $propertyChangeSet) {
635 968
            if ($versionPropertyName === $propertyName) {
636
                continue;
637
            }
638
639 968
            $property = $this->class->getProperty($propertyName);
640 968
            $newValue = $propertyChangeSet[1];
641
642 968
            if ($property instanceof FieldMetadata) {
643
                // @todo guilhermeblanco Please remove this in the future for good...
644 935
                $this->columns[$property->getColumnName()] = $property;
645
646 935
                $result[$property->getTableName()][$property->getColumnName()] = $newValue;
647
648 935
                continue;
649
            }
650
651
            // Only owning side of x-1 associations can have a FK column.
652 832
            if (! $property instanceof ToOneAssociationMetadata || ! $property->isOwningSide()) {
653 8
                continue;
654
            }
655
656
            // The associated entity $newVal is not yet persisted, so we must
657
            // set $newVal = null, in order to insert a null value and schedule an
658
            // extra update on the UnitOfWork.
659 832
            if ($newValue !== null && $uow->isScheduledForInsert($newValue)) {
660 32
                $uow->scheduleExtraUpdate($entity, [$propertyName => [null, $newValue]]);
661
662 32
                $newValue = null;
663
            }
664
665 832
            $targetClass     = $this->em->getClassMetadata($property->getTargetEntity());
666 832
            $targetPersister = $uow->getEntityPersister($targetClass->getClassName());
667
668 832
            foreach ($property->getJoinColumns() as $joinColumn) {
669
                /** @var JoinColumnMetadata $joinColumn */
670 832
                $referencedColumnName = $joinColumn->getReferencedColumnName();
671
672 832
                if (! $joinColumn->getType()) {
673 9
                    $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
674
                }
675
676
                // @todo guilhermeblanco Please remove this in the future for good...
677 832
                $this->columns[$joinColumn->getColumnName()] = $joinColumn;
678
679 832
                $result[$joinColumn->getTableName()][$joinColumn->getColumnName()] = $newValue !== null
680 614
                    ? $targetPersister->getColumnValue($newValue, $referencedColumnName)
681 832
                    : null
682
                ;
683
            }
684
        }
685
686 1001
        return $result;
687
    }
688
689
    /**
690
     * @param object $entity
691
     *
692
     * @return mixed|null
693
     */
694 614
    public function getColumnValue($entity, string $columnName)
695
    {
696
        // Looking for fields by column is the easiest way to look at local columns or x-1 owning side associations
697 614
        $propertyName = $this->class->fieldNames[$columnName];
698 614
        $property     = $this->class->getProperty($propertyName);
699
700 614
        if (! $property) {
701
            return null;
702
        }
703
704 614
        $propertyValue = $property->getValue($entity);
705
706 614
        if ($property instanceof LocalColumnMetadata) {
707 614
            return $propertyValue;
708
        }
709
710
        /* @var ToOneAssociationMetadata $property */
711 19
        $unitOfWork      = $this->em->getUnitOfWork();
712 19
        $targetClass     = $this->em->getClassMetadata($property->getTargetEntity());
713 19
        $targetPersister = $unitOfWork->getEntityPersister($property->getTargetEntity());
714
715 19
        foreach ($property->getJoinColumns() as $joinColumn) {
716
            /** @var JoinColumnMetadata $joinColumn */
717 19
            $referencedColumnName = $joinColumn->getReferencedColumnName();
718
719 19
            if (! $joinColumn->getType()) {
720
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
721
            }
722
723 19
            if ($joinColumn->getColumnName() !== $columnName) {
724
                continue;
725
            }
726
727 19
            return $targetPersister->getColumnValue($propertyValue, $referencedColumnName);
728
        }
729
730
        return null;
731
    }
732
733
    /**
734
     * {@inheritdoc}
735
     */
736 471
    public function load(
737
        array $criteria,
738
        $entity = null,
739
        ?AssociationMetadata $association = null,
740
        array $hints = [],
741
        $lockMode = null,
742
        $limit = null,
743
        array $orderBy = []
744
    ) {
745 471
        $this->switchPersisterContext(null, $limit);
746
747 471
        $sql = $this->getSelectSQL($criteria, $association, $lockMode, $limit, null, $orderBy);
748
749 470
        list($params, $types) = $this->expandParameters($criteria);
750
751 470
        $stmt = $this->conn->executeQuery($sql, $params, $types);
752
753 470
        if ($entity !== null) {
754 63
            $hints[Query::HINT_REFRESH]        = true;
755 63
            $hints[Query::HINT_REFRESH_ENTITY] = $entity;
756
        }
757
758 470
        $hydrator = $this->em->newHydrator($this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
759 470
        $entities = $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, $hints);
760
761 470
        return $entities ? $entities[0] : null;
762
    }
763
764
    /**
765
     * {@inheritdoc}
766
     */
767 395
    public function loadById(array $identifier, $entity = null)
768
    {
769 395
        return $this->load($identifier, $entity);
770
    }
771
772
    /**
773
     * {@inheritdoc}
774
     */
775 92
    public function loadToOneEntity(ToOneAssociationMetadata $association, $sourceEntity, array $identifier = [])
776
    {
777 92
        $unitOfWork   = $this->em->getUnitOfWork();
778 92
        $targetEntity = $association->getTargetEntity();
779 92
        $foundEntity  = $unitOfWork->tryGetById($identifier, $targetEntity);
780
781 92
        if ($foundEntity !== false) {
782
            return $foundEntity;
783
        }
784
785 92
        $targetClass = $this->em->getClassMetadata($targetEntity);
786
787 92
        if ($association->isOwningSide()) {
788 30
            $inversedBy            = $association->getInversedBy();
789 30
            $targetProperty        = $inversedBy ? $targetClass->getProperty($inversedBy) : null;
790 30
            $isInverseSingleValued = $targetProperty && $targetProperty instanceof ToOneAssociationMetadata;
791
792
            // Mark inverse side as fetched in the hints, otherwise the UoW would
793
            // try to load it in a separate query (remember: to-one inverse sides can not be lazy).
794 30
            $hints = [];
795
796 30
            if ($isInverseSingleValued) {
797
                $hints['fetched']['r'][$inversedBy] = true;
798
            }
799
800
            /* cascade read-only status
801
            if ($this->em->getUnitOfWork()->isReadOnly($sourceEntity)) {
802
                $hints[Query::HINT_READ_ONLY] = true;
803
            }
804
            */
805
806 30
            $entity = $this->load($identifier, null, $association, $hints);
807
808
            // Complete bidirectional association, if necessary
809 30
            if ($entity !== null && $isInverseSingleValued) {
810
                $targetProperty->setValue($entity, $sourceEntity);
811
            }
812
813 30
            return $entity;
814
        }
815
816 62
        $sourceClass       = $association->getDeclaringClass();
817 62
        $owningAssociation = $targetClass->getProperty($association->getMappedBy());
818 62
        $targetTableAlias  = $this->getSQLTableAlias($targetClass->getTableName());
819
820 62
        foreach ($owningAssociation->getJoinColumns() as $joinColumn) {
821 62
            $sourceKeyColumn = $joinColumn->getReferencedColumnName();
822 62
            $targetKeyColumn = $joinColumn->getColumnName();
823
824 62
            if (! isset($sourceClass->fieldNames[$sourceKeyColumn])) {
825
                throw MappingException::joinColumnMustPointToMappedField(
826
                    $sourceClass->getClassName(),
827
                    $sourceKeyColumn
828
                );
829
            }
830
831 62
            $property = $sourceClass->getProperty($sourceClass->fieldNames[$sourceKeyColumn]);
832 62
            $value    = $property->getValue($sourceEntity);
833
834
            // unset the old value and set the new sql aliased value here. By definition
835
            // unset($identifier[$targetKeyColumn] works here with how UnitOfWork::createEntity() calls this method.
836
            // @todo guilhermeblanco In master we have: $identifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
837 62
            unset($identifier[$targetKeyColumn]);
838
839 62
            $identifier[$targetClass->fieldNames[$targetKeyColumn]] = $value;
840
        }
841
842 62
        $entity = $this->load($identifier, null, $association);
843
844 62
        if ($entity !== null) {
845 16
            $owningAssociation->setValue($entity, $sourceEntity);
846
        }
847
848 62
        return $entity;
849
    }
850
851
    /**
852
     * {@inheritdoc}
853
     */
854 15
    public function refresh(array $id, $entity, $lockMode = null)
855
    {
856 15
        $sql                  = $this->getSelectSQL($id, null, $lockMode);
857 15
        list($params, $types) = $this->expandParameters($id);
858 15
        $stmt                 = $this->conn->executeQuery($sql, $params, $types);
859
860 15
        $hydrator = $this->em->newHydrator(Query::HYDRATE_OBJECT);
861 15
        $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [Query::HINT_REFRESH => true]);
862 15
    }
863
864
    /**
865
     * {@inheritDoc}
866
     */
867 46
    public function count($criteria = [])
868
    {
869 46
        $sql = $this->getCountSQL($criteria);
870
871 46
        list($params, $types) = ($criteria instanceof Criteria)
872 25
            ? $this->expandCriteriaParameters($criteria)
873 46
            : $this->expandParameters($criteria);
874
875 46
        return (int) $this->conn->executeQuery($sql, $params, $types)->fetchColumn();
876
    }
877
878
    /**
879
     * {@inheritdoc}
880
     */
881 8
    public function loadCriteria(Criteria $criteria)
882
    {
883 8
        $orderBy = $criteria->getOrderings();
884 8
        $limit   = $criteria->getMaxResults();
885 8
        $offset  = $criteria->getFirstResult();
886 8
        $query   = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy);
887
888 6
        list($params, $types) = $this->expandCriteriaParameters($criteria);
889
890 6
        $stmt         = $this->conn->executeQuery($query, $params, $types);
891 6
        $rsm          = $this->currentPersisterContext->rsm;
892 6
        $hints        = [UnitOfWork::HINT_DEFEREAGERLOAD => true];
893 6
        $hydratorType = $this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT;
894 6
        $hydrator     = $this->em->newHydrator($hydratorType);
895
896 6
        return $hydrator->hydrateAll($stmt, $rsm, $hints);
897
    }
898
899
    /**
900
     * {@inheritdoc}
901
     */
902 37
    public function expandCriteriaParameters(Criteria $criteria)
903
    {
904 37
        $expression = $criteria->getWhereExpression();
905 37
        $sqlParams  = [];
906 37
        $sqlTypes   = [];
907
908 37
        if ($expression === null) {
909 2
            return [$sqlParams, $sqlTypes];
910
        }
911
912 36
        $valueVisitor = new SqlValueVisitor();
913
914 36
        $valueVisitor->dispatch($expression);
915
916 36
        list($params, $types) = $valueVisitor->getParamsAndTypes();
917
918 36
        foreach ($params as $param) {
919 32
            $sqlParams = array_merge($sqlParams, $this->getValues($param));
920
        }
921
922 36
        foreach ($types as $type) {
923 32
            list ($field, $value) = $type;
924 32
            $sqlTypes             = array_merge($sqlTypes, $this->getTypes($field, $value, $this->class));
925
        }
926
927 36
        return [$sqlParams, $sqlTypes];
928
    }
929
930
    /**
931
     * {@inheritdoc}
932
     */
933 71
    public function loadAll(array $criteria = [], array $orderBy = [], $limit = null, $offset = null)
934
    {
935 71
        $this->switchPersisterContext($offset, $limit);
936
937 71
        $sql = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy);
938
939 67
        list($params, $types) = $this->expandParameters($criteria);
940
941 67
        $stmt         = $this->conn->executeQuery($sql, $params, $types);
942 67
        $rsm          = $this->currentPersisterContext->rsm;
943 67
        $hints        = [UnitOfWork::HINT_DEFEREAGERLOAD => true];
944 67
        $hydratorType = $this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT;
945 67
        $hydrator     = $this->em->newHydrator($hydratorType);
946
947 67
        return $hydrator->hydrateAll($stmt, $rsm, $hints);
948
    }
949
950
    /**
951
     * {@inheritdoc}
952
     */
953 8
    public function getManyToManyCollection(
954
        ManyToManyAssociationMetadata $association,
955
        $sourceEntity,
956
        $offset = null,
957
        $limit = null
958
    ) {
959 8
        $this->switchPersisterContext($offset, $limit);
960
961 8
        $stmt = $this->getManyToManyStatement($association, $sourceEntity, $offset, $limit);
962
963 8
        return $this->loadArrayFromStatement($association, $stmt);
964
    }
965
966
    /**
967
     * {@inheritdoc}
968
     */
969 73
    public function loadManyToManyCollection(
970
        ManyToManyAssociationMetadata $association,
971
        $sourceEntity,
972
        PersistentCollection $collection
973
    ) {
974 73
        $stmt = $this->getManyToManyStatement($association, $sourceEntity);
975
976 73
        return $this->loadCollectionFromStatement($association, $stmt, $collection);
977
    }
978
979
    /**
980
     * Loads an array of entities from a given DBAL statement.
981
     *
982
     * @param \Doctrine\DBAL\Statement $stmt
983
     *
984
     * @return mixed[]
985
     */
986 13
    private function loadArrayFromStatement(ToManyAssociationMetadata $association, $stmt)
987
    {
988 13
        $rsm = $this->currentPersisterContext->rsm;
989
990 13
        if ($association->getIndexedBy()) {
991 7
            $rsm = clone ($this->currentPersisterContext->rsm); // this is necessary because the "default rsm" should be changed.
992 7
            $rsm->addIndexBy('r', $association->getIndexedBy());
993
        }
994
995 13
        $hydrator = $this->em->newHydrator(Query::HYDRATE_OBJECT);
996 13
        $hints    = [UnitOfWork::HINT_DEFEREAGERLOAD => true];
997
998 13
        return $hydrator->hydrateAll($stmt, $rsm, $hints);
999
    }
1000
1001
    /**
1002
     * Hydrates a collection from a given DBAL statement.
1003
     *
1004
     * @param \Doctrine\DBAL\Statement $stmt
1005
     * @param PersistentCollection     $collection
1006
     *
1007
     * @return mixed[]
1008
     */
1009 135
    private function loadCollectionFromStatement(ToManyAssociationMetadata $association, $stmt, $collection)
1010
    {
1011 135
        $rsm = $this->currentPersisterContext->rsm;
1012
1013 135
        if ($association->getIndexedBy()) {
1014 10
            $rsm = clone ($this->currentPersisterContext->rsm); // this is necessary because the "default rsm" should be changed.
1015 10
            $rsm->addIndexBy('r', $association->getIndexedBy());
1016
        }
1017
1018 135
        $hydrator = $this->em->newHydrator(Query::HYDRATE_OBJECT);
1019
        $hints    = [
1020 135
            UnitOfWork::HINT_DEFEREAGERLOAD => true,
1021 135
            'collection' => $collection,
1022
        ];
1023
1024 135
        return $hydrator->hydrateAll($stmt, $rsm, $hints);
1025
    }
1026
1027
    /**
1028
     * @param object   $sourceEntity
1029
     * @param int|null $offset
1030
     * @param int|null $limit
1031
     *
1032
     * @return \Doctrine\DBAL\Driver\Statement
1033
     *
1034
     * @throws \Doctrine\ORM\Mapping\MappingException
1035
     */
1036 80
    private function getManyToManyStatement(
1037
        ManyToManyAssociationMetadata $association,
1038
        $sourceEntity,
1039
        $offset = null,
1040
        $limit = null
1041
    ) {
1042 80
        $this->switchPersisterContext($offset, $limit);
1043
1044
        /** @var ClassMetadata $sourceClass */
1045 80
        $sourceClass = $this->em->getClassMetadata($association->getSourceEntity());
1046 80
        $class       = $sourceClass;
1047 80
        $owningAssoc = $association;
1048 80
        $criteria    = [];
1049 80
        $parameters  = [];
1050
1051 80
        if (! $association->isOwningSide()) {
1052 12
            $class       = $this->em->getClassMetadata($association->getTargetEntity());
1053 12
            $owningAssoc = $class->getProperty($association->getMappedBy());
1054
        }
1055
1056 80
        $joinTable     = $owningAssoc->getJoinTable();
1057 80
        $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
1058 80
        $joinColumns   = $association->isOwningSide()
1059 73
            ? $joinTable->getJoinColumns()
1060 80
            : $joinTable->getInverseJoinColumns()
1061
        ;
1062
1063 80
        foreach ($joinColumns as $joinColumn) {
1064 80
            $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName());
1065 80
            $fieldName        = $sourceClass->fieldNames[$joinColumn->getReferencedColumnName()];
1066 80
            $property         = $sourceClass->getProperty($fieldName);
1067
1068 80
            if ($property instanceof FieldMetadata) {
1069 79
                $value = $property->getValue($sourceEntity);
1070 4
            } elseif ($property instanceof AssociationMetadata) {
1071 4
                $property    = $sourceClass->getProperty($fieldName);
1072 4
                $targetClass = $this->em->getClassMetadata($property->getTargetEntity());
1073 4
                $value       = $property->getValue($sourceEntity);
1074
1075 4
                $value = $this->em->getUnitOfWork()->getEntityIdentifier($value);
1076 4
                $value = $value[$targetClass->identifier[0]];
1077
            }
1078
1079 80
            $criteria[$joinTableName . '.' . $quotedColumnName] = $value;
1080 80
            $parameters[]                                       = [
1081 80
                'value' => $value,
1082 80
                'field' => $fieldName,
1083 80
                'class' => $sourceClass,
1084
            ];
1085
        }
1086
1087 80
        $sql = $this->getSelectSQL($criteria, $association, null, $limit, $offset);
1088
1089 80
        list($params, $types) = $this->expandToManyParameters($parameters);
1090
1091 80
        return $this->conn->executeQuery($sql, $params, $types);
1092
    }
1093
1094
    /**
1095
     * {@inheritdoc}
1096
     */
1097 524
    public function getSelectSQL(
1098
        $criteria,
1099
        ?AssociationMetadata $association = null,
1100
        $lockMode = null,
1101
        $limit = null,
1102
        $offset = null,
1103
        array $orderBy = []
1104
    ) {
1105 524
        $this->switchPersisterContext($offset, $limit);
1106
1107 524
        $lockSql    = '';
1108 524
        $joinSql    = '';
1109 524
        $orderBySql = '';
1110
1111 524
        if ($association instanceof ManyToManyAssociationMetadata) {
1112 81
            $joinSql = $this->getSelectManyToManyJoinSQL($association);
1113
        }
1114
1115 524
        if ($association instanceof ToManyAssociationMetadata && $association->getOrderBy()) {
1116 5
            $orderBy = $association->getOrderBy();
1117
        }
1118
1119 524
        if ($orderBy) {
1120 11
            $orderBySql = $this->getOrderBySQL($orderBy, $this->getSQLTableAlias($this->class->getTableName()));
1121
        }
1122
1123 522
        $conditionSql = ($criteria instanceof Criteria)
1124 8
            ? $this->getSelectConditionCriteriaSQL($criteria)
1125 520
            : $this->getSelectConditionSQL($criteria, $association);
1126
1127
        switch ($lockMode) {
1128 517
            case LockMode::PESSIMISTIC_READ:
1129
                $lockSql = ' ' . $this->platform->getReadLockSQL();
1130
                break;
1131
1132 517
            case LockMode::PESSIMISTIC_WRITE:
1133
                $lockSql = ' ' . $this->platform->getWriteLockSQL();
1134
                break;
1135
        }
1136
1137 517
        $columnList = $this->getSelectColumnsSQL();
1138 517
        $tableAlias = $this->getSQLTableAlias($this->class->getTableName());
1139 517
        $filterSql  = $this->generateFilterConditionSQL($this->class, $tableAlias);
1140 517
        $tableName  = $this->class->table->getQuotedQualifiedName($this->platform);
1141
1142 517
        if ($filterSql !== '') {
1143 12
            $conditionSql = $conditionSql
1144 11
                ? $conditionSql . ' AND ' . $filterSql
1145 12
                : $filterSql;
1146
        }
1147
1148 517
        $select = 'SELECT ' . $columnList;
1149 517
        $from   = ' FROM ' . $tableName . ' ' . $tableAlias;
1150 517
        $join   = $this->currentPersisterContext->selectJoinSql . $joinSql;
1151 517
        $where  = ($conditionSql ? ' WHERE ' . $conditionSql : '');
1152 517
        $lock   = $this->platform->appendLockHint($from, $lockMode);
1153
        $query  = $select
1154 517
            . $lock
1155 517
            . $join
1156 517
            . $where
1157 517
            . $orderBySql;
1158
1159 517
        return $this->platform->modifyLimitQuery($query, $limit, $offset) . $lockSql;
1160
    }
1161
1162
    /**
1163
     * {@inheritDoc}
1164
     */
1165 41
    public function getCountSQL($criteria = [])
1166
    {
1167 41
        $tableName  = $this->class->table->getQuotedQualifiedName($this->platform);
1168 41
        $tableAlias = $this->getSQLTableAlias($this->class->getTableName());
1169
1170 41
        $conditionSql = ($criteria instanceof Criteria)
1171 25
            ? $this->getSelectConditionCriteriaSQL($criteria)
1172 41
            : $this->getSelectConditionSQL($criteria);
1173
1174 41
        $filterSql = $this->generateFilterConditionSQL($this->class, $tableAlias);
1175
1176 41
        if ($filterSql !== '') {
1177 2
            $conditionSql = $conditionSql
1178 2
                ? $conditionSql . ' AND ' . $filterSql
1179 2
                : $filterSql;
1180
        }
1181
1182
        $sql = 'SELECT COUNT(*) '
1183 41
            . 'FROM ' . $tableName . ' ' . $tableAlias
1184 41
            . (empty($conditionSql) ? '' : ' WHERE ' . $conditionSql);
1185
1186 41
        return $sql;
1187
    }
1188
1189
    /**
1190
     * Gets the ORDER BY SQL snippet for ordered collections.
1191
     *
1192
     * @param mixed[] $orderBy
1193
     * @param string  $baseTableAlias
1194
     *
1195
     * @return string
1196
     *
1197
     * @throws \Doctrine\ORM\ORMException
1198
     */
1199 79
    final protected function getOrderBySQL(array $orderBy, $baseTableAlias)
1200
    {
1201 79
        if (! $orderBy) {
1202 67
            return '';
1203
        }
1204
1205 12
        $orderByList = [];
1206
1207 12
        foreach ($orderBy as $fieldName => $orientation) {
1208 12
            $orientation = strtoupper(trim($orientation));
1209
1210 12
            if (! in_array($orientation, ['ASC', 'DESC'])) {
1211 1
                throw ORMException::invalidOrientation($this->class->getClassName(), $fieldName);
1212
            }
1213
1214 11
            $property = $this->class->getProperty($fieldName);
1215
1216 11
            if ($property instanceof FieldMetadata) {
1217 9
                $tableAlias = $this->getSQLTableAlias($property->getTableName());
1218 9
                $columnName = $this->platform->quoteIdentifier($property->getColumnName());
1219
1220 9
                $orderByList[] = $tableAlias . '.' . $columnName . ' ' . $orientation;
1221
1222 9
                continue;
1223 2
            } elseif ($property instanceof AssociationMetadata) {
1224 2
                if (! $property->isOwningSide()) {
1225 1
                    throw ORMException::invalidFindByInverseAssociation($this->class->getClassName(), $fieldName);
1226
                }
1227
1228 1
                $class      = $this->class->isInheritedProperty($fieldName)
1229
                    ? $property->getDeclaringClass()
1230 1
                    : $this->class;
1231 1
                $tableAlias = $this->getSQLTableAlias($class->getTableName());
1232
1233 1
                foreach ($property->getJoinColumns() as $joinColumn) {
1234
                    /* @var JoinColumnMetadata $joinColumn */
1235 1
                    $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName());
1236
1237 1
                    $orderByList[] = $tableAlias . '.' . $quotedColumnName . ' ' . $orientation;
1238
                }
1239
1240 1
                continue;
1241
            }
1242
1243
            throw ORMException::unrecognizedField($fieldName);
1244
        }
1245
1246 10
        return ' ORDER BY ' . implode(', ', $orderByList);
1247
    }
1248
1249
    /**
1250
     * Gets the SQL fragment with the list of columns to select when querying for
1251
     * an entity in this persister.
1252
     *
1253
     * Subclasses should override this method to alter or change the select column
1254
     * list SQL fragment. Note that in the implementation of BasicEntityPersister
1255
     * the resulting SQL fragment is generated only once and cached in {@link selectColumnListSql}.
1256
     * Subclasses may or may not do the same.
1257
     *
1258
     * @return string The SQL fragment.
1259
     */
1260 518
    protected function getSelectColumnsSQL()
1261
    {
1262 518
        if ($this->currentPersisterContext->selectColumnListSql !== null) {
1263 100
            return $this->currentPersisterContext->selectColumnListSql;
1264
        }
1265
1266 518
        $this->currentPersisterContext->rsm->addEntityResult($this->class->getClassName(), 'r'); // r for root
1267 518
        $this->currentPersisterContext->selectJoinSql = '';
1268
1269 518
        $eagerAliasCounter = 0;
1270 518
        $columnList        = [];
1271
1272 518
        foreach ($this->class->getDeclaredPropertiesIterator() as $fieldName => $property) {
1273
            switch (true) {
1274 518
                case ($property instanceof FieldMetadata):
1275 516
                    $columnList[] = $this->getSelectColumnSQL($fieldName, $this->class);
1276 516
                    break;
1277
1278 465
                case ($property instanceof AssociationMetadata):
1279 461
                    $assocColumnSQL = $this->getSelectColumnAssociationSQL($fieldName, $property, $this->class);
1280
1281 461
                    if ($assocColumnSQL) {
1282 390
                        $columnList[] = $assocColumnSQL;
1283
                    }
1284
1285 461
                    $isAssocToOneInverseSide = $property instanceof ToOneAssociationMetadata && ! $property->isOwningSide();
1286 461
                    $isAssocFromOneEager     = ! $property instanceof ManyToManyAssociationMetadata && $property->getFetchMode() === FetchMode::EAGER;
1287
1288 461
                    if (! ($isAssocFromOneEager || $isAssocToOneInverseSide)) {
1289 439
                        break;
1290
                    }
1291
1292 178
                    if ($property instanceof ToManyAssociationMetadata && $this->currentPersisterContext->handlesLimits) {
1293 3
                        break;
1294
                    }
1295
1296 175
                    $targetEntity = $property->getTargetEntity();
1297 175
                    $eagerEntity  = $this->em->getClassMetadata($targetEntity);
1298
1299 175
                    if ($eagerEntity->inheritanceType !== InheritanceType::NONE) {
1300 5
                        break; // now this is why you shouldn't use inheritance
1301
                    }
1302
1303 170
                    $assocAlias = 'e' . ($eagerAliasCounter++);
1304
1305 170
                    $this->currentPersisterContext->rsm->addJoinedEntityResult($targetEntity, $assocAlias, 'r', $fieldName);
1306
1307 170
                    foreach ($eagerEntity->getDeclaredPropertiesIterator() as $eagerProperty) {
1308
                        switch (true) {
1309 170
                            case ($eagerProperty instanceof FieldMetadata):
1310 168
                                $columnList[] = $this->getSelectColumnSQL($eagerProperty->getName(), $eagerEntity, $assocAlias);
1311 168
                                break;
1312
1313 167
                            case ($eagerProperty instanceof ToOneAssociationMetadata && $eagerProperty->isOwningSide()):
1314 164
                                $columnList[] = $this->getSelectColumnAssociationSQL(
1315 164
                                    $eagerProperty->getName(),
1316 164
                                    $eagerProperty,
1317 164
                                    $eagerEntity,
0 ignored issues
show
$eagerEntity of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Persisters\...tColumnAssociationSQL(). ( Ignorable by Annotation )

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

1317
                                    /** @scrutinizer ignore-type */ $eagerEntity,
Loading history...
1318 164
                                    $assocAlias
1319
                                );
1320 170
                                break;
1321
                        }
1322
                    }
1323
1324 170
                    $owningAssociation = $property;
1325 170
                    $joinCondition     = [];
1326
1327 170
                    if ($property instanceof ToManyAssociationMetadata && $property->getIndexedBy()) {
1328 1
                        $this->currentPersisterContext->rsm->addIndexBy($assocAlias, $property->getIndexedBy());
1329
                    }
1330
1331 170
                    if (! $property->isOwningSide()) {
1332 163
                        $owningAssociation = $eagerEntity->getProperty($property->getMappedBy());
1333
                    }
1334
1335 170
                    $joinTableAlias = $this->getSQLTableAlias($eagerEntity->getTableName(), $assocAlias);
1336 170
                    $joinTableName  = $eagerEntity->table->getQuotedQualifiedName($this->platform);
1337
1338 170
                    $this->currentPersisterContext->selectJoinSql .= ' ' . $this->getJoinSQLForAssociation($property);
1339
1340 170
                    $sourceClass      = $this->em->getClassMetadata($owningAssociation->getSourceEntity());
1341 170
                    $targetClass      = $this->em->getClassMetadata($owningAssociation->getTargetEntity());
1342 170
                    $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName(), $property->isOwningSide() ? $assocAlias : '');
1343 170
                    $sourceTableAlias = $this->getSQLTableAlias($sourceClass->getTableName(), $property->isOwningSide() ? '' : $assocAlias);
1344
1345 170
                    foreach ($owningAssociation->getJoinColumns() as $joinColumn) {
1346 170
                        $joinCondition[] = sprintf(
1347 170
                            '%s.%s = %s.%s',
1348 170
                            $sourceTableAlias,
1349 170
                            $this->platform->quoteIdentifier($joinColumn->getColumnName()),
1350 170
                            $targetTableAlias,
1351 170
                            $this->platform->quoteIdentifier($joinColumn->getReferencedColumnName())
1352
                        );
1353
                    }
1354
1355 170
                    $filterSql = $this->generateFilterConditionSQL($eagerEntity, $targetTableAlias);
1356
1357
                    // Add filter SQL
1358 170
                    if ($filterSql) {
1359
                        $joinCondition[] = $filterSql;
1360
                    }
1361
1362 170
                    $this->currentPersisterContext->selectJoinSql .= ' ' . $joinTableName . ' ' . $joinTableAlias . ' ON ';
1363 170
                    $this->currentPersisterContext->selectJoinSql .= implode(' AND ', $joinCondition);
1364
1365 518
                    break;
1366
            }
1367
        }
1368
1369 518
        $this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList);
1370
1371 518
        return $this->currentPersisterContext->selectColumnListSql;
1372
    }
1373
1374
    /**
1375
     * Gets the SQL join fragment used when selecting entities from an association.
1376
     *
1377
     * @param string $field
1378
     * @param string $alias
1379
     *
1380
     * @return string
1381
     */
1382 461
    protected function getSelectColumnAssociationSQL($field, AssociationMetadata $association, ClassMetadata $class, $alias = 'r')
1383
    {
1384 461
        if (! ($association->isOwningSide() && $association instanceof ToOneAssociationMetadata)) {
1385 370
            return '';
1386
        }
1387
1388 407
        $columnList    = [];
1389 407
        $targetClass   = $this->em->getClassMetadata($association->getTargetEntity());
1390 407
        $sqlTableAlias = $this->getSQLTableAlias($class->getTableName(), ($alias === 'r' ? '' : $alias));
1391
1392 407
        foreach ($association->getJoinColumns() as $joinColumn) {
1393
            /** @var JoinColumnMetadata $joinColumn */
1394 407
            $columnName           = $joinColumn->getColumnName();
1395 407
            $quotedColumnName     = $this->platform->quoteIdentifier($columnName);
1396 407
            $referencedColumnName = $joinColumn->getReferencedColumnName();
1397 407
            $resultColumnName     = $this->getSQLColumnAlias();
1398
1399 407
            if (! $joinColumn->getType()) {
1400 9
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
1401
            }
1402
1403 407
            $this->currentPersisterContext->rsm->addMetaResult(
1404 407
                $alias,
1405 407
                $resultColumnName,
1406 407
                $columnName,
1407 407
                $association->isPrimaryKey(),
1408 407
                $joinColumn->getType()
1409
            );
1410
1411 407
            $columnList[] = sprintf('%s.%s AS %s', $sqlTableAlias, $quotedColumnName, $resultColumnName);
1412
        }
1413
1414 407
        return implode(', ', $columnList);
1415
    }
1416
1417
    /**
1418
     * Gets the SQL join fragment used when selecting entities from a
1419
     * many-to-many association.
1420
     *
1421
     * @param ManyToManyAssociationMetadata $manyToMany
1422
     *
1423
     * @return string
1424
     */
1425 83
    protected function getSelectManyToManyJoinSQL(ManyToManyAssociationMetadata $association)
1426
    {
1427 83
        $conditions        = [];
1428 83
        $owningAssociation = $association;
1429 83
        $sourceTableAlias  = $this->getSQLTableAlias($this->class->getTableName());
1430
1431 83
        if (! $association->isOwningSide()) {
1432 13
            $targetEntity      = $this->em->getClassMetadata($association->getTargetEntity());
1433 13
            $owningAssociation = $targetEntity->getProperty($association->getMappedBy());
1434
        }
1435
1436 83
        $joinTable     = $owningAssociation->getJoinTable();
1437 83
        $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
1438 83
        $joinColumns   = $association->isOwningSide()
1439 75
            ? $joinTable->getInverseJoinColumns()
1440 83
            : $joinTable->getJoinColumns()
1441
        ;
1442
1443 83
        foreach ($joinColumns as $joinColumn) {
1444 83
            $conditions[] = sprintf(
1445 83
                '%s.%s = %s.%s',
1446 83
                $sourceTableAlias,
1447 83
                $this->platform->quoteIdentifier($joinColumn->getReferencedColumnName()),
1448 83
                $joinTableName,
1449 83
                $this->platform->quoteIdentifier($joinColumn->getColumnName())
1450
            );
1451
        }
1452
1453 83
        return ' INNER JOIN ' . $joinTableName . ' ON ' . implode(' AND ', $conditions);
1454
    }
1455
1456
    /**
1457
     * {@inheritdoc}
1458
     */
1459 997
    public function getInsertSQL()
1460
    {
1461 997
        if ($this->insertSql !== null) {
1462 660
            return $this->insertSql;
1463
        }
1464
1465 997
        $columns   = $this->getInsertColumnList();
1466 997
        $tableName = $this->class->table->getQuotedQualifiedName($this->platform);
1467
1468 997
        if (empty($columns)) {
1469 104
            $property       = $this->class->getProperty($this->class->identifier[0]);
1470 104
            $identityColumn = $this->platform->quoteIdentifier($property->getColumnName());
1471
1472 104
            $this->insertSql = $this->platform->getEmptyIdentityInsertSQL($tableName, $identityColumn);
1473
1474 104
            return $this->insertSql;
1475
        }
1476
1477 975
        $quotedColumns = [];
1478 975
        $values        = [];
1479
1480 975
        foreach ($columns as $columnName) {
1481 975
            $column = $this->columns[$columnName];
1482
1483 975
            $quotedColumns[] = $this->platform->quoteIdentifier($column->getColumnName());
1484 975
            $values[]        = $column->getType()->convertToDatabaseValueSQL('?', $this->platform);
1485
        }
1486
1487 975
        $quotedColumns = implode(', ', $quotedColumns);
1488 975
        $values        = implode(', ', $values);
1489
1490 975
        $this->insertSql = sprintf('INSERT INTO %s (%s) VALUES (%s)', $tableName, $quotedColumns, $values);
1491
1492 975
        return $this->insertSql;
1493
    }
1494
1495
    /**
1496
     * Gets the list of columns to put in the INSERT SQL statement.
1497
     *
1498
     * Subclasses should override this method to alter or change the list of
1499
     * columns placed in the INSERT statements used by the persister.
1500
     *
1501
     * @return string[] The list of columns.
1502
     */
1503 913
    protected function getInsertColumnList()
1504
    {
1505 913
        $columns             = [];
1506 913
        $versionPropertyName = $this->class->isVersioned()
1507 196
            ? $this->class->versionProperty->getName()
1508 913
            : null
1509
        ;
1510
1511 913
        foreach ($this->class->getDeclaredPropertiesIterator() as $name => $property) {
1512
            /*if (isset($this->class->embeddedClasses[$name])) {
1513
                continue;
1514
            }*/
1515
1516
            switch (true) {
1517 913
                case ($property instanceof VersionFieldMetadata):
1518
                    // Do nothing
1519 196
                    break;
1520
1521 913
                case ($property instanceof LocalColumnMetadata):
1522 913
                    if (($property instanceof FieldMetadata
1523
                            && (
1524 913
                                ! $property->hasValueGenerator()
1525 913
                                || $property->getValueGenerator()->getType() !== GeneratorType::IDENTITY
1526
                            )
1527
                        )
1528 913
                        || $this->class->identifier[0] !== $name
1529
                    ) {
1530 850
                        $columnName = $property->getColumnName();
1531
1532 850
                        $columns[] = $columnName;
1533
1534 850
                        $this->columns[$columnName] = $property;
1535
                    }
1536
1537 913
                    break;
1538
1539 807
                case ($property instanceof AssociationMetadata):
1540 803
                    if ($property->isOwningSide() && $property instanceof ToOneAssociationMetadata) {
1541 761
                        $targetClass = $this->em->getClassMetadata($property->getTargetEntity());
1542
1543 761
                        foreach ($property->getJoinColumns() as $joinColumn) {
1544
                            /** @var JoinColumnMetadata $joinColumn */
1545 761
                            $columnName           = $joinColumn->getColumnName();
1546 761
                            $referencedColumnName = $joinColumn->getReferencedColumnName();
1547
1548 761
                            if (! $joinColumn->getType()) {
1549 116
                                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
1550
                            }
1551
1552 761
                            $columns[] = $columnName;
1553
1554 761
                            $this->columns[$columnName] = $joinColumn;
1555
                        }
1556
                    }
1557
1558 913
                    break;
1559
            }
1560
        }
1561
1562 913
        return $columns;
1563
    }
1564
1565
    /**
1566
     * Gets the SQL snippet of a qualified column name for the given field name.
1567
     *
1568
     * @param string        $field The field name.
1569
     * @param ClassMetadata $class The class that declares this field. The table this class is
1570
     *                             mapped to must own the column for the given field.
1571
     * @param string        $alias
1572
     *
1573
     * @return string
1574
     */
1575 552
    protected function getSelectColumnSQL($field, ClassMetadata $class, $alias = 'r')
1576
    {
1577 552
        $property    = $class->getProperty($field);
1578 552
        $columnAlias = $this->getSQLColumnAlias();
1579 552
        $sql         = sprintf(
1580 552
            '%s.%s',
1581 552
            $this->getSQLTableAlias($property->getTableName(), ($alias === 'r' ? '' : $alias)),
1582 552
            $this->platform->quoteIdentifier($property->getColumnName())
1583
        );
1584
1585 552
        $this->currentPersisterContext->rsm->addFieldResult($alias, $columnAlias, $field, $class->getClassName());
1586
1587 552
        return $property->getType()->convertToPHPValueSQL($sql, $this->platform) . ' AS ' . $columnAlias;
1588
    }
1589
1590
    /**
1591
     * Gets the SQL table alias for the given class name.
1592
     *
1593
     * @param string $tableName
1594
     * @param string $assocName
1595
     *
1596
     * @return string The SQL table alias.
1597
     */
1598 586
    protected function getSQLTableAlias($tableName, $assocName = '')
1599
    {
1600 586
        if ($tableName) {
1601 586
            $tableName .= '#' . $assocName;
1602
        }
1603
1604 586
        if (isset($this->currentPersisterContext->sqlTableAliases[$tableName])) {
1605 578
            return $this->currentPersisterContext->sqlTableAliases[$tableName];
1606
        }
1607
1608 586
        $tableAlias = 't' . $this->currentPersisterContext->sqlAliasCounter++;
1609
1610 586
        $this->currentPersisterContext->sqlTableAliases[$tableName] = $tableAlias;
1611
1612 586
        return $tableAlias;
1613
    }
1614
1615
    /**
1616
     * {@inheritdoc}
1617
     */
1618
    public function lock(array $criteria, $lockMode)
1619
    {
1620
        $lockSql      = '';
1621
        $conditionSql = $this->getSelectConditionSQL($criteria);
1622
1623
        switch ($lockMode) {
1624
            case LockMode::PESSIMISTIC_READ:
1625
                $lockSql = $this->platform->getReadLockSQL();
1626
1627
                break;
1628
            case LockMode::PESSIMISTIC_WRITE:
1629
                $lockSql = $this->platform->getWriteLockSQL();
1630
                break;
1631
        }
1632
1633
        $lock  = $this->getLockTablesSql($lockMode);
1634
        $where = ($conditionSql ? ' WHERE ' . $conditionSql : '') . ' ';
1635
        $sql   = 'SELECT 1 '
1636
             . $lock
1637
             . $where
1638
             . $lockSql;
1639
1640
        list($params, $types) = $this->expandParameters($criteria);
1641
1642
        $this->conn->executeQuery($sql, $params, $types);
1643
    }
1644
1645
    /**
1646
     * Gets the FROM and optionally JOIN conditions to lock the entity managed by this persister.
1647
     *
1648
     * @param int $lockMode One of the Doctrine\DBAL\LockMode::* constants.
1649
     *
1650
     * @return string
1651
     */
1652 13
    protected function getLockTablesSql($lockMode)
1653
    {
1654 13
        $tableName = $this->class->table->getQuotedQualifiedName($this->platform);
1655
1656 13
        return $this->platform->appendLockHint(
1657 13
            'FROM ' . $tableName . ' ' . $this->getSQLTableAlias($this->class->getTableName()),
1658 13
            $lockMode
1659
        );
1660
    }
1661
1662
    /**
1663
     * Gets the Select Where Condition from a Criteria object.
1664
     *
1665
     * @return string
1666
     */
1667 39
    protected function getSelectConditionCriteriaSQL(Criteria $criteria)
1668
    {
1669 39
        $expression = $criteria->getWhereExpression();
1670
1671 39
        if ($expression === null) {
1672 2
            return '';
1673
        }
1674
1675 38
        $visitor = new SqlExpressionVisitor($this, $this->class);
1676
1677 38
        return $visitor->dispatch($expression);
1678
    }
1679
1680
    /**
1681
     * {@inheritdoc}
1682
     */
1683 565
    public function getSelectConditionStatementSQL(
1684
        $field,
1685
        $value,
1686
        ?AssociationMetadata $association = null,
1687
        $comparison = null
1688
    ) {
1689 565
        $selectedColumns = [];
1690 565
        $columns         = $this->getSelectConditionStatementColumnSQL($field, $association);
1691
1692 561
        if (in_array($comparison, [Comparison::IN, Comparison::NIN]) && isset($columns[1])) {
1693
            // @todo try to support multi-column IN expressions. Example: (col1, col2) IN (('val1A', 'val2A'), ...)
1694 1
            throw ORMException::cantUseInOperatorOnCompositeKeys();
1695
        }
1696
1697 560
        foreach ($columns as $column) {
1698 560
            $property    = $this->class->getProperty($field);
1699 560
            $placeholder = '?';
1700
1701 560
            if ($property instanceof FieldMetadata) {
1702 472
                $placeholder = $property->getType()->convertToDatabaseValueSQL($placeholder, $this->platform);
1703
            }
1704
1705 560
            if ($comparison !== null) {
1706
                // special case null value handling
1707 42
                if (($comparison === Comparison::EQ || $comparison === Comparison::IS) && $value ===null) {
1708 6
                    $selectedColumns[] = $column . ' IS NULL';
1709
1710 6
                    continue;
1711
                }
1712
1713 36
                if ($comparison === Comparison::NEQ && $value === null) {
1714 3
                    $selectedColumns[] = $column . ' IS NOT NULL';
1715
1716 3
                    continue;
1717
                }
1718
1719 33
                $selectedColumns[] = $column . ' ' . sprintf(self::$comparisonMap[$comparison], $placeholder);
1720
1721 33
                continue;
1722
            }
1723
1724 533
            if (is_array($value)) {
1725 14
                $in = sprintf('%s IN (%s)', $column, $placeholder);
1726
1727 14
                if (array_search(null, $value, true) !== false) {
1728 4
                    $selectedColumns[] = sprintf('(%s OR %s IS NULL)', $in, $column);
1729
1730 4
                    continue;
1731
                }
1732
1733 10
                $selectedColumns[] = $in;
1734
1735 10
                continue;
1736
            }
1737
1738 522
            if ($value === null) {
1739 9
                $selectedColumns[] = sprintf('%s IS NULL', $column);
1740
1741 9
                continue;
1742
            }
1743
1744 514
            $selectedColumns[] = sprintf('%s = %s', $column, $placeholder);
1745
        }
1746
1747 560
        return implode(' AND ', $selectedColumns);
1748
    }
1749
1750
    /**
1751
     * Builds the left-hand-side of a where condition statement.
1752
     *
1753
     * @param string $field
1754
     *
1755
     * @return string[]
1756
     *
1757
     * @throws \Doctrine\ORM\ORMException
1758
     */
1759 565
    private function getSelectConditionStatementColumnSQL($field, ?AssociationMetadata $association = null)
1760
    {
1761 565
        $property = $this->class->getProperty($field);
1762
1763 565
        if ($property instanceof FieldMetadata) {
1764 472
            $tableAlias = $this->getSQLTableAlias($property->getTableName());
1765 472
            $columnName = $this->platform->quoteIdentifier($property->getColumnName());
1766
1767 472
            return [$tableAlias . '.' . $columnName];
1768
        }
1769
1770 281
        if ($property instanceof AssociationMetadata) {
1771 142
            $owningAssociation = $property;
1772 142
            $columns           = [];
1773
1774
            // Many-To-Many requires join table check for joinColumn
1775 142
            if ($owningAssociation instanceof ManyToManyAssociationMetadata) {
1776 3
                if (! $owningAssociation->isOwningSide()) {
1777 2
                    $owningAssociation = $association;
1778
                }
1779
1780 3
                $joinTable     = $owningAssociation->getJoinTable();
1781 3
                $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
1782 3
                $joinColumns   = $association->isOwningSide()
1783 2
                    ? $joinTable->getJoinColumns()
1784 3
                    : $joinTable->getInverseJoinColumns()
1785
                ;
1786
1787 3
                foreach ($joinColumns as $joinColumn) {
1788 3
                    $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName());
1789
1790 3
                    $columns[] = $joinTableName . '.' . $quotedColumnName;
1791
                }
1792
            } else {
1793 140
                if (! $owningAssociation->isOwningSide()) {
1794 1
                    throw ORMException::invalidFindByInverseAssociation($this->class->getClassName(), $field);
1795
                }
1796
1797 139
                $class      = $this->class->isInheritedProperty($field)
1798 11
                    ? $owningAssociation->getDeclaringClass()
1799 139
                    : $this->class
1800
                ;
1801 139
                $tableAlias = $this->getSQLTableAlias($class->getTableName());
1802
1803 139
                foreach ($owningAssociation->getJoinColumns() as $joinColumn) {
1804 139
                    $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName());
1805
1806 139
                    $columns[] = $tableAlias . '.' . $quotedColumnName;
1807
                }
1808
            }
1809
1810 141
            return $columns;
1811
        }
1812
1813 154
        if ($association !== null && strpos($field, ' ') === false && strpos($field, '(') === false) {
1814
            // very careless developers could potentially open up this normally hidden api for userland attacks,
1815
            // therefore checking for spaces and function calls which are not allowed.
1816
1817
            // found a join column condition, not really a "field"
1818 151
            return [$field];
1819
        }
1820
1821 3
        throw ORMException::unrecognizedField($field);
1822
    }
1823
1824
    /**
1825
     * Gets the conditional SQL fragment used in the WHERE clause when selecting
1826
     * entities in this persister.
1827
     *
1828
     * Subclasses are supposed to override this method if they intend to change
1829
     * or alter the criteria by which entities are selected.
1830
     *
1831
     * @param mixed[] $criteria
1832
     *
1833
     * @return string
1834
     */
1835 559
    protected function getSelectConditionSQL(array $criteria, ?AssociationMetadata $association = null)
1836
    {
1837 559
        $conditions = [];
1838
1839 559
        foreach ($criteria as $field => $value) {
1840 535
            $conditions[] = $this->getSelectConditionStatementSQL($field, $value, $association);
1841
        }
1842
1843 556
        return implode(' AND ', $conditions);
1844
    }
1845
1846
    /**
1847
     * {@inheritdoc}
1848
     */
1849 5
    public function getOneToManyCollection(
1850
        OneToManyAssociationMetadata $association,
1851
        $sourceEntity,
1852
        $offset = null,
1853
        $limit = null
1854
    ) {
1855 5
        $this->switchPersisterContext($offset, $limit);
1856
1857 5
        $stmt = $this->getOneToManyStatement($association, $sourceEntity, $offset, $limit);
1858
1859 5
        return $this->loadArrayFromStatement($association, $stmt);
1860
    }
1861
1862
    /**
1863
     * {@inheritdoc}
1864
     */
1865 72
    public function loadOneToManyCollection(
1866
        OneToManyAssociationMetadata $association,
1867
        $sourceEntity,
1868
        PersistentCollection $collection
1869
    ) {
1870 72
        $stmt = $this->getOneToManyStatement($association, $sourceEntity);
1871
1872 72
        return $this->loadCollectionFromStatement($association, $stmt, $collection);
1873
    }
1874
1875
    /**
1876
     * Builds criteria and execute SQL statement to fetch the one to many entities from.
1877
     *
1878
     * @param object   $sourceEntity
1879
     * @param int|null $offset
1880
     * @param int|null $limit
1881
     *
1882
     * @return \Doctrine\DBAL\Statement
1883
     */
1884 77
    private function getOneToManyStatement(
1885
        OneToManyAssociationMetadata $association,
1886
        $sourceEntity,
1887
        $offset = null,
1888
        $limit = null
1889
    ) {
1890 77
        $this->switchPersisterContext($offset, $limit);
1891
1892 77
        $criteria    = [];
1893 77
        $parameters  = [];
1894 77
        $owningAssoc = $this->class->getProperty($association->getMappedBy());
1895 77
        $sourceClass = $this->em->getClassMetadata($association->getSourceEntity());
1896 77
        $class       = $owningAssoc->getDeclaringClass();
1897 77
        $tableAlias  = $this->getSQLTableAlias($class->getTableName());
1898
1899 77
        foreach ($owningAssoc->getJoinColumns() as $joinColumn) {
1900 77
            $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName());
1901 77
            $fieldName        = $sourceClass->fieldNames[$joinColumn->getReferencedColumnName()];
1902 77
            $property         = $sourceClass->getProperty($fieldName);
1903
1904 77
            if ($property instanceof FieldMetadata) {
1905 77
                $value = $property->getValue($sourceEntity);
1906 3
            } elseif ($property instanceof AssociationMetadata) {
1907 3
                $targetClass = $this->em->getClassMetadata($property->getTargetEntity());
1908 3
                $value       = $property->getValue($sourceEntity);
1909
1910 3
                $value = $this->em->getUnitOfWork()->getEntityIdentifier($value);
1911 3
                $value = $value[$targetClass->identifier[0]];
1912
            }
1913
1914 77
            $criteria[$tableAlias . '.' . $quotedColumnName] = $value;
1915 77
            $parameters[]                                    = [
1916 77
                'value' => $value,
1917 77
                'field' => $fieldName,
1918 77
                'class' => $sourceClass,
1919
            ];
1920
        }
1921
1922 77
        $sql                  = $this->getSelectSQL($criteria, $association, null, $limit, $offset);
1923 77
        list($params, $types) = $this->expandToManyParameters($parameters);
1924
1925 77
        return $this->conn->executeQuery($sql, $params, $types);
1926
    }
1927
1928
    /**
1929
     * {@inheritdoc}
1930
     */
1931 536
    public function expandParameters($criteria)
1932
    {
1933 536
        $params = [];
1934 536
        $types  = [];
1935
1936 536
        foreach ($criteria as $field => $value) {
1937 512
            if ($value === null) {
1938 3
                continue; // skip null values.
1939
            }
1940
1941 510
            $types  = array_merge($types, $this->getTypes($field, $value, $this->class));
1942 510
            $params = array_merge($params, $this->getValues($value));
1943
        }
1944
1945 536
        return [$params, $types];
1946
    }
1947
1948
    /**
1949
     * Expands the parameters from the given criteria and use the correct binding types if found,
1950
     * specialized for OneToMany or ManyToMany associations.
1951
     *
1952
     * @param mixed[][] $criteria an array of arrays containing following:
1953
     *                             - field to which each criterion will be bound
1954
     *                             - value to be bound
1955
     *                             - class to which the field belongs to
1956
     *
1957
     *
1958
     * @return mixed[][]
1959
     */
1960 147
    private function expandToManyParameters($criteria)
1961
    {
1962 147
        $params = [];
1963 147
        $types  = [];
1964
1965 147
        foreach ($criteria as $criterion) {
1966 147
            if ($criterion['value'] === null) {
1967 6
                continue; // skip null values.
1968
            }
1969
1970 141
            $types  = array_merge($types, $this->getTypes($criterion['field'], $criterion['value'], $criterion['class']));
1971 141
            $params = array_merge($params, $this->getValues($criterion['value']));
1972
        }
1973
1974 147
        return [$params, $types];
1975
    }
1976
1977
    /**
1978
     * Infers field types to be used by parameter type casting.
1979
     *
1980
     * @param string $field
1981
     * @param mixed  $value
1982
     *
1983
     * @return mixed[]
1984
     *
1985
     * @throws \Doctrine\ORM\Query\QueryException
1986
     */
1987 667
    private function getTypes($field, $value, ClassMetadata $class)
1988
    {
1989 667
        $property = $class->getProperty($field);
1990 667
        $types    = [];
1991
1992
        switch (true) {
1993 667
            case ($property instanceof FieldMetadata):
1994 610
                $types = array_merge($types, [$property->getType()]);
1995 610
                break;
1996
1997 141
            case ($property instanceof AssociationMetadata):
1998 140
                $class = $this->em->getClassMetadata($property->getTargetEntity());
1999
2000 140
                if (! $property->isOwningSide()) {
2001 2
                    $property = $class->getProperty($property->getMappedBy());
2002 2
                    $class    = $this->em->getClassMetadata($property->getTargetEntity());
2003
                }
2004
2005 140
                $joinColumns = $property instanceof ManyToManyAssociationMetadata
2006 3
                    ? $property->getJoinTable()->getInverseJoinColumns()
2007 140
                    : $property->getJoinColumns()
2008
                ;
2009
2010 140
                foreach ($joinColumns as $joinColumn) {
2011
                    /** @var JoinColumnMetadata $joinColumn */
2012 140
                    $referencedColumnName = $joinColumn->getReferencedColumnName();
2013
2014 140
                    if (! $joinColumn->getType()) {
2015 1
                        $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $class, $this->em));
2016
                    }
2017
2018 140
                    $types[] = $joinColumn->getType();
2019
                }
2020
2021 140
                break;
2022
2023
            default:
2024 1
                $types[] = null;
2025 1
                break;
2026
        }
2027
2028 667
        if (is_array($value)) {
2029 16
            return array_map(function ($type) {
2030 16
                return $type->getBindingType() + Connection::ARRAY_PARAM_OFFSET;
2031 16
            }, $types);
2032
        }
2033
2034 657
        return $types;
2035
    }
2036
2037
    /**
2038
     * Retrieves the parameters that identifies a value.
2039
     *
2040
     * @param mixed $value
2041
     *
2042
     * @return mixed[]
2043
     */
2044 542
    private function getValues($value)
2045
    {
2046 542
        if (is_array($value)) {
2047 16
            $newValue = [];
2048
2049 16
            foreach ($value as $itemValue) {
2050 16
                $newValue = array_merge($newValue, $this->getValues($itemValue));
2051
            }
2052
2053 16
            return [$newValue];
2054
        }
2055
2056 542
        $metadataFactory = $this->em->getMetadataFactory();
2057 542
        $unitOfWork      = $this->em->getUnitOfWork();
2058
2059 542
        if (is_object($value) && $metadataFactory->hasMetadataFor(StaticClassNameConverter::getClass($value))) {
2060 45
            $class     = $metadataFactory->getMetadataFor(get_class($value));
2061 45
            $persister = $unitOfWork->getEntityPersister($class->getClassName());
2062
2063 45
            if ($class->isIdentifierComposite()) {
2064 3
                $newValue = [];
2065
2066 3
                foreach ($persister->getIdentifier($value) as $innerValue) {
2067 3
                    $newValue = array_merge($newValue, $this->getValues($innerValue));
2068
                }
2069
2070 3
                return $newValue;
2071
            }
2072
        }
2073
2074 542
        return [$this->getIndividualValue($value)];
2075
    }
2076
2077
    /**
2078
     * Retrieves an individual parameter value.
2079
     *
2080
     * @param mixed $value
2081
     *
2082
     * @return mixed
2083
     */
2084 542
    private function getIndividualValue($value)
2085
    {
2086 542
        if (! is_object($value) || ! $this->em->getMetadataFactory()->hasMetadataFor(StaticClassNameConverter::getClass($value))) {
2087 540
            return $value;
2088
        }
2089
2090 45
        return $this->em->getUnitOfWork()->getSingleIdentifierValue($value);
2091
    }
2092
2093
    /**
2094
     * {@inheritdoc}
2095
     */
2096 14
    public function exists($entity, ?Criteria $extraConditions = null)
2097
    {
2098 14
        $criteria = $this->getIdentifier($entity);
2099
2100 14
        if (! $criteria) {
2101 2
            return false;
2102
        }
2103
2104 13
        $alias = $this->getSQLTableAlias($this->class->getTableName());
2105
2106
        $sql = 'SELECT 1 '
2107 13
             . $this->getLockTablesSql(null)
2108 13
             . ' WHERE ' . $this->getSelectConditionSQL($criteria);
2109
2110 13
        list($params, $types) = $this->expandParameters($criteria);
2111
2112 13
        if ($extraConditions !== null) {
2113 9
            $sql                                 .= ' AND ' . $this->getSelectConditionCriteriaSQL($extraConditions);
2114 9
            list($criteriaParams, $criteriaTypes) = $this->expandCriteriaParameters($extraConditions);
2115
2116 9
            $params = array_merge($params, $criteriaParams);
2117 9
            $types  = array_merge($types, $criteriaTypes);
2118
        }
2119
2120 13
        $filterSql = $this->generateFilterConditionSQL($this->class, $alias);
2121
2122 13
        if ($filterSql) {
2123 3
            $sql .= ' AND ' . $filterSql;
2124
        }
2125
2126 13
        return (bool) $this->conn->fetchColumn($sql, $params, 0, $types);
2127
    }
2128
2129
    /**
2130
     * Generates the appropriate join SQL for the given association.
2131
     *
2132
     * @return string LEFT JOIN if one of the columns is nullable, INNER JOIN otherwise.
2133
     */
2134 170
    protected function getJoinSQLForAssociation(AssociationMetadata $association)
2135
    {
2136 170
        if (! $association->isOwningSide()) {
2137 163
            return 'LEFT JOIN';
2138
        }
2139
2140
        // if one of the join columns is nullable, return left join
2141 13
        foreach ($association->getJoinColumns() as $joinColumn) {
2142 13
            if (! $joinColumn->isNullable()) {
2143 5
                continue;
2144
            }
2145
2146 11
            return 'LEFT JOIN';
2147
        }
2148
2149 5
        return 'INNER JOIN';
2150
    }
2151
2152
    /**
2153
     * Gets an SQL column alias for a column name.
2154
     *
2155
     * @return string
2156
     */
2157 553
    public function getSQLColumnAlias()
2158
    {
2159 553
        return $this->platform->getSQLResultCasing('c' . $this->currentPersisterContext->sqlAliasCounter++);
2160
    }
2161
2162
    /**
2163
     * Generates the filter SQL for a given entity and table alias.
2164
     *
2165
     * @param ClassMetadata $targetEntity     Metadata of the target entity.
2166
     * @param string        $targetTableAlias The table alias of the joined/selected table.
2167
     *
2168
     * @return string The SQL query part to add to a query.
2169
     */
2170 577
    protected function generateFilterConditionSQL(ClassMetadata $targetEntity, $targetTableAlias)
2171
    {
2172 577
        $filterClauses = [];
2173
2174 577
        foreach ($this->em->getFilters()->getEnabledFilters() as $filter) {
2175 22
            $filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias);
2176
2177 22
            if ($filterExpr !== '') {
2178 22
                $filterClauses[] = '(' . $filterExpr . ')';
2179
            }
2180
        }
2181
2182 577
        $sql = implode(' AND ', $filterClauses);
2183
2184 577
        return $sql ? '(' . $sql . ')' : ''; // Wrap again to avoid "X or Y and FilterConditionSQL"
2185
    }
2186
2187
    /**
2188
     * Switches persister context according to current query offset/limits
2189
     *
2190
     * This is due to the fact that to-many associations cannot be fetch-joined when a limit is involved
2191
     *
2192
     * @param int|null $offset
2193
     * @param int|null $limit
2194
     */
2195 559
    protected function switchPersisterContext($offset, $limit)
2196
    {
2197 559
        if ($offset === null && $limit === null) {
2198 546
            $this->currentPersisterContext = $this->noLimitsContext;
2199
2200 546
            return;
2201
        }
2202
2203 41
        $this->currentPersisterContext = $this->limitsHandlingContext;
2204 41
    }
2205
}
2206