Failed Conditions
Push — master ( ddb3cd...4476ec )
by Marco
11:47
created

BasicEntityPersister   F

Complexity

Total Complexity 279

Size/Duplication

Total Lines 2133
Duplicated Lines 0 %

Test Coverage

Coverage 94.76%

Importance

Changes 0
Metric Value
dl 0
loc 2133
ccs 868
cts 916
cp 0.9476
rs 0.6314
c 0
b 0
f 0
wmc 279

58 Methods

Rating   Name   Duplication   Size   Complexity  
A loadOneToManyCollection() 0 8 1
B expandCriteriaParameters() 0 26 4
A getLockTablesSql() 0 7 1
C delete() 0 59 9
D deleteJoinTableRecords() 0 60 13
F getSelectColumnsSQL() 0 112 24
C loadToOneEntity() 0 74 11
B getCountSQL() 0 22 5
A prepareInsertData() 0 3 1
B getSelectManyToManyJoinSQL() 0 29 4
A extractIdentifierTypes() 0 9 2
A getResultSetMapping() 0 3 1
B exists() 0 31 4
C getInsertColumnList() 0 60 14
B fetchVersionValue() 0 28 1
B getOneToManyStatement() 0 42 4
A count() 0 9 2
A loadById() 0 3 1
A loadCollectionFromStatement() 0 16 2
A switchPersisterContext() 0 9 3
A getJoinSQLForAssociation() 0 16 4
C prepareUpdateData() 0 63 12
B getInsertSQL() 0 34 4
A getSQLTableAlias() 0 15 3
A getSelectConditionSQL() 0 9 2
A getIndividualValue() 0 7 3
A refresh() 0 8 1
A loadArrayFromStatement() 0 13 2
A getSQLColumnAlias() 0 3 1
A __construct() 0 15 1
A expandParameters() 0 15 3
B insert() 0 28 5
A getOneToManyCollection() 0 11 1
D getSelectSQL() 0 63 11
B getManyToManyStatement() 0 56 6
A getIdentifier() 0 14 3
B getSelectColumnAssociationSQL() 0 33 6
B lock() 0 25 4
B getColumnValue() 0 37 6
C getTypes() 0 48 8
A setIdentifier() 0 6 2
A expandToManyParameters() 0 15 3
B update() 0 24 4
A loadAll() 0 15 2
A getSelectConditionCriteriaSQL() 0 11 2
A getClassMetadata() 0 3 1
A assignDefaultVersionValue() 0 6 1
A getManyToManyCollection() 0 11 1
A generateFilterConditionSQL() 0 15 4
A loadManyToManyCollection() 0 8 1
B load() 0 26 4
A loadCriteria() 0 16 2
A getSelectColumnSQL() 0 13 2
C getValues() 0 31 7
C getSelectConditionStatementColumnSQL() 0 63 13
C getOrderBySQL() 0 48 9
D updateTable() 0 84 14
C getSelectConditionStatementSQL() 0 65 14

How to fix   Complexity   

Complex Class

Complex classes like BasicEntityPersister often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use BasicEntityPersister, and based on these observations, apply Extract Interface, too.

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 1040
    public function __construct(EntityManagerInterface $em, ClassMetadata $class)
157
    {
158 1040
        $this->em                    = $em;
159 1040
        $this->class                 = $class;
160 1040
        $this->conn                  = $em->getConnection();
161 1040
        $this->platform              = $this->conn->getDatabasePlatform();
162 1040
        $this->noLimitsContext       = $this->currentPersisterContext = new CachedPersisterContext(
163 1040
            $class,
164 1040
            new Query\ResultSetMapping(),
165 1040
            false
166
        );
167 1040
        $this->limitsHandlingContext = new CachedPersisterContext(
168 1040
            $class,
169 1040
            new Query\ResultSetMapping(),
170 1040
            true
171
        );
172 1040
    }
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 972
    public function getIdentifier($entity) : array
194
    {
195 972
        $id = [];
196
197 972
        foreach ($this->class->getIdentifier() as $fieldName) {
198 972
            $property = $this->class->getProperty($fieldName);
199 972
            $value    = $property->getValue($entity);
200
201 972
            if ($value !== null) {
202 972
                $id[$fieldName] = $value;
203
            }
204
        }
205
206 972
        return $id;
207
    }
208
209
    /**
210
     * Populates the entity identifier of an entity.
211
     *
212
     * @param object  $entity
213
     * @param mixed[] $id
214
     */
215 198
    public function setIdentifier($entity, array $id) : void
216
    {
217 198
        foreach ($id as $idField => $idValue) {
218 198
            $property = $this->class->getProperty($idField);
219
220 198
            $property->setValue($entity, $idValue);
221
        }
222 198
    }
223
224
    /**
225
     * {@inheritdoc}
226
     */
227 846
    public function insert($entity)
228
    {
229 846
        $stmt           = $this->conn->prepare($this->getInsertSQL());
230 846
        $tableName      = $this->class->getTableName();
231 846
        $insertData     = $this->prepareInsertData($entity);
232 846
        $generationPlan = $this->class->getValueGenerationPlan();
233
234 846
        if (isset($insertData[$tableName])) {
235 826
            $paramIndex = 1;
236
237 826
            foreach ($insertData[$tableName] as $columnName => $value) {
238 826
                $type = $this->columns[$columnName]->getType();
239
240 826
                $stmt->bindValue($paramIndex++, $value, $type);
241
            }
242
        }
243
244 846
        $stmt->execute();
245
246 845
        if ($generationPlan->containsDeferred()) {
247 760
            $generationPlan->executeDeferred($this->em, $entity);
248
        }
249
250 845
        if ($this->class->isVersioned()) {
251 188
            $this->assignDefaultVersionValue($entity, $this->getIdentifier($entity));
252
        }
253
254 845
        $stmt->closeCursor();
255 845
    }
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 196
    protected function assignDefaultVersionValue($entity, array $id)
266
    {
267 196
        $versionProperty = $this->class->versionProperty;
268 196
        $versionValue    = $this->fetchVersionValue($versionProperty, $id);
269
270 196
        $versionProperty->setValue($entity, $versionValue);
271 196
    }
272
273
    /**
274
     * Fetches the current version value of a versioned entity.
275
     *
276
     * @param mixed[] $id
277
     *
278
     * @return mixed
279
     */
280 196
    protected function fetchVersionValue(VersionFieldMetadata $versionProperty, array $id)
