Failed Conditions
Push — master ( 2ccf23...d791f7 )
by Michael
10:40
created

getSelectConditionStatementSQL()   C

Complexity

Conditions 14
Paths 16

Size

Total Lines 65
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 31
CRAP Score 14

Importance

Changes 0
Metric Value
cc 14
eloc 30
nc 16
nop 4
dl 0
loc 65
ccs 31
cts 31
cp 1
crap 14
rs 5.9509
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
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 1125
    public function __construct(EntityManagerInterface $em, ClassMetadata $class)
157
    {
158 1125
        $this->em                    = $em;
159 1125
        $this->class                 = $class;
160 1125
        $this->conn                  = $em->getConnection();
161 1125
        $this->platform              = $this->conn->getDatabasePlatform();
162 1125
        $this->noLimitsContext       = $this->currentPersisterContext = new CachedPersisterContext(
163 1125
            $class,
164 1125
            new Query\ResultSetMapping(),
165 1125
            false
166
        );
167 1125
        $this->limitsHandlingContext = new CachedPersisterContext(
168 1125
            $class,
169 1125
            new Query\ResultSetMapping(),
170 1125
            true
171
        );
172 1125
    }
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 1045
    public function getIdentifier($entity) : array
194
    {
195 1045
        $id = [];
196
197 1045
        foreach ($this->class->getIdentifier() as $fieldName) {
198 1045
            $property = $this->class->getProperty($fieldName);
199 1045
            $value    = $property->getValue($entity);
200
201 1045
            if ($value !== null) {
202 1045
                $id[$fieldName] = $value;
203
            }
204
        }
205
206 1045
        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 908
    public function insert($entity)
228
    {
229 908
        $stmt           = $this->conn->prepare($this->getInsertSQL());
230 908
        $tableName      = $this->class->getTableName();
231 908
        $insertData     = $this->prepareInsertData($entity);
232 908
        $generationPlan = $this->class->getValueGenerationPlan();
233
234 908
        if (isset($insertData[$tableName])) {
235 884
            $paramIndex = 1;
236
237 884
            foreach ($insertData[$tableName] as $columnName => $value) {
238 884
                $type = $this->columns[$columnName]->getType();
239
240 884
                $stmt->bindValue($paramIndex++, $value, $type);
241
            }
242
        }
243
244 908
        $stmt->execute();
245
246 907
        if ($generationPlan->containsDeferred()) {
247 822
            $generationPlan->executeDeferred($this->em, $entity);
248
        }
249
250 907
        if ($this->class->isVersioned()) {
251 196
            $this->assignDefaultVersionValue($entity, $this->getIdentifier($entity));
252
        }
253
254 907
        $stmt->closeCursor();
255 907
    }
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);
0 ignored issues
show
Unused Code Comprehensibility introduced by
42% 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...
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 992
    protected function prepareInsertData($entity) : array