281
    {
282 196
        $versionedClass = $versionProperty->getDeclaringClass();
283 196
        $tableName      = $versionedClass->table->getQuotedQualifiedName($this->platform);
284 196
        $columnName     = $this->platform->quoteIdentifier($versionProperty->getColumnName());
285 196
        $identifier     = array_map(
286 196
            function ($columnName) {
287 196
                return $this->platform->quoteIdentifier($columnName);
288 196
            },
289 196
            array_keys($versionedClass->getIdentifierColumns($this->em))
290
        );
291
292
        // FIXME: Order with composite keys might not be correct
293 196
        $sql = 'SELECT ' . $columnName
294 196
             . ' FROM ' . $tableName
295 196
             . ' WHERE ' . implode(' = ? AND ', $identifier) . ' = ?';
296
297 196
        $flattenedId = $this->em->getIdentifierFlattener()->flattenIdentifier($versionedClass, $id);
298 196
        $versionType = $versionProperty->getType();
299
300 196
        $value = $this->conn->fetchColumn(
301 196
            $sql,
302 196
            array_values($flattenedId),
303 196
            0,
304 196
            $this->extractIdentifierTypes($id, $versionedClass)
305
        );
306
307 196
        return $versionType->convertToPHPValue($value, $this->platform);
308
    }
309
310
    /**
311
     * @param mixed[] $id
312
     *
313
     * @return mixed[]
314
     */
315 196
    private function extractIdentifierTypes(array $id, ClassMetadata $versionedClass) : array
316
    {
317 196
        $types = [];
318
319 196
        foreach ($id as $field => $value) {
320 196
            $types = array_merge($types, $this->getTypes($field, $value, $versionedClass));
321
        }
322
323 196
        return $types;
324
    }
325
326
    /**
327
     * {@inheritdoc}
328
     */
329 70
    public function update($entity)
330
    {
331 70
        $tableName  = $this->class->getTableName();
332 70
        $updateData = $this->prepareUpdateData($entity);
333
334 70
        if (! isset($updateData[$tableName])) {
335 8
            return;
336
        }
337
338 62
        $data = $updateData[$tableName];
339
340 62
        if (! $data) {
341
            return;
342
        }
343
344 62
        $isVersioned     = $this->class->isVersioned();
345 62
        $quotedTableName = $this->class->table->getQuotedQualifiedName($this->platform);
346
347 62
        $this->updateTable($entity, $quotedTableName, $data, $isVersioned);
348
349 62
        if ($isVersioned) {
350 10
            $id = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
351
352 10
            $this->assignDefaultVersionValue($entity, $id);
353
        }
354 62
    }
355
356
    /**
357
     * {@inheritdoc}
358
     */
359 53
    public function delete($entity)
360
    {
361 53
        $class      = $this->class;
362 53
        $unitOfWork = $this->em->getUnitOfWork();
363 53
        $identifier = $unitOfWork->getEntityIdentifier($entity);
364 53
        $tableName  = $class->table->getQuotedQualifiedName($this->platform);
365
366 53
        $types = [];
367 53
        $id    = [];
368
369 53
        foreach ($class->identifier as $field) {
370 53
            $property = $class->getProperty($field);
371
372 53
            if ($property instanceof FieldMetadata) {
373 51
                $columnName       = $property->getColumnName();
374 51
                $quotedColumnName = $this->platform->quoteIdentifier($columnName);
375
376 51
                $id[$quotedColumnName] = $identifier[$field];
377 51
                $types[]               = $property->getType();
378
379 51
                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 53
        $this->deleteJoinTableRecords($identifier);
416
417 53
        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 89
    final protected function updateTable($entity, $quotedTableName, array $updateData, $versioned = false)
433
    {
434 89
        $set    = [];
435 89
        $types  = [];
436 89
        $params = [];
437
438 89
        foreach ($updateData as $columnName => $value) {
439 89
            $column           = $this->columns[$columnName];
440 89
            $quotedColumnName = $this->platform->quoteIdentifier($column->getColumnName());
441 89
            $type             = $column->getType();
442 89
            $placeholder      = $type->convertToDatabaseValueSQL('?', $this->platform);
443
444 89
            $set[]    = sprintf('%s = %s', $quotedColumnName, $placeholder);
445 89
            $params[] = $value;
446 89
            $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 89
        $identifier = $this->getIdentifier($entity);
451 89
        $where      = [];
452
453 89
        foreach ($this->class->identifier as $idField) {
454 89
            $property = $this->class->getProperty($idField);
455
456
            switch (true) {
457 89
                case ($property instanceof FieldMetadata):
458 86
                    $where[]  = $this->platform->quoteIdentifier($property->getColumnName());
459 86
                    $params[] = $identifier[$idField];
460 86
                    $types[]  = $property->getType();
461 86
                    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 89
                    break;
483
            }
484
        }
485
486 89
        if ($versioned) {
487 14
            $versionProperty   = $this->class->versionProperty;
488 14
            $versionColumnType = $versionProperty->getType();
489 14
            $versionColumnName = $this->platform->quoteIdentifier($versionProperty->getColumnName());
490
491 14
            $where[]  = $versionColumnName;
492 14
            $types[]  = $versionColumnType;
493 14
            $params[] = $versionProperty->getValue($entity);
494
495 14
            switch ($versionColumnType->getName()) {
496
                case Type::SMALLINT:
497
                case Type::INTEGER:
498
                case Type::BIGINT:
499 14
                    $set[] = $versionColumnName . ' = ' . $versionColumnName . ' + 1';
500 14
                    break;
501
502
                case Type::DATETIME:
503
                    $set[] = $versionColumnName . ' = CURRENT_TIMESTAMP';
504
                    break;
505
            }
506
        }
507
508 89
        $sql = 'UPDATE ' . $quotedTableName
509 89
             . ' SET ' . implode(', ', $set)
510 89
             . ' WHERE ' . implode(' = ? AND ', $where) . ' = ?';
511
512 89
        $result = $this->conn->executeUpdate($sql, $params, $types);
513
514 89
        if ($versioned && ! $result) {
515
            throw OptimisticLockException::lockFailed($entity);
516
        }
517 89
    }
518
519
    /**
520
     * @todo Add check for platform if it supports foreign keys/cascading.
521
     *
522
     * @param mixed[] $identifier
523
     */
524 57
    protected function deleteJoinTableRecords($identifier)
525
    {
526 57
        foreach ($this->class->getDeclaredPropertiesIterator() as $association) {
527 57
            if (! ($association instanceof ManyToManyAssociationMetadata)) {
528 57
                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 20
            $selfReferential   = $association->getTargetEntity() === $association->getSourceEntity();
534 20
            $owningAssociation = $association;
535 20
            $otherColumns      = [];
536 20
            $otherKeys         = [];
537 20
            $keys              = [];
538
539 20
            if (! $owningAssociation->isOwningSide()) {
540 6
                $class             = $this->em->getClassMetadata($association->getTargetEntity());
541 6
                $owningAssociation = $class->getProperty($association->getMappedBy());
542
            }
543
544 20
            $joinTable     = $owningAssociation->getJoinTable();
545 20
            $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
546 20
            $joinColumns   = $association->isOwningSide()
547 16
                ? $joinTable->getJoinColumns()
548 20
                : $joinTable->getInverseJoinColumns()
549
            ;
550
551 20
            if ($selfReferential) {
552 1
                $otherColumns = ! $association->isOwningSide()
553
                    ? $joinTable->getJoinColumns()
554 1
                    : $joinTable->getInverseJoinColumns()
555
                ;
556
            }
557
558 20
            $isOnDeleteCascade = false;
559
560 20
            foreach ($joinColumns as $joinColumn) {
561 20
                $keys[] = $this->platform->quoteIdentifier($joinColumn->getColumnName());
562
563 20
                if ($joinColumn->isOnDeleteCascade()) {
564 20
                    $isOnDeleteCascade = true;
565
                }
566
            }
567
568 20
            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 20
            if ($isOnDeleteCascade) {
577 5
                continue;
578
            }
579
580 16
            $this->conn->delete($joinTableName, array_combine($keys, $identifier));
581
582 16
            if ($selfReferential) {
583 16
                $this->conn->delete($joinTableName, array_combine($otherKeys, $identifier));
584
            }
585
        }
586 57
    }
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 926
    protected function prepareInsertData($entity) : array
599
    {
600 926
        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 927
    protected function prepareUpdateData($entity)
625
    {
626 927
        $uow                 = $this->em->getUnitOfWork();
627 927
        $result              = [];
628 927
        $versionPropertyName = $this->class->isVersioned()
629 196
            ? $this->class->versionProperty->getName()
630 927
            : null
631
        ;
632
633
        // @todo guilhermeblanco This should check column insertability/updateability instead of field changeset
634 927
        foreach ($uow->getEntityChangeSet($entity) as $propertyName => $propertyChangeSet) {
635 899
            if ($versionPropertyName === $propertyName) {
636
                continue;
637
            }
638
639 899
            $property = $this->class->getProperty($propertyName);
640 899
            $newValue = $propertyChangeSet[1];
641
642 899
            if ($property instanceof FieldMetadata) {
643
                // @todo guilhermeblanco Please remove this in the future for good...
644 870
                $this->columns[$property->getColumnName()] = $property;
645
646 870
                $result[$property->getTableName()][$property->getColumnName()] = $newValue;
647
648 870
                continue;
649
            }
650
651
            // Only owning side of x-1 associations can have a FK column.
652 782
            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 782
            if ($newValue !== null && $uow->isScheduledForInsert($newValue)) {
660 30
                $uow->scheduleExtraUpdate($entity, [$propertyName => [null, $newValue]]);
661
662 30
                $newValue = null;
663
            }
664
665 782
            $targetClass     = $this->em->getClassMetadata($property->getTargetEntity());
666 782
            $targetPersister = $uow->getEntityPersister($targetClass->getClassName());
667
668 782
            foreach ($property->getJoinColumns() as $joinColumn) {
669
                /** @var JoinColumnMetadata $joinColumn */
670 782
                $referencedColumnName = $joinColumn->getReferencedColumnName();
671
672 782
                if (! $joinColumn->getType()) {
673 8
                    $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
674
                }
675
676
                // @todo guilhermeblanco Please remove this in the future for good...
677 782
                $this->columns[$joinColumn->getColumnName()] = $joinColumn;
678
679 782
                $result[$joinColumn->getTableName()][$joinColumn->getColumnName()] = $newValue !== null
680 583
                    ? $targetPersister->getColumnValue($newValue, $referencedColumnName)
681 782
                    : null
682
                ;
683
            }
684
        }
685
686 927
        return $result;
687
    }
688
689
    /**
690
     * @param object $entity
691
     *
692
     * @return mixed|null
693
     */
694 583
    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 583
        $propertyName = $this->class->fieldNames[$columnName];
698 583
        $property     = $this->class->getProperty($propertyName);
699
700 583
        if (! $property) {
701
            return null;
702
        }
703
704 583
        $propertyValue = $property->getValue($entity);
705
706 583
        if ($property instanceof LocalColumnMetadata) {
707 583
            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 438
    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 438
        $this->switchPersisterContext(null, $limit);
746
747 438
        $sql = $this->getSelectSQL($criteria, $association, $lockMode, $limit, null, $orderBy);
748
749 438
        list($params, $types) = $this->expandParameters($criteria);
750
751 438
        $stmt = $this->conn->executeQuery($sql, $params, $types);
752
753 438
        if ($entity !== null) {
754 55
            $hints[Query::HINT_REFRESH]        = true;
755 55
            $hints[Query::HINT_REFRESH_ENTITY] = $entity;
756
        }
757
758 438
        $hydrator = $this->em->newHydrator($this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
759 438
        $entities = $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, $hints);
760
761 438
        return $entities ? $entities[0] : null;
762
    }
763
764
    /**
765
     * {@inheritdoc}
766
     */
767 364
    public function loadById(array $identifier, $entity = null)
768
    {
769 364
        return $this->load($identifier, $entity);
770
    }
771
772
    /**
773
     * {@inheritdoc}
774
     */
775 86
    public function loadToOneEntity(ToOneAssociationMetadata $association, $sourceEntity, array $identifier = [])
776
    {
777 86
        $unitOfWork   = $this->em->getUnitOfWork();
778 86
        $targetEntity = $association->getTargetEntity();
779 86
        $foundEntity  = $unitOfWork->tryGetById($identifier, $targetEntity);
780
781 86
        if ($foundEntity !== false) {
782
            return $foundEntity;
783
        }
784
785 86
        $targetClass = $this->em->getClassMetadata($targetEntity);
786
787 86
        if ($association->isOwningSide()) {
788 29
            $inversedBy            = $association->getInversedBy();
789 29
            $targetProperty        = $inversedBy ? $targetClass->getProperty($inversedBy) : null;
790 29
            $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 29
            $hints = [];
795
796 29
            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 29
            $entity = $this->load($identifier, null, $association, $hints);
807
808
            // Complete bidirectional association, if necessary
809 29
            if ($entity !== null && $isInverseSingleValued) {
810
                $targetProperty->setValue($entity, $sourceEntity);
811
            }
812
813 29
            return $entity;
814
        }
815
816 57
        $sourceClass       = $association->getDeclaringClass();
817 57
        $owningAssociation = $targetClass->getProperty($association->getMappedBy());
818 57
        $targetTableAlias  = $this->getSQLTableAlias($targetClass->getTableName());
819
820 57
        foreach ($owningAssociation->getJoinColumns() as $joinColumn) {
821 57
            $sourceKeyColumn = $joinColumn->getReferencedColumnName();
822 57
            $targetKeyColumn = $joinColumn->getColumnName();
823
824 57
            if (! isset($sourceClass->fieldNames[$sourceKeyColumn])) {
825
                throw MappingException::joinColumnMustPointToMappedField(
826
                    $sourceClass->getClassName(),
827
                    $sourceKeyColumn
828
                );
829
            }
830
831 57
            $property = $sourceClass->getProperty($sourceClass->fieldNames[$sourceKeyColumn]);
832 57
            $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 57
            unset($identifier[$targetKeyColumn]);
838
839 57
            $identifier[$targetClass->fieldNames[$targetKeyColumn]] = $value;
840
        }
841
842 57
        $entity = $this->load($identifier, null, $association);
843
844 57
        if ($entity !== null) {
845 16
            $owningAssociation->setValue($entity, $sourceEntity);
846
        }
847
848 57
        return $entity;
849
    }
850
851
    /**
852
     * {@inheritdoc}
853
     */
854 11
    public function refresh(array $id, $entity, $lockMode = null)
855
    {
856 11
        $sql                  = $this->getSelectSQL($id, null, $lockMode);
857 11
        list($params, $types) = $this->expandParameters($id);
858 11
        $stmt                 = $this->conn->executeQuery($sql, $params, $types);
859
860 11
        $hydrator = $this->em->newHydrator(Query::HYDRATE_OBJECT);
861 11
        $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [Query::HINT_REFRESH => true]);
862 11
    }
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 6
    public function loadCriteria(Criteria $criteria)
882
    {
883 6
        $orderBy = $criteria->getOrderings();
884 6
        $limit   = $criteria->getMaxResults();
885 6
        $offset  = $criteria->getFirstResult();
886 6
        $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 62
    public function loadAll(array $criteria = [], array $orderBy = [], $limit = null, $offset = null)
934
    {
935 62
        $this->switchPersisterContext($offset, $limit);
936
937 62
        $sql = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy);
938
939 62
        list($params, $types) = $this->expandParameters($criteria);
940
941 62
        $stmt         = $this->conn->executeQuery($sql, $params, $types);
942 62
        $rsm          = $this->currentPersisterContext->rsm;
943 62
        $hints        = [UnitOfWork::HINT_DEFEREAGERLOAD => true];
944 62
        $hydratorType = $this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT;
945 62
        $hydrator     = $this->em->newHydrator($hydratorType);
946
947 62
        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 70
    public function loadManyToManyCollection(
970
        ManyToManyAssociationMetadata $association,
971
        $sourceEntity,
972
        PersistentCollection $collection
973
    ) {
974 70
        $stmt = $this->getManyToManyStatement($association, $sourceEntity);
975
976 70
        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 129
    private function loadCollectionFromStatement(ToManyAssociationMetadata $association, $stmt, $collection)
1010
    {
1011 129
        $rsm = $this->currentPersisterContext->rsm;
1012
1013 129
        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 129
        $hydrator = $this->em->newHydrator(Query::HYDRATE_OBJECT);
1019
        $hints    = [
1020 129
            UnitOfWork::HINT_DEFEREAGERLOAD => true,
1021 129
            'collection' => $collection,
1022
        ];
1023
1024 129
        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 77
    private function getManyToManyStatement(
1037
        ManyToManyAssociationMetadata $association,
1038
        $sourceEntity,
1039
        $offset = null,
1040
        $limit = null
1041
    ) {
1042 77
        $this->switchPersisterContext($offset, $limit);
1043
1044
        /** @var ClassMetadata $sourceClass */
1045 77
        $sourceClass = $this->em->getClassMetadata($association->getSourceEntity());
1046 77
        $class       = $sourceClass;
1047 77
        $owningAssoc = $association;
1048 77
        $criteria    = [];
1049 77
        $parameters  = [];
1050
1051 77
        if (! $association->isOwningSide()) {
1052 12
            $class       = $this->em->getClassMetadata($association->getTargetEntity());
1053 12
            $owningAssoc = $class->getProperty($association->getMappedBy());
1054
        }
1055
1056 77
        $joinTable     = $owningAssoc->getJoinTable();
1057 77
        $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
1058 77
        $joinColumns   = $association->isOwningSide()
1059 70
            ? $joinTable->getJoinColumns()
1060 77
            : $joinTable->getInverseJoinColumns()
1061
        ;
1062
1063 77
        foreach ($joinColumns as $joinColumn) {
1064 77
            $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName());
1065 77
            $fieldName        = $sourceClass->fieldNames[$joinColumn->getReferencedColumnName()];
1066 77
            $property         = $sourceClass->getProperty($fieldName);
1067
1068 77
            if ($property instanceof FieldMetadata) {
1069 76
                $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 77
            $criteria[$joinTableName . '.' . $quotedColumnName] = $value;
1080 77
            $parameters[]                                       = [
1081 77
                'value' => $value,
1082 77
                'field' => $fieldName,
1083 77
                'class' => $sourceClass,
1084
            ];
1085
        }
1086
1087 77
        $sql = $this->getSelectSQL($criteria, $association, null, $limit, $offset);
1088
1089 77
        list($params, $types) = $this->expandToManyParameters($parameters);
1090
1091 77
        return $this->conn->executeQuery($sql, $params, $types);
1092
    }
1093
1094
    /**
1095
     * {@inheritdoc}
1096
     */
1097 480
    public function getSelectSQL(
1098
        $criteria,
1099
        ?AssociationMetadata $association = null,
1100
        $lockMode = null,
1101
        $limit = null,
1102
        $offset = null,
1103
        array $orderBy = []
1104
    ) {
1105 480
        $this->switchPersisterContext($offset, $limit);
1106
1107 480
        $lockSql    = '';
1108 480
        $joinSql    = '';
1109 480
        $orderBySql = '';
1110
1111 480
        if ($association instanceof ManyToManyAssociationMetadata) {
1112 78
            $joinSql = $this->getSelectManyToManyJoinSQL($association);
1113
        }
1114
1115 480
        if ($association instanceof ToManyAssociationMetadata && $association->getOrderBy()) {
1116 5
            $orderBy = $association->getOrderBy();
1117
        }
1118
1119 480
        if ($orderBy) {
1120 9
            $orderBySql = $this->getOrderBySQL($orderBy, $this->getSQLTableAlias($this->class->getTableName()));
1121
        }
1122
1123 480
        $conditionSql = ($criteria instanceof Criteria)
1124 6
            ? $this->getSelectConditionCriteriaSQL($criteria)
1125 480
            : $this->getSelectConditionSQL($criteria, $association);
1126
1127
        switch ($lockMode) {
1128 480
            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 480
            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 480
        $columnList = $this->getSelectColumnsSQL();
1138 480
        $tableAlias = $this->getSQLTableAlias($this->class->getTableName());
1139 480
        $filterSql  = $this->generateFilterConditionSQL($this->class, $tableAlias);
1140 480
        $tableName  = $this->class->table->getQuotedQualifiedName($this->platform);
1141
1142 480
        if ($filterSql !== '') {
1143 12
            $conditionSql = $conditionSql
1144 11
                ? $conditionSql . ' AND ' . $filterSql
1145 12
                : $filterSql;
1146
        }
1147
1148 480
        $select = 'SELECT ' . $columnList;
1149 480
        $from   = ' FROM ' . $tableName . ' ' . $tableAlias;
1150 480
        $join   = $this->currentPersisterContext->selectJoinSql . $joinSql;
1151 480
        $where  = ($conditionSql ? ' WHERE ' . $conditionSql : '');
1152 480
        $lock   = $this->platform->appendLockHint($from, $lockMode);
1153
        $query  = $select
1154 480
            . $lock
1155 480
            . $join
1156 480
            . $where
1157 480
            . $orderBySql;
1158
1159 480
        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 76
    final protected function getOrderBySQL(array $orderBy, $baseTableAlias)
1200
    {
1201 76
        if (! $orderBy) {
1202 66
            return '';
1203
        }
1204
1205 10
        $orderByList = [];
1206
1207 10
        foreach ($orderBy as $fieldName => $orientation) {
1208 10
            $orientation = strtoupper(trim($orientation));
1209
1210 10
            if (! in_array($orientation, ['ASC', 'DESC'], true)) {
1211
                throw ORMException::invalidOrientation($this->class->getClassName(), $fieldName);
1212
            }
1213
1214 10
            $property = $this->class->getProperty($fieldName);
1215
1216 10
            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 1
            } elseif ($property instanceof AssociationMetadata) {
1224 1
                if (! $property->isOwningSide()) {
1225
                    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 481
    protected function getSelectColumnsSQL()
1261
    {
1262 481
        if ($this->currentPersisterContext->selectColumnListSql !== null) {
1263 95
            return $this->currentPersisterContext->selectColumnListSql;
1264
        }
1265
1266 481
        $this->currentPersisterContext->rsm->addEntityResult($this->class->getClassName(), 'r'); // r for root
1267 481
        $this->currentPersisterContext->selectJoinSql = '';
1268
1269 481
        $eagerAliasCounter = 0;
1270 481
        $columnList        = [];
1271
1272 481
        foreach ($this->class->getDeclaredPropertiesIterator() as $fieldName => $property) {
1273
            switch (true) {
1274 481
                case ($property instanceof FieldMetadata):
1275 479
                    $columnList[] = $this->getSelectColumnSQL($fieldName, $this->class);
1276 479
                    break;
1277
1278 431
                case ($property instanceof AssociationMetadata):
1279 429
                    $assocColumnSQL = $this->getSelectColumnAssociationSQL($fieldName, $property, $this->class);
1280
1281 429
                    if ($assocColumnSQL) {
1282 366
                        $columnList[] = $assocColumnSQL;
1283
                    }
1284
1285 429
                    $isAssocToOneInverseSide = $property instanceof ToOneAssociationMetadata && ! $property->isOwningSide();
1286 429
                    $isAssocFromOneEager     = ! $property instanceof ManyToManyAssociationMetadata && $property->getFetchMode() === FetchMode::EAGER;
1287
1288 429
                    if (! ($isAssocFromOneEager || $isAssocToOneInverseSide)) {
1289 415
                        break;
1290
                    }
1291
1292 163
                    if ($property instanceof ToManyAssociationMetadata && $this->currentPersisterContext->handlesLimits) {
1293 3
                        break;
1294
                    }
1295
1296 160
                    $targetEntity = $property->getTargetEntity();
1297 160
                    $eagerEntity  = $this->em->getClassMetadata($targetEntity);
1298
1299 160
                    if ($eagerEntity->inheritanceType !== InheritanceType::NONE) {
1300 5
                        break; // now this is why you shouldn't use inheritance
1301
                    }
1302
1303 155
                    $assocAlias = 'e' . ($eagerAliasCounter++);
1304
1305 155
                    $this->currentPersisterContext->rsm->addJoinedEntityResult($targetEntity, $assocAlias, 'r', $fieldName);
1306
1307 155
                    foreach ($eagerEntity->getDeclaredPropertiesIterator() as $eagerProperty) {
1308
                        switch (true) {
1309 155
                            case ($eagerProperty instanceof FieldMetadata):
1310 153
                                $columnList[] = $this->getSelectColumnSQL($eagerProperty->getName(), $eagerEntity, $assocAlias);
1311 153
                                break;
1312
1313 152
                            case ($eagerProperty instanceof ToOneAssociationMetadata && $eagerProperty->isOwningSide()):
1314 150
                                $columnList[] = $this->getSelectColumnAssociationSQL(
1315 150
                                    $eagerProperty->getName(),
1316 150
                                    $eagerProperty,
1317 150
                                    $eagerEntity,
1318 150
                                    $assocAlias
1319
                                );
1320 155
                                break;
1321
                        }
1322
                    }
1323
1324 155
                    $owningAssociation = $property;
1325 155
                    $joinCondition     = [];
1326
1327 155
                    if ($property instanceof ToManyAssociationMetadata && $property->getIndexedBy()) {
1328 1
                        $this->currentPersisterContext->rsm->addIndexBy($assocAlias, $property->getIndexedBy());
1329
                    }
1330
1331 155
                    if (! $property->isOwningSide()) {
1332 150
                        $owningAssociation = $eagerEntity->getProperty($property->getMappedBy());
1333
                    }
1334
1335 155
                    $joinTableAlias = $this->getSQLTableAlias($eagerEntity->getTableName(), $assocAlias);
1336 155
                    $joinTableName  = $eagerEntity->table->getQuotedQualifiedName($this->platform);
1337
1338 155
                    $this->currentPersisterContext->selectJoinSql .= ' ' . $this->getJoinSQLForAssociation($property);
1339
1340 155
                    $sourceClass      = $this->em->getClassMetadata($owningAssociation->getSourceEntity());
1341 155
                    $targetClass      = $this->em->getClassMetadata($owningAssociation->getTargetEntity());
1342 155
                    $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName(), $property->isOwningSide() ? $assocAlias : '');
1343 155
                    $sourceTableAlias = $this->getSQLTableAlias($sourceClass->getTableName(), $property->isOwningSide() ? '' : $assocAlias);
1344
1345 155
                    foreach ($owningAssociation->getJoinColumns() as $joinColumn) {
1346 155
                        $joinCondition[] = sprintf(
1347 155
                            '%s.%s = %s.%s',
1348 155
                            $sourceTableAlias,
1349 155
                            $this->platform->quoteIdentifier($joinColumn->getColumnName()),
1350 155
                            $targetTableAlias,
1351 155
                            $this->platform->quoteIdentifier($joinColumn->getReferencedColumnName())
1352
                        );
1353
                    }
1354
1355 155
                    $filterSql = $this->generateFilterConditionSQL($eagerEntity, $targetTableAlias);
1356
1357
                    // Add filter SQL
1358 155
                    if ($filterSql) {
1359
                        $joinCondition[] = $filterSql;
1360
                    }
1361
1362 155
                    $this->currentPersisterContext->selectJoinSql .= ' ' . $joinTableName . ' ' . $joinTableAlias . ' ON ';
1363 155
                    $this->currentPersisterContext->selectJoinSql .= implode(' AND ', $joinCondition);
1364
1365 481
                    break;
1366
            }
1367
        }
1368
1369 481
        $this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList);
1370
1371 481
        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 429
    protected function getSelectColumnAssociationSQL($field, AssociationMetadata $association, ClassMetadata $class, $alias = 'r')
1383
    {
1384 429
        if (! ($association->isOwningSide() && $association instanceof ToOneAssociationMetadata)) {
1385 352
            return '';
1386
        }
1387
1388 378
        $columnList    = [];
1389 378
        $targetClass   = $this->em->getClassMetadata($association->getTargetEntity());
1390 378
        $sqlTableAlias = $this->getSQLTableAlias($class->getTableName(), ($alias === 'r' ? '' : $alias));
1391
1392 378
        foreach ($association->getJoinColumns() as $joinColumn) {
1393
            /** @var JoinColumnMetadata $joinColumn */
1394 378
            $columnName           = $joinColumn->getColumnName();
1395 378
            $quotedColumnName     = $this->platform->quoteIdentifier($columnName);
1396 378
            $referencedColumnName = $joinColumn->getReferencedColumnName();
1397 378
            $resultColumnName     = $this->getSQLColumnAlias();
1398
1399 378
            if (! $joinColumn->getType()) {
1400 9
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
1401
            }
1402
1403 378
            $this->currentPersisterContext->rsm->addMetaResult(
1404 378
                $alias,
1405 378
                $resultColumnName,
1406 378
                $columnName,
1407 378
                $association->isPrimaryKey(),
1408 378
                $joinColumn->getType()
1409
            );
1410
1411 378
            $columnList[] = sprintf('%s.%s AS %s', $sqlTableAlias, $quotedColumnName, $resultColumnName);
1412
        }
1413
1414 378
        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 80
    protected function getSelectManyToManyJoinSQL(ManyToManyAssociationMetadata $association)
1426
    {
1427 80
        $conditions        = [];
1428 80
        $owningAssociation = $association;
1429 80
        $sourceTableAlias  = $this->getSQLTableAlias($this->class->getTableName());
1430
1431 80
        if (! $association->isOwningSide()) {
1432 13
            $targetEntity      = $this->em->getClassMetadata($association->getTargetEntity());
1433 13
            $owningAssociation = $targetEntity->getProperty($association->getMappedBy());
1434
        }
1435
1436 80
        $joinTable     = $owningAssociation->getJoinTable();
1437 80
        $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
1438 80
        $joinColumns   = $association->isOwningSide()
1439 72
            ? $joinTable->getInverseJoinColumns()
1440 80
            : $joinTable->getJoinColumns()
1441
        ;
1442
1443 80
        foreach ($joinColumns as $joinColumn) {
1444 80
            $conditions[] = sprintf(
1445 80
                '%s.%s = %s.%s',
1446 80
                $sourceTableAlias,
1447 80
                $this->platform->quoteIdentifier($joinColumn->getReferencedColumnName()),
1448 80
                $joinTableName,
1449 80
                $this->platform->quoteIdentifier($joinColumn->getColumnName())
1450
            );
1451
        }
1452
1453 80
        return ' INNER JOIN ' . $joinTableName . ' ON ' . implode(' AND ', $conditions);
1454
    }
1455
1456
    /**
1457
     * {@inheritdoc}
1458
     */
1459 927
    public function getInsertSQL()
1460
    {
1461 927
        if ($this->insertSql !== null) {
1462 639
            return $this->insertSql;
1463
        }
1464
1465 927
        $columns   = $this->getInsertColumnList();
1466 927
        $tableName = $this->class->table->getQuotedQualifiedName($this->platform);
1467
1468 927
        if (empty($columns)) {
1469 93
            $property       = $this->class->getProperty($this->class->identifier[0]);
1470 93
            $identityColumn = $this->platform->quoteIdentifier($property->getColumnName());
1471
1472 93
            $this->insertSql = $this->platform->getEmptyIdentityInsertSQL($tableName, $identityColumn);
1473
1474 93
            return $this->insertSql;
1475
        }
1476
1477 909
        $quotedColumns = [];
1478 909
        $values        = [];
1479
1480 909
        foreach ($columns as $columnName) {
1481 909
            $column = $this->columns[$columnName];
1482
1483 909
            $quotedColumns[] = $this->platform->quoteIdentifier($column->getColumnName());
1484 909
            $values[]        = $column->getType()->convertToDatabaseValueSQL('?', $this->platform);
1485
        }
1486
1487 909
        $quotedColumns = implode(', ', $quotedColumns);
1488 909
        $values        = implode(', ', $values);
1489
1490 909
        $this->insertSql = sprintf('INSERT INTO %s (%s) VALUES (%s)', $tableName, $quotedColumns, $values);
1491
1492 909
        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 847
    protected function getInsertColumnList()
1504
    {
1505 847
        $columns             = [];
1506 847
        $versionPropertyName = $this->class->isVersioned()
1507 188
            ? $this->class->versionProperty->getName()
1508 847
            : null
1509
        ;
1510
1511 847
        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 847
                case ($property instanceof VersionFieldMetadata):
1518
                    // Do nothing
1519 188
                    break;
1520
1521 847
                case ($property instanceof LocalColumnMetadata):
1522 847
                    if (($property instanceof FieldMetadata
1523
                            && (
1524 847
                                ! $property->hasValueGenerator()
1525 847
                                || $property->getValueGenerator()->getType() !== GeneratorType::IDENTITY
1526
                            )
1527
                        )
1528 847
                        || $this->class->identifier[0] !== $name
1529
                    ) {
1530 791
                        $columnName = $property->getColumnName();
1531
1532 791
                        $columns[] = $columnName;
1533
1534 791
                        $this->columns[$columnName] = $property;
1535
                    }
1536
1537 847
                    break;
1538
1539 752
                case ($property instanceof AssociationMetadata):
1540 750
                    if ($property->isOwningSide() && $property instanceof ToOneAssociationMetadata) {
1541 712
                        $targetClass = $this->em->getClassMetadata($property->getTargetEntity());
1542
1543 712
                        foreach ($property->getJoinColumns() as $joinColumn) {
1544
                            /** @var JoinColumnMetadata $joinColumn */
1545 712
                            $columnName           = $joinColumn->getColumnName();
1546 712
                            $referencedColumnName = $joinColumn->getReferencedColumnName();
1547
1548 712
                            if (! $joinColumn->getType()) {
1549 112
                                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
1550
                            }
1551
1552 712
                            $columns[] = $columnName;
1553
1554 712
                            $this->columns[$columnName] = $joinColumn;
1555
                        }
1556
                    }
1557
1558 847
                    break;
1559
            }
1560
        }
1561
1562 847
        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 514
    protected function getSelectColumnSQL($field, ClassMetadata $class, $alias = 'r')
1576
    {
1577 514
        $property    = $class->getProperty($field);
1578 514
        $columnAlias = $this->getSQLColumnAlias();
1579 514
        $sql         = sprintf(
1580 514
            '%s.%s',
1581 514
            $this->getSQLTableAlias($property->getTableName(), ($alias === 'r' ? '' : $alias)),
1582 514
            $this->platform->quoteIdentifier($property->getColumnName())
1583
        );
1584
1585 514
        $this->currentPersisterContext->rsm->addFieldResult($alias, $columnAlias, $field, $class->getClassName());
1586
1587 514
        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 545
    protected function getSQLTableAlias($tableName, $assocName = '')
1599
    {
1600 545
        if ($tableName) {
1601 545
            $tableName .= '#' . $assocName;
1602
        }
1603
1604 545
        if (isset($this->currentPersisterContext->sqlTableAliases[$tableName])) {
1605 540
            return $this->currentPersisterContext->sqlTableAliases[$tableName];
1606
        }
1607
1608 545
        $tableAlias = 't' . $this->currentPersisterContext->sqlAliasCounter++;
1609
1610 545
        $this->currentPersisterContext->sqlTableAliases[$tableName] = $tableAlias;
1611
1612 545
        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 37
    protected function getSelectConditionCriteriaSQL(Criteria $criteria)
1668
    {
1669 37
        $expression = $criteria->getWhereExpression();
1670
1671 37
        if ($expression === null) {
1672 2
            return '';
1673
        }
1674
1675 36
        $visitor = new SqlExpressionVisitor($this, $this->class);
1676
1677 36
        return $visitor->dispatch($expression);
1678
    }
1679
1680
    /**
1681
     * {@inheritdoc}
1682
     */
1683 523
    public function getSelectConditionStatementSQL(
1684
        $field,
1685
        $value,
1686
        ?AssociationMetadata $association = null,
1687
        $comparison = null
1688
    ) {
1689 523
        $selectedColumns = [];
1690 523
        $columns         = $this->getSelectConditionStatementColumnSQL($field, $association);
1691
1692 523
        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
            throw ORMException::cantUseInOperatorOnCompositeKeys();
1695
        }
1696
1697 523
        foreach ($columns as $column) {
1698 523
            $property    = $this->class->getProperty($field);
1699 523
            $placeholder = '?';
1700
1701 523
            if ($property instanceof FieldMetadata) {
1702 440
                $placeholder = $property->getType()->convertToDatabaseValueSQL($placeholder, $this->platform);
1703
            }
1704
1705 523
            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 496
            if (is_array($value)) {
1725 10
                $in = sprintf('%s IN (%s)', $column, $placeholder);
1726
1727 10
                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 6
                $selectedColumns[] = $in;
1734
1735 6
                continue;
1736
            }
1737
1738 486
            if ($value === null) {
1739 9
                $selectedColumns[] = sprintf('%s IS NULL', $column);
1740
1741 9
                continue;
1742
            }
1743
1744 478
            $selectedColumns[] = sprintf('%s = %s', $column, $placeholder);
1745
        }
1746
1747 523
        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 523
    private function getSelectConditionStatementColumnSQL($field, ?AssociationMetadata $association = null)
1760
    {
1761 523
        $property = $this->class->getProperty($field);
1762
1763 523
        if ($property instanceof FieldMetadata) {
1764 440
            $tableAlias = $this->getSQLTableAlias($property->getTableName());
1765 440
            $columnName = $this->platform->quoteIdentifier($property->getColumnName());
1766
1767 440
            return [$tableAlias . '.' . $columnName];
1768
        }
1769
1770 266
        if ($property instanceof AssociationMetadata) {
1771 135
            $owningAssociation = $property;
1772 135
            $columns           = [];
1773
1774
            // Many-To-Many requires join table check for joinColumn
1775 135
            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 133
                if (! $owningAssociation->isOwningSide()) {
1794
                    throw ORMException::invalidFindByInverseAssociation($this->class->getClassName(), $field);
1795
                }
1796
1797 133
                $class      = $this->class->isInheritedProperty($field)
1798 11
                    ? $owningAssociation->getDeclaringClass()
1799 133
                    : $this->class
1800
                ;
1801 133
                $tableAlias = $this->getSQLTableAlias($class->getTableName());
1802
1803 133
                foreach ($owningAssociation->getJoinColumns() as $joinColumn) {
1804 133
                    $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName());
1805
1806 133
                    $columns[] = $tableAlias . '.' . $quotedColumnName;
1807
                }
1808
            }
1809
1810 135
            return $columns;
1811
        }
1812
1813 145
        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 145
            return [$field];
1819
        }
1820
1821
        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 518
    protected function getSelectConditionSQL(array $criteria, ?AssociationMetadata $association = null)
1836
    {
1837 518
        $conditions = [];
1838
1839 518
        foreach ($criteria as $field => $value) {
1840 495
            $conditions[] = $this->getSelectConditionStatementSQL($field, $value, $association);
1841
        }
1842
1843 518
        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 67
    public function loadOneToManyCollection(
1866
        OneToManyAssociationMetadata $association,
1867
        $sourceEntity,
1868
        PersistentCollection $collection
1869
    ) {
1870 67
        $stmt = $this->getOneToManyStatement($association, $sourceEntity);
1871
1872 67
        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 72
    private function getOneToManyStatement(
1885
        OneToManyAssociationMetadata $association,
1886
        $sourceEntity,
1887
        $offset = null,
1888
        $limit = null
1889
    ) {
1890 72
        $this->switchPersisterContext($offset, $limit);
1891
1892 72
        $criteria    = [];
1893 72
        $parameters  = [];
1894 72
        $owningAssoc = $this->class->getProperty($association->getMappedBy());
1895 72
        $sourceClass = $this->em->getClassMetadata($association->getSourceEntity());
1896 72
        $class       = $owningAssoc->getDeclaringClass();
1897 72
        $tableAlias  = $this->getSQLTableAlias($class->getTableName());
1898
1899 72
        foreach ($owningAssoc->getJoinColumns() as $joinColumn) {
1900 72
            $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName());
1901 72
            $fieldName        = $sourceClass->fieldNames[$joinColumn->getReferencedColumnName()];
1902 72
            $property         = $sourceClass->getProperty($fieldName);
1903
1904 72
            if ($property instanceof FieldMetadata) {
1905 72
                $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 72
            $criteria[$tableAlias . '.' . $quotedColumnName] = $value;
1915 72
            $parameters[]                                    = [
1916 72
                'value' => $value,
1917 72
                'field' => $fieldName,
1918 72
                'class' => $sourceClass,
1919
            ];
1920
        }
1921
1922 72
        $sql                  = $this->getSelectSQL($criteria, $association, null, $limit, $offset);
1923 72
        list($params, $types) = $this->expandToManyParameters($parameters);
1924
1925 72
        return $this->conn->executeQuery($sql, $params, $types);
1926
    }
1927
1928
    /**
1929
     * {@inheritdoc}
1930
     */
1931 500
    public function expandParameters($criteria)
1932
    {
1933 500
        $params = [];
1934 500
        $types  = [];
1935
1936 500
        foreach ($criteria as $field => $value) {
1937 477
            if ($value === null) {
1938 3
                continue; // skip null values.
1939
            }
1940
1941 475
            $types  = array_merge($types, $this->getTypes($field, $value, $this->class));
1942 475
            $params = array_merge($params, $this->getValues($value));
1943
        }
1944
1945 500
        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 141
    private function expandToManyParameters($criteria)
1961
    {
1962 141
        $params = [];
1963 141
        $types  = [];
1964
1965 141
        foreach ($criteria as $criterion) {
1966 141
            if ($criterion['value'] === null) {
1967 6
                continue; // skip null values.
1968
            }
1969
1970 135
            $types  = array_merge($types, $this->getTypes($criterion['field'], $criterion['value'], $criterion['class']));
1971 135
            $params = array_merge($params, $this->getValues($criterion['value']));
1972
        }
1973
1974 141
        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 622
    private function getTypes($field, $value, ClassMetadata $class)
1988
    {
1989 622
        $property = $class->getProperty($field);
1990 622
        $types    = [];
1991
1992
        switch (true) {
1993 622
            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 568
                $types = array_merge($types, [$property->getType()]);
1995 568
                break;
1996
1997 136
            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 135
                $class = $this->em->getClassMetadata($property->getTargetEntity());
1999
2000 135
                if (! $property->isOwningSide()) {
2001 2
                    $property = $class->getProperty($property->getMappedBy());
2002 2
                    $class    = $this->em->getClassMetadata($property->getTargetEntity());
2003
                }
2004
2005 135
                $joinColumns = $property instanceof ManyToManyAssociationMetadata
2006 3
                    ? $property->getJoinTable()->getInverseJoinColumns()
2007 135
                    : $property->getJoinColumns()
2008
                ;
2009
2010 135
                foreach ($joinColumns as $joinColumn) {
2011
                    /** @var JoinColumnMetadata $joinColumn */
2012 135
                    $referencedColumnName = $joinColumn->getReferencedColumnName();
2013
2014 135
                    if (! $joinColumn->getType()) {
2015 1
                        $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $class, $this->em));
2016
                    }
2017
2018 135
                    $types[] = $joinColumn->getType();
2019
                }
2020
2021 135
                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 622
        if (is_array($value)) {
2029 12
            return array_map(function ($type) {
2030 12
                return $type->getBindingType() + Connection::ARRAY_PARAM_OFFSET;
2031 12
            }, $types);
2032
        }
2033
2034 613
        return $types;
2035
    }
2036
2037
    /**
2038
     * Retrieves the parameters that identifies a value.
2039
     *
2040
     * @param mixed $value
2041
     *
2042
     * @return mixed[]
2043
     */
2044 505
    private function getValues($value)
2045
    {
2046 505
        if (is_array($value)) {
2047 12
            $newValue = [];
2048
2049 12
            foreach ($value as $itemValue) {
2050 12
                $newValue = array_merge($newValue, $this->getValues($itemValue));
2051
            }
2052
2053 12
            return [$newValue];
2054
        }
2055
2056 505
        $metadataFactory = $this->em->getMetadataFactory();
2057 505
        $unitOfWork      = $this->em->getUnitOfWork();
2058
2059 505
        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 505
        return [$this->getIndividualValue($value)];
2075
    }
2076
2077
    /**
2078
     * Retrieves an individual parameter value.
2079
     *
2080
     * @param mixed $value
2081
     *
2082
     * @return mixed
2083
     */
2084 505
    private function getIndividualValue($value)
2085
    {
2086 505
        if (! is_object($value) || ! $this->em->getMetadataFactory()->hasMetadataFor(StaticClassNameConverter::getClass($value))) {
2087 503
            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 155
    protected function getJoinSQLForAssociation(AssociationMetadata $association)
2135
    {
2136 155
        if (! $association->isOwningSide()) {
2137 150
            return 'LEFT JOIN';
2138
        }
2139
2140
        // if one of the join columns is nullable, return left join
2141 8
        foreach ($association->getJoinColumns() as $joinColumn) {
2142 8
            if (! $joinColumn->isNullable()) {
2143 1
                continue;
2144
            }
2145
2146 8
            return 'LEFT JOIN';
2147
        }
2148
2149 1
        return 'INNER JOIN';
2150
    }
2151
2152
    /**
2153
     * Gets an SQL column alias for a column name.
2154
     *
2155
     * @return string
2156
     */
2157 515
    public function getSQLColumnAlias()
2158
    {
2159 515
        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 539
    protected function generateFilterConditionSQL(ClassMetadata $targetEntity, $targetTableAlias)
2171
    {
2172 539
        $filterClauses = [];
2173
2174 539
        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 539
        $sql = implode(' AND ', $filterClauses);
2183
2184 539
        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 514
    protected function switchPersisterContext($offset, $limit)
2196
    {
2197 514
        if ($offset === null && $limit === null) {
2198 502
            $this->currentPersisterContext = $this->noLimitsContext;
2199
2200 502
            return;
2201
        }
2202
2203 38
        $this->currentPersisterContext = $this->limitsHandlingContext;
2204 38
    }
2205
}
2206