599
    {
600 992
        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 997
    protected function prepareUpdateData($entity)
625
    {
626 997
        $uow                 = $this->em->getUnitOfWork();
627 997
        $result              = [];
628 997
        $versionPropertyName = $this->class->isVersioned()
629 209
            ? $this->class->versionProperty->getName()
630 997
            : null
631
        ;
632
633
        // @todo guilhermeblanco This should check column insertability/updateability instead of field changeset
634 997
        foreach ($uow->getEntityChangeSet($entity) as $propertyName => $propertyChangeSet) {
635 964
            if ($versionPropertyName === $propertyName) {
636
                continue;
637
            }
638
639 964
            $property = $this->class->getProperty($propertyName);
640 964
            $newValue = $propertyChangeSet[1];
641
642 964
            if ($property instanceof FieldMetadata) {
643
                // @todo guilhermeblanco Please remove this in the future for good...
644 931
                $this->columns[$property->getColumnName()] = $property;
645
646 931
                $result[$property->getTableName()][$property->getColumnName()] = $newValue;
647
648 931
                continue;
649
            }
650
651
            // Only owning side of x-1 associations can have a FK column.
652 828
            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 828
            if ($newValue !== null && $uow->isScheduledForInsert($newValue)) {
660 32
                $uow->scheduleExtraUpdate($entity, [$propertyName => [null, $newValue]]);
661
662 32
                $newValue = null;
663
            }
664
665 828
            $targetClass     = $this->em->getClassMetadata($property->getTargetEntity());
666 828
            $targetPersister = $uow->getEntityPersister($targetClass->getClassName());
667
668 828
            foreach ($property->getJoinColumns() as $joinColumn) {
669
                /** @var JoinColumnMetadata $joinColumn */
670 828
                $referencedColumnName = $joinColumn->getReferencedColumnName();
671
672 828
                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 828
                $this->columns[$joinColumn->getColumnName()] = $joinColumn;
678
679 828
                $result[$joinColumn->getTableName()][$joinColumn->getColumnName()] = $newValue !== null
680 611
                    ? $targetPersister->getColumnValue($newValue, $referencedColumnName)
681 828
                    : null
682
                ;
683
            }
684
        }
685
686 997
        return $result;
687
    }
688
689
    /**
690
     * @param object $entity
691
     *
692
     * @return mixed|null
693
     */
694 611
    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 611
        $propertyName = $this->class->fieldNames[$columnName];
698 611
        $property     = $this->class->getProperty($propertyName);
699
700 611
        if (! $property) {
701
            return null;
702
        }
703
704 611
        $propertyValue = $property->getValue($entity);
705
706 611
        if ($property instanceof LocalColumnMetadata) {
707 611
            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
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...
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 70
    public function loadAll(array $criteria = [], array $orderBy = [], $limit = null, $offset = null)
934
    {
935 70
        $this->switchPersisterContext($offset, $limit);
936
937 70
        $sql = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy);
938
939 66
        list($params, $types) = $this->expandParameters($criteria);
940
941 66
        $stmt         = $this->conn->executeQuery($sql, $params, $types);
942 66
        $rsm          = $this->currentPersisterContext->rsm;
943 66
        $hints        = [UnitOfWork::HINT_DEFEREAGERLOAD => true];
944 66
        $hydratorType = $this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT;
945 66
        $hydrator     = $this->em->newHydrator($hydratorType);
946
947 66
        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 523
    public function getSelectSQL(
1098
        $criteria,
1099
        ?AssociationMetadata $association = null,
1100
        $lockMode = null,
1101
        $limit = null,
1102
        $offset = null,
1103
        array $orderBy = []
1104
    ) {
1105 523
        $this->switchPersisterContext($offset, $limit);
1106
1107 523
        $lockSql    = '';
1108 523
        $joinSql    = '';
1109 523
        $orderBySql = '';
1110
1111 523
        if ($association instanceof ManyToManyAssociationMetadata) {
1112 81
            $joinSql = $this->getSelectManyToManyJoinSQL($association);
1113
        }
1114
1115 523
        if ($association instanceof ToManyAssociationMetadata && $association->getOrderBy()) {
1116 5
            $orderBy = $association->getOrderBy();
1117
        }
1118
1119 523
        if ($orderBy) {
1120 11
            $orderBySql = $this->getOrderBySQL($orderBy, $this->getSQLTableAlias($this->class->getTableName()));
1121
        }
1122
1123 521
        $conditionSql = ($criteria instanceof Criteria)
1124 8
            ? $this->getSelectConditionCriteriaSQL($criteria)
1125 519
            : $this->getSelectConditionSQL($criteria, $association);
1126
1127
        switch ($lockMode) {
1128 516
            case LockMode::PESSIMISTIC_READ:
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...
1129
                $lockSql = ' ' . $this->platform->getReadLockSQL();
1130
                break;
1131
1132 516
            case LockMode::PESSIMISTIC_WRITE:
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...
1133
                $lockSql = ' ' . $this->platform->getWriteLockSQL();
1134
                break;
1135
        }
1136
1137 516
        $columnList = $this->getSelectColumnsSQL();
1138 516
        $tableAlias = $this->getSQLTableAlias($this->class->getTableName());
1139 516
        $filterSql  = $this->generateFilterConditionSQL($this->class, $tableAlias);
1140 516
        $tableName  = $this->class->table->getQuotedQualifiedName($this->platform);
1141
1142 516
        if ($filterSql !== '') {
1143 12
            $conditionSql = $conditionSql
1144 11
                ? $conditionSql . ' AND ' . $filterSql
1145 12
                : $filterSql;
1146
        }
1147
1148 516
        $select = 'SELECT ' . $columnList;
1149 516
        $from   = ' FROM ' . $tableName . ' ' . $tableAlias;
1150 516
        $join   = $this->currentPersisterContext->selectJoinSql . $joinSql;
1151 516
        $where  = ($conditionSql ? ' WHERE ' . $conditionSql : '');
1152 516
        $lock   = $this->platform->appendLockHint($from, $lockMode);
1153
        $query  = $select
1154 516
            . $lock
1155 516
            . $join
1156 516
            . $where
1157 516
            . $orderBySql;
1158
1159 516
        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'], true)) {
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 517
    protected function getSelectColumnsSQL()
1261
    {
1262 517
        if ($this->currentPersisterContext->selectColumnListSql !== null) {
1263 100
            return $this->currentPersisterContext->selectColumnListSql;
1264
        }
1265
1266 517
        $this->currentPersisterContext->rsm->addEntityResult($this->class->getClassName(), 'r'); // r for root
1267 517
        $this->currentPersisterContext->selectJoinSql = '';
1268
1269 517
        $eagerAliasCounter = 0;
1270 517
        $columnList        = [];
1271
1272 517
        foreach ($this->class->getDeclaredPropertiesIterator() as $fieldName => $property) {
1273
            switch (true) {
1274 517
                case ($property instanceof FieldMetadata):
1275 515
                    $columnList[] = $this->getSelectColumnSQL($fieldName, $this->class);
1276 515
                    break;
1277
1278 464
                case ($property instanceof AssociationMetadata):
1279 460
                    $assocColumnSQL = $this->getSelectColumnAssociationSQL($fieldName, $property, $this->class);
1280
1281 460
                    if ($assocColumnSQL) {
1282 389
                        $columnList[] = $assocColumnSQL;
1283
                    }
1284
1285 460
                    $isAssocToOneInverseSide = $property instanceof ToOneAssociationMetadata && ! $property->isOwningSide();
1286 460
                    $isAssocFromOneEager     = ! $property instanceof ManyToManyAssociationMetadata && $property->getFetchMode() === FetchMode::EAGER;
1287
1288 460
                    if (! ($isAssocFromOneEager || $isAssocToOneInverseSide)) {
1289 438
                        break;
1290
                    }
1291
1292 177
                    if ($property instanceof ToManyAssociationMetadata && $this->currentPersisterContext->handlesLimits) {
1293 3
                        break;
1294
                    }
1295
1296 174
                    $targetEntity = $property->getTargetEntity();
1297 174
                    $eagerEntity  = $this->em->getClassMetadata($targetEntity);
1298
1299 174
                    if ($eagerEntity->inheritanceType !== InheritanceType::NONE) {
1300 5
                        break; // now this is why you shouldn't use inheritance
1301
                    }
1302
1303 169
                    $assocAlias = 'e' . ($eagerAliasCounter++);
1304
1305 169
                    $this->currentPersisterContext->rsm->addJoinedEntityResult($targetEntity, $assocAlias, 'r', $fieldName);
1306
1307 169
                    foreach ($eagerEntity->getDeclaredPropertiesIterator() as $eagerProperty) {
1308
                        switch (true) {
1309 169
                            case ($eagerProperty instanceof FieldMetadata):
1310 167
                                $columnList[] = $this->getSelectColumnSQL($eagerProperty->getName(), $eagerEntity, $assocAlias);
1311 167
                                break;
1312
1313 166
                            case ($eagerProperty instanceof ToOneAssociationMetadata && $eagerProperty->isOwningSide()):
1314 163
                                $columnList[] = $this->getSelectColumnAssociationSQL(
1315 163
                                    $eagerProperty->getName(),
1316 163
                                    $eagerProperty,
1317 163
                                    $eagerEntity,
1318 163
                                    $assocAlias
1319
                                );
1320 169
                                break;
1321
                        }
1322
                    }
1323
1324 169
                    $owningAssociation = $property;
1325 169
                    $joinCondition     = [];
1326
1327 169
                    if ($property instanceof ToManyAssociationMetadata && $property->getIndexedBy()) {
1328 1
                        $this->currentPersisterContext->rsm->addIndexBy($assocAlias, $property->getIndexedBy());
1329
                    }
1330
1331 169
                    if (! $property->isOwningSide()) {
1332 162
                        $owningAssociation = $eagerEntity->getProperty($property->getMappedBy());
1333
                    }
1334
1335 169
                    $joinTableAlias = $this->getSQLTableAlias($eagerEntity->getTableName(), $assocAlias);
1336 169
                    $joinTableName  = $eagerEntity->table->getQuotedQualifiedName($this->platform);
1337
1338 169
                    $this->currentPersisterContext->selectJoinSql .= ' ' . $this->getJoinSQLForAssociation($property);
1339
1340 169
                    $sourceClass      = $this->em->getClassMetadata($owningAssociation->getSourceEntity());
1341 169
                    $targetClass      = $this->em->getClassMetadata($owningAssociation->getTargetEntity());
1342 169
                    $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName(), $property->isOwningSide() ? $assocAlias : '');
1343 169
                    $sourceTableAlias = $this->getSQLTableAlias($sourceClass->getTableName(), $property->isOwningSide() ? '' : $assocAlias);
1344
1345 169
                    foreach ($owningAssociation->getJoinColumns() as $joinColumn) {
1346 169
                        $joinCondition[] = sprintf(
1347 169
                            '%s.%s = %s.%s',
1348 169
                            $sourceTableAlias,
1349 169
                            $this->platform->quoteIdentifier($joinColumn->getColumnName()),
1350 169
                            $targetTableAlias,
1351 169
                            $this->platform->quoteIdentifier($joinColumn->getReferencedColumnName())
1352
                        );
1353
                    }
1354
1355 169
                    $filterSql = $this->generateFilterConditionSQL($eagerEntity, $targetTableAlias);
1356
1357
                    // Add filter SQL
1358 169
                    if ($filterSql) {
1359
                        $joinCondition[] = $filterSql;
1360
                    }
1361
1362 169
                    $this->currentPersisterContext->selectJoinSql .= ' ' . $joinTableName . ' ' . $joinTableAlias . ' ON ';
1363 169
                    $this->currentPersisterContext->selectJoinSql .= implode(' AND ', $joinCondition);
1364
1365 517
                    break;
1366
            }
1367
        }
1368
1369 517
        $this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList);
1370
1371 517
        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 460
    protected function getSelectColumnAssociationSQL($field, AssociationMetadata $association, ClassMetadata $class, $alias = 'r')
1383
    {
1384 460
        if (! ($association->isOwningSide() && $association instanceof ToOneAssociationMetadata)) {
1385 369
            return '';
1386
        }
1387
1388 406
        $columnList    = [];
1389 406
        $targetClass   = $this->em->getClassMetadata($association->getTargetEntity());
1390 406
        $sqlTableAlias = $this->getSQLTableAlias($class->getTableName(), ($alias === 'r' ? '' : $alias));
1391
1392 406
        foreach ($association->getJoinColumns() as $joinColumn) {
1393
            /** @var JoinColumnMetadata $joinColumn */
1394 406
            $columnName           = $joinColumn->getColumnName();
1395 406
            $quotedColumnName     = $this->platform->quoteIdentifier($columnName);
1396 406
            $referencedColumnName = $joinColumn->getReferencedColumnName();
1397 406
            $resultColumnName     = $this->getSQLColumnAlias();
1398
1399 406
            if (! $joinColumn->getType()) {
1400 9
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
1401
            }
1402
1403 406
            $this->currentPersisterContext->rsm->addMetaResult(
1404 406
                $alias,
1405 406
                $resultColumnName,
1406 406
                $columnName,
1407 406
                $association->isPrimaryKey(),
1408 406
                $joinColumn->getType()
1409
            );
1410
1411 406
            $columnList[] = sprintf('%s.%s AS %s', $sqlTableAlias, $quotedColumnName, $resultColumnName);
1412
        }
1413
1414 406
        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 993
    public function getInsertSQL()
1460
    {
1461 993
        if ($this->insertSql !== null) {
1462 657
            return $this->insertSql;
1463
        }
1464
1465 993
        $columns   = $this->getInsertColumnList();
1466 993
        $tableName = $this->class->table->getQuotedQualifiedName($this->platform);
1467
1468 993
        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 971
        $quotedColumns = [];
1478 971
        $values        = [];
1479
1480 971
        foreach ($columns as $columnName) {
1481 971
            $column = $this->columns[$columnName];
1482
1483 971
            $quotedColumns[] = $this->platform->quoteIdentifier($column->getColumnName());
1484 971
            $values[]        = $column->getType()->convertToDatabaseValueSQL('?', $this->platform);
1485
        }
1486
1487 971
        $quotedColumns = implode(', ', $quotedColumns);
1488 971
        $values        = implode(', ', $values);
1489
1490 971
        $this->insertSql = sprintf('INSERT INTO %s (%s) VALUES (%s)', $tableName, $quotedColumns, $values);
1491
1492 971
        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 909
    protected function getInsertColumnList()
1504
    {
1505 909
        $columns             = [];
1506 909
        $versionPropertyName = $this->class->isVersioned()
1507 196
            ? $this->class->versionProperty->getName()
1508 909
            : null
1509
        ;
1510
1511 909
        foreach ($this->class->getDeclaredPropertiesIterator() as $name => $property) {
1512
            /*if (isset($this->class->embeddedClasses[$name])) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
73% 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...
1513
                continue;
1514
            }*/
1515
1516
            switch (true) {
1517 909
                case ($property instanceof VersionFieldMetadata):
1518
                    // Do nothing
1519 196
                    break;
1520
1521 909
                case ($property instanceof LocalColumnMetadata):
1522 909
                    if (($property instanceof FieldMetadata
1523
                            && (
1524 909
                                ! $property->hasValueGenerator()
1525 909
                                || $property->getValueGenerator()->getType() !== GeneratorType::IDENTITY
1526
                            )
1527
                        )
1528 909
                        || $this->class->identifier[0] !== $name
1529
                    ) {
1530 846
                        $columnName = $property->getColumnName();
1531
1532 846
                        $columns[] = $columnName;
1533
1534 846
                        $this->columns[$columnName] = $property;
1535
                    }
1536
1537 909
                    break;
1538
1539 803
                case ($property instanceof AssociationMetadata):
1540 799
                    if ($property->isOwningSide() && $property instanceof ToOneAssociationMetadata) {
1541 757
                        $targetClass = $this->em->getClassMetadata($property->getTargetEntity());
1542
1543 757
                        foreach ($property->getJoinColumns() as $joinColumn) {
1544
                            /** @var JoinColumnMetadata $joinColumn */
1545 757
                            $columnName           = $joinColumn->getColumnName();
1546 757
                            $referencedColumnName = $joinColumn->getReferencedColumnName();
1547
1548 757
                            if (! $joinColumn->getType()) {
1549 115
                                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
1550
                            }
1551
1552 757
                            $columns[] = $columnName;
1553
1554 757
                            $this->columns[$columnName] = $joinColumn;
1555
                        }
1556
                    }
1557
1558 909
                    break;
1559
            }
1560
        }
1561
1562 909
        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 551
    protected function getSelectColumnSQL($field, ClassMetadata $class, $alias = 'r')
1576
    {
1577 551
        $property    = $class->getProperty($field);
1578 551
        $columnAlias = $this->getSQLColumnAlias();
1579 551
        $sql         = sprintf(
1580 551
            '%s.%s',
1581 551
            $this->getSQLTableAlias($property->getTableName(), ($alias === 'r' ? '' : $alias)),
1582 551
            $this->platform->quoteIdentifier($property->getColumnName())
1583
        );
1584
1585 551
        $this->currentPersisterContext->rsm->addFieldResult($alias, $columnAlias, $field, $class->getClassName());
1586
1587 551
        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 585
    protected function getSQLTableAlias($tableName, $assocName = '')
1599
    {
1600 585
        if ($tableName) {
1601 585
            $tableName .= '#' . $assocName;
1602
        }
1603
1604 585
        if (isset($this->currentPersisterContext->sqlTableAliases[$tableName])) {
1605 577
            return $this->currentPersisterContext->sqlTableAliases[$tableName];
1606
        }
1607
1608 585
        $tableAlias = 't' . $this->currentPersisterContext->sqlAliasCounter++;
1609
1610 585
        $this->currentPersisterContext->sqlTableAliases[$tableName] = $tableAlias;
1611
1612 585
        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], true) && 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 558
    protected function getSelectConditionSQL(array $criteria, ?AssociationMetadata $association = null)
1836
    {
1837 558
        $conditions = [];
1838
1839 558
        foreach ($criteria as $field => $value) {
1840 535
            $conditions[] = $this->getSelectConditionStatementSQL($field, $value, $association);
1841
        }
1842
1843 555
        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 535
    public function expandParameters($criteria)
1932
    {
1933 535
        $params = [];
1934 535
        $types  = [];
1935
1936 535
        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 535
        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):
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...
1994 610
                $types = array_merge($types, [$property->getType()]);
1995 610
                break;
1996
1997 141
            case ($property instanceof AssociationMetadata):
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...
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:
0 ignored issues
show
Coding Style introduced by
DEFAULT statements must be defined using a colon

As per the PSR-2 coding standard, default statements should not be wrapped in curly braces.

switch ($expr) {
    default: { //wrong
        doSomething();
        break;
    }
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

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

Loading history...
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 169
    protected function getJoinSQLForAssociation(AssociationMetadata $association)
2135
    {
2136 169
        if (! $association->isOwningSide()) {
2137 162
            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 552
    public function getSQLColumnAlias()
2158
    {
2159 552
        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 576
    protected function generateFilterConditionSQL(ClassMetadata $targetEntity, $targetTableAlias)
2171
    {
2172 576
        $filterClauses = [];
2173
2174 576
        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 576
        $sql = implode(' AND ', $filterClauses);
2183
2184 576
        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 558
    protected function switchPersisterContext($offset, $limit)
2196
    {
2197 558
        if ($offset === null && $limit === null) {
2198 545
            $this->currentPersisterContext = $this->noLimitsContext;
2199
2200 545
            return;
2201
        }
2202
2203 41
        $this->currentPersisterContext = $this->limitsHandlingContext;
2204 41
    }
2205
}
2206