Failed Conditions
Push — master ( 7c9ab7...fa4d3b )
by Marco
13:03
created

ORM/Persisters/Entity/BasicEntityPersister.php (10 issues)

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\Driver\Statement as DriverStatement;
11
use Doctrine\DBAL\LockMode;
12
use Doctrine\DBAL\Platforms\AbstractPlatform;
13
use Doctrine\DBAL\Statement;
14
use Doctrine\DBAL\Types\Type;
15
use Doctrine\ORM\EntityManagerInterface;
16
use Doctrine\ORM\Mapping\AssociationMetadata;
17
use Doctrine\ORM\Mapping\ClassMetadata;
18
use Doctrine\ORM\Mapping\ColumnMetadata;
19
use Doctrine\ORM\Mapping\FetchMode;
20
use Doctrine\ORM\Mapping\FieldMetadata;
21
use Doctrine\ORM\Mapping\GeneratorType;
22
use Doctrine\ORM\Mapping\InheritanceType;
23
use Doctrine\ORM\Mapping\JoinColumnMetadata;
24
use Doctrine\ORM\Mapping\LocalColumnMetadata;
25
use Doctrine\ORM\Mapping\ManyToManyAssociationMetadata;
26
use Doctrine\ORM\Mapping\MappingException;
27
use Doctrine\ORM\Mapping\OneToManyAssociationMetadata;
28
use Doctrine\ORM\Mapping\ToManyAssociationMetadata;
29
use Doctrine\ORM\Mapping\ToOneAssociationMetadata;
30
use Doctrine\ORM\Mapping\VersionFieldMetadata;
31
use Doctrine\ORM\OptimisticLockException;
32
use Doctrine\ORM\PersistentCollection;
33
use Doctrine\ORM\Persisters\Exception\CantUseInOperatorOnCompositeKeys;
34
use Doctrine\ORM\Persisters\Exception\InvalidOrientation;
35
use Doctrine\ORM\Persisters\Exception\UnrecognizedField;
36
use Doctrine\ORM\Persisters\SqlExpressionVisitor;
37
use Doctrine\ORM\Persisters\SqlValueVisitor;
38
use Doctrine\ORM\Query;
39
use Doctrine\ORM\Query\QueryException;
40
use Doctrine\ORM\Repository\Exception\InvalidFindByCall;
41
use Doctrine\ORM\UnitOfWork;
42
use Doctrine\ORM\Utility\PersisterHelper;
43
use Doctrine\ORM\Utility\StaticClassNameConverter;
44
use function array_combine;
45
use function array_keys;
46
use function array_map;
47
use function array_merge;
48
use function array_values;
49
use function get_class;
50
use function implode;
51
use function in_array;
52
use function is_array;
53
use function is_object;
54
use function sprintf;
55
use function strpos;
56
use function strtoupper;
57
use function trim;
58
59
/**
60
 * A BasicEntityPersister maps an entity to a single table in a relational database.
61
 *
62
 * A persister is always responsible for a single entity type.
63
 *
64
 * EntityPersisters are used during a UnitOfWork to apply any changes to the persistent
65
 * state of entities onto a relational database when the UnitOfWork is committed,
66
 * as well as for basic querying of entities and their associations (not DQL).
67
 *
68
 * The persisting operations that are invoked during a commit of a UnitOfWork to
69
 * persist the persistent entity state are:
70
 *
71
 *   - {@link insert} : To insert the persistent state of an entity.
72
 *   - {@link update} : To update the persistent state of an entity.
73
 *   - {@link delete} : To delete the persistent state of an entity.
74
 *
75
 * As can be seen from the above list, insertions are batched and executed all at once
76
 * for increased efficiency.
77
 *
78
 * The querying operations invoked during a UnitOfWork, either through direct find
79
 * requests or lazy-loading, are the following:
80
 *
81
 *   - {@link load} : Loads (the state of) a single, managed entity.
82
 *   - {@link loadAll} : Loads multiple, managed entities.
83
 *   - {@link loadToOneEntity} : Loads a one/many-to-one entity association (lazy-loading).
84
 *   - {@link loadOneToManyCollection} : Loads a one-to-many entity association (lazy-loading).
85
 *   - {@link loadManyToManyCollection} : Loads a many-to-many entity association (lazy-loading).
86
 *
87
 * The BasicEntityPersister implementation provides the default behavior for
88
 * persisting and querying entities that are mapped to a single database table.
89
 *
90
 * Subclasses can be created to provide custom persisting and querying strategies,
91
 * i.e. spanning multiple tables.
92
 */
93
class BasicEntityPersister implements EntityPersister
94
{
95
    /** @var string[] */
96
    private static $comparisonMap = [
97
        Comparison::EQ          => '= %s',
98
        Comparison::IS          => '= %s',
99
        Comparison::NEQ         => '!= %s',
100
        Comparison::GT          => '> %s',
101
        Comparison::GTE         => '>= %s',
102
        Comparison::LT          => '< %s',
103
        Comparison::LTE         => '<= %s',
104
        Comparison::IN          => 'IN (%s)',
105
        Comparison::NIN         => 'NOT IN (%s)',
106
        Comparison::CONTAINS    => 'LIKE %s',
107
        Comparison::STARTS_WITH => 'LIKE %s',
108
        Comparison::ENDS_WITH   => 'LIKE %s',
109
    ];
110
111
    /**
112
     * Metadata object that describes the mapping of the mapped entity class.
113
     *
114
     * @var ClassMetadata
115
     */
116
    protected $class;
117
118
    /**
119
     * The underlying DBAL Connection of the used EntityManager.
120
     *
121
     * @var Connection
122
     */
123
    protected $conn;
124
125
    /**
126
     * The database platform.
127
     *
128
     * @var AbstractPlatform
129
     */
130
    protected $platform;
131
132
    /**
133
     * The EntityManager instance.
134
     *
135
     * @var EntityManagerInterface
136
     */
137
    protected $em;
138
139
    /**
140
     * The map of column names to DBAL columns used when INSERTing or UPDATEing an entity.
141
     *
142
     * @see prepareInsertData($entity)
143
     * @see prepareUpdateData($entity)
144
     *
145
     * @var ColumnMetadata[]
146
     */
147
    protected $columns = [];
148
149
    /**
150
     * The INSERT SQL statement used for entities handled by this persister.
151
     * This SQL is only generated once per request, if at all.
152
     *
153
     * @var string
154
     */
155
    private $insertSql;
156
157
    /** @var CachedPersisterContext */
158
    protected $currentPersisterContext;
159
160
    /** @var CachedPersisterContext */
161
    private $limitsHandlingContext;
162
163
    /** @var CachedPersisterContext */
164
    private $noLimitsContext;
165
166
    /**
167
     * Initializes a new <tt>BasicEntityPersister</tt> that uses the given EntityManager
168
     * and persists instances of the class described by the given ClassMetadata descriptor.
169
     */
170 1128
    public function __construct(EntityManagerInterface $em, ClassMetadata $class)
171
    {
172 1128
        $this->em                    = $em;
173 1128
        $this->class                 = $class;
174 1128
        $this->conn                  = $em->getConnection();
175 1128
        $this->platform              = $this->conn->getDatabasePlatform();
176 1128
        $this->noLimitsContext       = $this->currentPersisterContext = new CachedPersisterContext(
177 1128
            $class,
178 1128
            new Query\ResultSetMapping(),
179 1128
            false
180
        );
181 1128
        $this->limitsHandlingContext = new CachedPersisterContext(
182 1128
            $class,
183 1128
            new Query\ResultSetMapping(),
184 1128
            true
185
        );
186 1128
    }
187
188
    /**
189
     * {@inheritdoc}
190
     */
191 15
    public function getClassMetadata()
192
    {
193 15
        return $this->class;
194
    }
195
196
    /**
197
     * {@inheritdoc}
198
     */
199 11
    public function getResultSetMapping()
200
    {
201 11
        return $this->currentPersisterContext->rsm;
202
    }
203
204
    /**
205
     * {@inheritdoc}
206
     */
207 1051
    public function getIdentifier($entity) : array
208
    {
209 1051
        $id = [];
210
211 1051
        foreach ($this->class->getIdentifier() as $fieldName) {
212 1051
            $property = $this->class->getProperty($fieldName);
213 1051
            $value    = $property->getValue($entity);
214
215 1051
            if ($value !== null) {
216 1051
                $id[$fieldName] = $value;
217
            }
218
        }
219
220 1051
        return $id;
221
    }
222
223
    /**
224
     * Populates the entity identifier of an entity.
225
     *
226
     * @param object  $entity
227
     * @param mixed[] $id
228
     */
229 218
    public function setIdentifier($entity, array $id) : void
230
    {
231 218
        foreach ($id as $idField => $idValue) {
232 218
            $property = $this->class->getProperty($idField);
233
234 218
            $property->setValue($entity, $idValue);
235
        }
236 218
    }
237
238
    /**
239
     * {@inheritdoc}
240
     */
241 911
    public function insert($entity)
242
    {
243 911
        $stmt           = $this->conn->prepare($this->getInsertSQL());
244 911
        $tableName      = $this->class->getTableName();
245 911
        $insertData     = $this->prepareInsertData($entity);
246 911
        $generationPlan = $this->class->getValueGenerationPlan();
247
248 911
        if (isset($insertData[$tableName])) {
249 886
            $paramIndex = 1;
250
251 886
            foreach ($insertData[$tableName] as $columnName => $value) {
252 886
                $type = $this->columns[$columnName]->getType();
253
254 886
                $stmt->bindValue($paramIndex++, $value, $type);
255
            }
256
        }
257
258 911
        $stmt->execute();
259
260 910
        if ($generationPlan->containsDeferred()) {
261 854
            $generationPlan->executeDeferred($this->em, $entity);
262
        }
263
264 910
        if ($this->class->isVersioned()) {
265 198
            $this->assignDefaultVersionValue($entity, $this->getIdentifier($entity));
266
        }
267
268 910
        $stmt->closeCursor();
269 910
    }
270
271
    /**
272
     * Retrieves the default version value which was created
273
     * by the preceding INSERT statement and assigns it back in to the
274
     * entities version field.
275
     *
276
     * @param object  $entity
277
     * @param mixed[] $id
278
     */
279 207
    protected function assignDefaultVersionValue($entity, array $id)
280
    {
281 207
        $versionProperty = $this->class->versionProperty;
282 207
        $versionValue    = $this->fetchVersionValue($versionProperty, $id);
283
284 207
        $versionProperty->setValue($entity, $versionValue);
285 207
    }
286
287
    /**
288
     * Fetches the current version value of a versioned entity.
289
     *
290
     * @param mixed[] $id
291
     *
292
     * @return mixed
293
     */
294 207
    protected function fetchVersionValue(VersionFieldMetadata $versionProperty, array $id)
295
    {
296 207
        $versionedClass = $versionProperty->getDeclaringClass();
297 207
        $tableName      = $versionedClass->table->getQuotedQualifiedName($this->platform);
298 207
        $columnName     = $this->platform->quoteIdentifier($versionProperty->getColumnName());
299 207
        $identifier     = array_map(
300
            function ($columnName) {
301 207
                return $this->platform->quoteIdentifier($columnName);
302 207
            },
303 207
            array_keys($versionedClass->getIdentifierColumns($this->em))
304
        );
305
306
        // FIXME: Order with composite keys might not be correct
307 207
        $sql = 'SELECT ' . $columnName
308 207
             . ' FROM ' . $tableName
309 207
             . ' WHERE ' . implode(' = ? AND ', $identifier) . ' = ?';
310
311 207
        $flattenedId = $this->em->getIdentifierFlattener()->flattenIdentifier($versionedClass, $id);
312 207
        $versionType = $versionProperty->getType();
313
314 207
        $value = $this->conn->fetchColumn(
315 207
            $sql,
316 207
            array_values($flattenedId),
317 207
            0,
318 207
            $this->extractIdentifierTypes($id, $versionedClass)
319
        );
320
321 207
        return $versionType->convertToPHPValue($value, $this->platform);
322
    }
323
324
    /**
325
     * @param mixed[] $id
326
     *
327
     * @return mixed[]
328
     */
329 207
    private function extractIdentifierTypes(array $id, ClassMetadata $versionedClass) : array
330
    {
331 207
        $types = [];
332
333 207
        foreach ($id as $field => $value) {
334 207
            $types = array_merge($types, $this->getTypes($field, $value, $versionedClass));
335
        }
336
337 207
        return $types;
338
    }
339
340
    /**
341
     * {@inheritdoc}
342
     */
343 79
    public function update($entity)
344
    {
345 79
        $tableName  = $this->class->getTableName();
346 79
        $updateData = $this->prepareUpdateData($entity);
347
348 79
        if (! isset($updateData[$tableName])) {
349 8
            return;
350
        }
351
352 71
        $data = $updateData[$tableName];
353
354 71
        if (! $data) {
355
            return;
356
        }
357
358 71
        $isVersioned     = $this->class->isVersioned();
359 71
        $quotedTableName = $this->class->table->getQuotedQualifiedName($this->platform);
360
361 71
        $this->updateTable($entity, $quotedTableName, $data, $isVersioned);
362
363 69
        if ($isVersioned) {
364 12
            $id = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
365
366 12
            $this->assignDefaultVersionValue($entity, $id);
367
        }
368 69
    }
369
370
    /**
371
     * {@inheritdoc}
372
     */
373 58
    public function delete($entity)
374
    {
375 58
        $class      = $this->class;
376 58
        $unitOfWork = $this->em->getUnitOfWork();
377 58
        $identifier = $unitOfWork->getEntityIdentifier($entity);
378 58
        $tableName  = $class->table->getQuotedQualifiedName($this->platform);
379
380 58
        $types = [];
381 58
        $id    = [];
382
383 58
        foreach ($class->identifier as $field) {
384 58
            $property = $class->getProperty($field);
385
386 58
            if ($property instanceof FieldMetadata) {
387 56
                $columnName       = $property->getColumnName();
388 56
                $quotedColumnName = $this->platform->quoteIdentifier($columnName);
389
390 56
                $id[$quotedColumnName] = $identifier[$field];
391 56
                $types[]               = $property->getType();
392
393 56
                continue;
394
            }
395
396 5
            $targetClass = $this->em->getClassMetadata($property->getTargetEntity());
397 5
            $joinColumns = $property instanceof ManyToManyAssociationMetadata
398
                ? $property->getTable()->getJoinColumns()
399 5
                : $property->getJoinColumns();
400
401 5
            $associationValue = null;
402 5
            $value            = $identifier[$field];
403
404 5
            if ($value !== null) {
405
                // @todo guilhermeblanco Make sure we do not have flat association values.
406 5
                if (! is_array($value)) {
407 5
                    $value = [$targetClass->identifier[0] => $value];
408
                }
409
410 5
                $associationValue = $value;
411
            }
412
413 5
            foreach ($joinColumns as $joinColumn) {
414
                /** @var JoinColumnMetadata $joinColumn */
415 5
                $quotedColumnName     = $this->platform->quoteIdentifier($joinColumn->getColumnName());
416 5
                $referencedColumnName = $joinColumn->getReferencedColumnName();
417 5
                $targetField          = $targetClass->fieldNames[$referencedColumnName];
418
419 5
                if (! $joinColumn->getType()) {
420
                    $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
0 ignored issues
show
$targetClass of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Utility\Per...lper::getTypeOfColumn(). ( Ignorable by Annotation )

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

420
                    $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, /** @scrutinizer ignore-type */ $targetClass, $this->em));
Loading history...
421
                }
422
423 5
                $id[$quotedColumnName] = $associationValue ? $associationValue[$targetField] : null;
424 5
                $types[]               = $joinColumn->getType();
425
            }
426
        }
427
428 58
        $this->deleteJoinTableRecords($identifier);
429
430 58
        return (bool) $this->conn->delete($tableName, $id, $types);
431
    }
432
433
    /**
434
     * Performs an UPDATE statement for an entity on a specific table.
435
     * The UPDATE can optionally be versioned, which requires the entity to have a version field.
436
     *
437
     * @param object  $entity          The entity object being updated.
438
     * @param string  $quotedTableName The quoted name of the table to apply the UPDATE on.
439
     * @param mixed[] $updateData      The map of columns to update (column => value).
440
     * @param bool    $versioned       Whether the UPDATE should be versioned.
441
     *
442
     * @throws ORMException
443
     * @throws OptimisticLockException
444
     */
445 101
    final protected function updateTable($entity, $quotedTableName, array $updateData, $versioned = false)
446
    {
447 101
        $set    = [];
448 101
        $types  = [];
449 101
        $params = [];
450
451 101
        foreach ($updateData as $columnName => $value) {
452 101
            $column           = $this->columns[$columnName];
453 101
            $quotedColumnName = $this->platform->quoteIdentifier($column->getColumnName());
454 101
            $type             = $column->getType();
455 101
            $placeholder      = $type->convertToDatabaseValueSQL('?', $this->platform);
456
457 101
            $set[]    = sprintf('%s = %s', $quotedColumnName, $placeholder);
458 101
            $params[] = $value;
459 101
            $types[]  = $column->getType();
460
        }
461
462
        // @todo guilhermeblanco Bring this back: $this->em->getUnitOfWork()->getEntityIdentifier($entity);
463 101
        $identifier = $this->getIdentifier($entity);
464 101
        $where      = [];
465
466 101
        foreach ($this->class->identifier as $idField) {
467 101
            $property = $this->class->getProperty($idField);
468
469
            switch (true) {
470 101
                case $property instanceof FieldMetadata:
471 96
                    $where[]  = $this->platform->quoteIdentifier($property->getColumnName());
472 96
                    $params[] = $identifier[$idField];
473 96
                    $types[]  = $property->getType();
474 96
                    break;
475
476 6
                case $property instanceof ToOneAssociationMetadata:
477 6
                    $targetClass     = $this->em->getClassMetadata($property->getTargetEntity());
478 6
                    $targetPersister = $this->em->getUnitOfWork()->getEntityPersister($property->getTargetEntity());
479
480 6
                    foreach ($property->getJoinColumns() as $joinColumn) {
481
                        /** @var JoinColumnMetadata $joinColumn */
482 6
                        $quotedColumnName     = $this->platform->quoteIdentifier($joinColumn->getColumnName());
483 6
                        $referencedColumnName = $joinColumn->getReferencedColumnName();
484
485 6
                        if (! $joinColumn->getType()) {
486
                            $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
0 ignored issues
show
$targetClass of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Utility\Per...lper::getTypeOfColumn(). ( Ignorable by Annotation )

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

486
                            $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, /** @scrutinizer ignore-type */ $targetClass, $this->em));
Loading history...
487
                        }
488
489 6
                        $value = $targetPersister->getColumnValue($identifier[$idField], $referencedColumnName);
490
491 6
                        $where[]  = $quotedColumnName;
492 6
                        $params[] = $value;
493 6
                        $types[]  = $joinColumn->getType();
494
                    }
495 101
                    break;
496
            }
497
        }
498
499 101
        if ($versioned) {
500 20
            $versionProperty   = $this->class->versionProperty;
501 20
            $versionColumnType = $versionProperty->getType();
502 20
            $versionColumnName = $this->platform->quoteIdentifier($versionProperty->getColumnName());
503
504 20
            $where[]  = $versionColumnName;
505 20
            $types[]  = $versionColumnType;
506 20
            $params[] = $versionProperty->getValue($entity);
507
508 20
            switch ($versionColumnType->getName()) {
509
                case Type::SMALLINT:
510
                case Type::INTEGER:
511
                case Type::BIGINT:
512 18
                    $set[] = $versionColumnName . ' = ' . $versionColumnName . ' + 1';
513 18
                    break;
514
515
                case Type::DATETIME:
516 2
                    $set[] = $versionColumnName . ' = CURRENT_TIMESTAMP';
517 2
                    break;
518
            }
519
        }
520
521 101
        $sql = 'UPDATE ' . $quotedTableName
522 101
             . ' SET ' . implode(', ', $set)
523 101
             . ' WHERE ' . implode(' = ? AND ', $where) . ' = ?';
524
525 101
        $result = $this->conn->executeUpdate($sql, $params, $types);
526
527 101
        if ($versioned && ! $result) {
528 4
            throw OptimisticLockException::lockFailed($entity);
529
        }
530 98
    }
531
532
    /**
533
     * @param mixed[] $identifier
534
     *
535
     * @todo Add check for platform if it supports foreign keys/cascading.
536
     */
537 62
    protected function deleteJoinTableRecords($identifier)
538
    {
539 62
        foreach ($this->class->getDeclaredPropertiesIterator() as $association) {
540 62
            if (! ($association instanceof ManyToManyAssociationMetadata)) {
541 62
                continue;
542
            }
543
544
            // @Todo this only covers scenarios with no inheritance or of the same level. Is there something
545
            // like self-referential relationship between different levels of an inheritance hierarchy? I hope not!
546 23
            $selfReferential   = $association->getTargetEntity() === $association->getSourceEntity();
547 23
            $owningAssociation = $association;
548 23
            $otherColumns      = [];
549 23
            $otherKeys         = [];
550 23
            $keys              = [];
551
552 23
            if (! $owningAssociation->isOwningSide()) {
553 6
                $class             = $this->em->getClassMetadata($association->getTargetEntity());
554 6
                $owningAssociation = $class->getProperty($association->getMappedBy());
555
            }
556
557 23
            $joinTable     = $owningAssociation->getJoinTable();
558 23
            $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
559 23
            $joinColumns   = $association->isOwningSide()
560 19
                ? $joinTable->getJoinColumns()
561 23
                : $joinTable->getInverseJoinColumns();
562
563 23
            if ($selfReferential) {
564 1
                $otherColumns = ! $association->isOwningSide()
565
                    ? $joinTable->getJoinColumns()
566 1
                    : $joinTable->getInverseJoinColumns();
567
            }
568
569 23
            $isOnDeleteCascade = false;
570
571 23
            foreach ($joinColumns as $joinColumn) {
572 23
                $keys[] = $this->platform->quoteIdentifier($joinColumn->getColumnName());
573
574 23
                if ($joinColumn->isOnDeleteCascade()) {
575 23
                    $isOnDeleteCascade = true;
576
                }
577
            }
578
579 23
            foreach ($otherColumns as $joinColumn) {
580 1
                $otherKeys[] = $this->platform->quoteIdentifier($joinColumn->getColumnName());
581
582 1
                if ($joinColumn->isOnDeleteCascade()) {
583 1
                    $isOnDeleteCascade = true;
584
                }
585
            }
586
587 23
            if ($isOnDeleteCascade) {
588 5
                continue;
589
            }
590
591 19
            $this->conn->delete($joinTableName, array_combine($keys, $identifier));
592
593 19
            if ($selfReferential) {
594 19
                $this->conn->delete($joinTableName, array_combine($otherKeys, $identifier));
595
            }
596
        }
597 62
    }
598
599
    /**
600
     * Prepares the data changeset of a managed entity for database insertion (initial INSERT).
601
     * The changeset of the entity is obtained from the currently running UnitOfWork.
602
     *
603
     * The default insert data preparation is the same as for updates.
604
     *
605
     * @param object $entity The entity for which to prepare the data.
606
     *
607
     * @return mixed[] The prepared data for the tables to update.
608
     */
609 998
    protected function prepareInsertData($entity) : array
610
    {
611 998
        return $this->prepareUpdateData($entity);
612
    }
613
614
    /**
615
     * Prepares the changeset of an entity for database insertion (UPDATE).
616
     *
617
     * The changeset is obtained from the currently running UnitOfWork.
618
     *
619
     * During this preparation the array that is passed as the second parameter is filled with
620
     * <columnName> => <value> pairs, grouped by table name.
621
     *
622
     * Example:
623
     * <code>
624
     * array(
625
     *    'foo_table' => array('column1' => 'value1', 'column2' => 'value2', ...),
626
     *    'bar_table' => array('columnX' => 'valueX', 'columnY' => 'valueY', ...),
627
     *    ...
628
     * )
629
     * </code>
630
     *
631
     * @param object $entity The entity for which to prepare the data.
632
     *
633
     * @return mixed[] The prepared data.
634
     */
635 1003
    protected function prepareUpdateData($entity)
636
    {
637 1003
        $uow                 = $this->em->getUnitOfWork();
638 1003
        $result              = [];
639 1003
        $versionPropertyName = $this->class->isVersioned()
640 211
            ? $this->class->versionProperty->getName()
641 1003
            : null;
642
643
        // @todo guilhermeblanco This should check column insertability/updateability instead of field changeset
644 1003
        foreach ($uow->getEntityChangeSet($entity) as $propertyName => $propertyChangeSet) {
645 969
            if ($versionPropertyName === $propertyName) {
646
                continue;
647
            }
648
649 969
            $property = $this->class->getProperty($propertyName);
650 969
            $newValue = $propertyChangeSet[1];
651
652 969
            if ($property instanceof FieldMetadata) {
653
                // @todo guilhermeblanco Please remove this in the future for good...
654 933
                $this->columns[$property->getColumnName()] = $property;
655
656 933
                $result[$property->getTableName()][$property->getColumnName()] = $newValue;
657
658 933
                continue;
659
            }
660
661
            // Only owning side of x-1 associations can have a FK column.
662 829
            if (! $property instanceof ToOneAssociationMetadata || ! $property->isOwningSide()) {
663 8
                continue;
664
            }
665
666
            // The associated entity $newVal is not yet persisted, so we must
667
            // set $newVal = null, in order to insert a null value and schedule an
668
            // extra update on the UnitOfWork.
669 829
            if ($newValue !== null && $uow->isScheduledForInsert($newValue)) {
670 28
                $uow->scheduleExtraUpdate($entity, [$propertyName => [null, $newValue]]);
671
672 28
                $newValue = null;
673
            }
674
675 829
            $targetClass     = $this->em->getClassMetadata($property->getTargetEntity());
676 829
            $targetPersister = $uow->getEntityPersister($targetClass->getClassName());
677
678 829
            foreach ($property->getJoinColumns() as $joinColumn) {
679
                /** @var JoinColumnMetadata $joinColumn */
680 829
                $referencedColumnName = $joinColumn->getReferencedColumnName();
681
682 829
                if (! $joinColumn->getType()) {
683 9
                    $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
0 ignored issues
show
$targetClass of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Utility\Per...lper::getTypeOfColumn(). ( Ignorable by Annotation )

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

683
                    $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, /** @scrutinizer ignore-type */ $targetClass, $this->em));
Loading history...
684
                }
685
686
                // @todo guilhermeblanco Please remove this in the future for good...
687 829
                $this->columns[$joinColumn->getColumnName()] = $joinColumn;
688
689 829
                $result[$joinColumn->getTableName()][$joinColumn->getColumnName()] = $newValue !== null
690 611
                    ? $targetPersister->getColumnValue($newValue, $referencedColumnName)
691 829
                    : null;
692
            }
693
        }
694
695 1003
        return $result;
696
    }
697
698
    /**
699
     * @param object $entity
700
     *
701
     * @return mixed|null
702
     */
703 611
    public function getColumnValue($entity, string $columnName)
704
    {
705
        // Looking for fields by column is the easiest way to look at local columns or x-1 owning side associations
706 611
        $propertyName = $this->class->fieldNames[$columnName];
707 611
        $property     = $this->class->getProperty($propertyName);
708
709 611
        if (! $property) {
710
            return null;
711
        }
712
713 611
        $propertyValue = $property->getValue($entity);
714
715 611
        if ($property instanceof LocalColumnMetadata) {
716 611
            return $propertyValue;
717
        }
718
719
        /** @var ToOneAssociationMetadata $property */
720 20
        $unitOfWork      = $this->em->getUnitOfWork();
721 20
        $targetClass     = $this->em->getClassMetadata($property->getTargetEntity());
722 20
        $targetPersister = $unitOfWork->getEntityPersister($property->getTargetEntity());
723
724 20
        foreach ($property->getJoinColumns() as $joinColumn) {
725
            /** @var JoinColumnMetadata $joinColumn */
726 20
            $referencedColumnName = $joinColumn->getReferencedColumnName();
727
728 20
            if (! $joinColumn->getType()) {
729
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
0 ignored issues
show
$targetClass of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Utility\Per...lper::getTypeOfColumn(). ( Ignorable by Annotation )

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

729
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, /** @scrutinizer ignore-type */ $targetClass, $this->em));
Loading history...
730
            }
731
732 20
            if ($joinColumn->getColumnName() !== $columnName) {
733
                continue;
734
            }
735
736 20
            return $targetPersister->getColumnValue($propertyValue, $referencedColumnName);
737
        }
738
739
        return null;
740
    }
741
742
    /**
743
     * {@inheritdoc}
744
     */
745 470
    public function load(
746
        array $criteria,
747
        $entity = null,
748
        ?AssociationMetadata $association = null,
749
        array $hints = [],
750
        $lockMode = null,
751
        $limit = null,
752
        array $orderBy = []
753
    ) {
754 470
        $this->switchPersisterContext(null, $limit);
755
756 470
        $sql = $this->getSelectSQL($criteria, $association, $lockMode, $limit, null, $orderBy);
757
758 469
        [$params, $types] = $this->expandParameters($criteria);
759
760 469
        $stmt = $this->conn->executeQuery($sql, $params, $types);
761
762 469
        if ($entity !== null) {
763 62
            $hints[Query::HINT_REFRESH]        = true;
764 62
            $hints[Query::HINT_REFRESH_ENTITY] = $entity;
765
        }
766
767 469
        $hydrator = $this->em->newHydrator($this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
768 469
        $entities = $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, $hints);
769
770 469
        return $entities ? $entities[0] : null;
771
    }
772
773
    /**
774
     * {@inheritdoc}
775
     */
776 396
    public function loadById(array $identifier, $entity = null)
777
    {
778 396
        return $this->load($identifier, $entity);
779
    }
780
781
    /**
782
     * {@inheritdoc}
783
     */
784 93
    public function loadToOneEntity(ToOneAssociationMetadata $association, $sourceEntity, array $identifier = [])
785
    {
786 93
        $unitOfWork   = $this->em->getUnitOfWork();
787 93
        $targetEntity = $association->getTargetEntity();
788 93
        $foundEntity  = $unitOfWork->tryGetById($identifier, $targetEntity);
789
790 93
        if ($foundEntity !== false) {
791
            return $foundEntity;
792
        }
793
794 93
        $targetClass = $this->em->getClassMetadata($targetEntity);
795
796 93
        if ($association->isOwningSide()) {
797 29
            $inversedBy            = $association->getInversedBy();
798 29
            $targetProperty        = $inversedBy ? $targetClass->getProperty($inversedBy) : null;
799 29
            $isInverseSingleValued = $targetProperty && $targetProperty instanceof ToOneAssociationMetadata;
800
801
            // Mark inverse side as fetched in the hints, otherwise the UoW would
802
            // try to load it in a separate query (remember: to-one inverse sides can not be lazy).
803 29
            $hints = [];
804
805 29
            if ($isInverseSingleValued) {
806
                $hints['fetched']['r'][$inversedBy] = true;
807
            }
808
809
            /* cascade read-only status
810
            if ($this->em->getUnitOfWork()->isReadOnly($sourceEntity)) {
811
                $hints[Query::HINT_READ_ONLY] = true;
812
            }
813
            */
814
815 29
            $entity = $this->load($identifier, null, $association, $hints);
816
817
            // Complete bidirectional association, if necessary
818 29
            if ($entity !== null && $isInverseSingleValued) {
819
                $targetProperty->setValue($entity, $sourceEntity);
820
            }
821
822 29
            return $entity;
823
        }
824
825 64
        $sourceClass       = $association->getDeclaringClass();
826 64
        $owningAssociation = $targetClass->getProperty($association->getMappedBy());
827 64
        $targetTableAlias  = $this->getSQLTableAlias($targetClass->getTableName());
828
829 64
        foreach ($owningAssociation->getJoinColumns() as $joinColumn) {
830 64
            $sourceKeyColumn = $joinColumn->getReferencedColumnName();
831 64
            $targetKeyColumn = $joinColumn->getColumnName();
832
833 64
            if (! isset($sourceClass->fieldNames[$sourceKeyColumn])) {
834
                throw MappingException::joinColumnMustPointToMappedField(
835
                    $sourceClass->getClassName(),
836
                    $sourceKeyColumn
837
                );
838
            }
839
840 64
            $property = $sourceClass->getProperty($sourceClass->fieldNames[$sourceKeyColumn]);
841 64
            $value    = $property->getValue($sourceEntity);
842
843
            // unset the old value and set the new sql aliased value here. By definition
844
            // unset($identifier[$targetKeyColumn] works here with how UnitOfWork::createEntity() calls this method.
845
            // @todo guilhermeblanco In master we have: $identifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
846 64
            unset($identifier[$targetKeyColumn]);
847
848 64
            $identifier[$targetClass->fieldNames[$targetKeyColumn]] = $value;
849
        }
850
851 64
        $entity = $this->load($identifier, null, $association);
852
853 64
        if ($entity !== null) {
854 17
            $owningAssociation->setValue($entity, $sourceEntity);
855
        }
856
857 64
        return $entity;
858
    }
859
860
    /**
861
     * {@inheritdoc}
862
     */
863 15
    public function refresh(array $id, $entity, $lockMode = null)
864
    {
865 15
        $sql              = $this->getSelectSQL($id, null, $lockMode);
866 15
        [$params, $types] = $this->expandParameters($id);
867 15
        $stmt             = $this->conn->executeQuery($sql, $params, $types);
868
869 15
        $hydrator = $this->em->newHydrator(Query::HYDRATE_OBJECT);
870 15
        $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [Query::HINT_REFRESH => true]);
871 15
    }
872
873
    /**
874
     * {@inheritDoc}
875
     */
876 46
    public function count($criteria = [])
877
    {
878 46
        $sql = $this->getCountSQL($criteria);
879
880 46
        [$params, $types] = $criteria instanceof Criteria
881 25
            ? $this->expandCriteriaParameters($criteria)
882 46
            : $this->expandParameters($criteria);
883
884 46
        return (int) $this->conn->executeQuery($sql, $params, $types)->fetchColumn();
885
    }
886
887
    /**
888
     * {@inheritdoc}
889
     */
890 8
    public function loadCriteria(Criteria $criteria)
891
    {
892 8
        $orderBy = $criteria->getOrderings();
893 8
        $limit   = $criteria->getMaxResults();
894 8
        $offset  = $criteria->getFirstResult();
895 8
        $query   = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy);
896
897 6
        [$params, $types] = $this->expandCriteriaParameters($criteria);
898
899 6
        $stmt         = $this->conn->executeQuery($query, $params, $types);
900 6
        $rsm          = $this->currentPersisterContext->rsm;
901 6
        $hints        = [UnitOfWork::HINT_DEFEREAGERLOAD => true];
902 6
        $hydratorType = $this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT;
903 6
        $hydrator     = $this->em->newHydrator($hydratorType);
904
905 6
        return $hydrator->hydrateAll($stmt, $rsm, $hints);
906
    }
907
908
    /**
909
     * {@inheritdoc}
910
     */
911 37
    public function expandCriteriaParameters(Criteria $criteria)
912
    {
913 37
        $expression = $criteria->getWhereExpression();
914 37
        $sqlParams  = [];
915 37
        $sqlTypes   = [];
916
917 37
        if ($expression === null) {
918 2
            return [$sqlParams, $sqlTypes];
919
        }
920
921 36
        $valueVisitor = new SqlValueVisitor();
922
923 36
        $valueVisitor->dispatch($expression);
924
925 36
        [$params, $types] = $valueVisitor->getParamsAndTypes();
926
927 36
        foreach ($params as $param) {
928 32
            $sqlParams = array_merge($sqlParams, $this->getValues($param));
929
        }
930
931 36
        foreach ($types as $type) {
932 32
            [$field, $value] = $type;
933 32
            $sqlTypes        = array_merge($sqlTypes, $this->getTypes($field, $value, $this->class));
934
        }
935
936 36
        return [$sqlParams, $sqlTypes];
937
    }
938
939
    /**
940
     * {@inheritdoc}
941
     */
942 70
    public function loadAll(array $criteria = [], array $orderBy = [], $limit = null, $offset = null)
943
    {
944 70
        $this->switchPersisterContext($offset, $limit);
945
946 70
        $sql = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy);
947
948 66
        [$params, $types] = $this->expandParameters($criteria);
949
950 66
        $stmt         = $this->conn->executeQuery($sql, $params, $types);
951 66
        $rsm          = $this->currentPersisterContext->rsm;
952 66
        $hints        = [UnitOfWork::HINT_DEFEREAGERLOAD => true];
953 66
        $hydratorType = $this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT;
954 66
        $hydrator     = $this->em->newHydrator($hydratorType);
955
956 66
        return $hydrator->hydrateAll($stmt, $rsm, $hints);
957
    }
958
959
    /**
960
     * {@inheritdoc}
961
     */
962 8
    public function getManyToManyCollection(
963
        ManyToManyAssociationMetadata $association,
964
        $sourceEntity,
965
        $offset = null,
966
        $limit = null
967
    ) {
968 8
        $this->switchPersisterContext($offset, $limit);
969
970 8
        $stmt = $this->getManyToManyStatement($association, $sourceEntity, $offset, $limit);
971
972 8
        return $this->loadArrayFromStatement($association, $stmt);
973
    }
974
975
    /**
976
     * {@inheritdoc}
977
     */
978 73
    public function loadManyToManyCollection(
979
        ManyToManyAssociationMetadata $association,
980
        $sourceEntity,
981
        PersistentCollection $collection
982
    ) {
983 73
        $stmt = $this->getManyToManyStatement($association, $sourceEntity);
984
985 73
        return $this->loadCollectionFromStatement($association, $stmt, $collection);
986
    }
987
988
    /**
989
     * Loads an array of entities from a given DBAL statement.
990
     *
991
     * @param Statement $stmt
992
     *
993
     * @return mixed[]
994
     */
995 13
    private function loadArrayFromStatement(ToManyAssociationMetadata $association, $stmt)
996
    {
997 13
        $rsm = $this->currentPersisterContext->rsm;
998
999 13
        if ($association->getIndexedBy()) {
1000 7
            $rsm = clone $this->currentPersisterContext->rsm; // this is necessary because the "default rsm" should be changed.
1001 7
            $rsm->addIndexBy('r', $association->getIndexedBy());
1002
        }
1003
1004 13
        $hydrator = $this->em->newHydrator(Query::HYDRATE_OBJECT);
1005 13
        $hints    = [UnitOfWork::HINT_DEFEREAGERLOAD => true];
1006
1007 13
        return $hydrator->hydrateAll($stmt, $rsm, $hints);
1008
    }
1009
1010
    /**
1011
     * Hydrates a collection from a given DBAL statement.
1012
     *
1013
     * @param Statement            $stmt
1014
     * @param PersistentCollection $collection
1015
     *
1016
     * @return mixed[]
1017
     */
1018 136
    private function loadCollectionFromStatement(ToManyAssociationMetadata $association, $stmt, $collection)
1019
    {
1020 136
        $rsm = $this->currentPersisterContext->rsm;
1021
1022 136
        if ($association->getIndexedBy()) {
1023 10
            $rsm = clone $this->currentPersisterContext->rsm; // this is necessary because the "default rsm" should be changed.
1024 10
            $rsm->addIndexBy('r', $association->getIndexedBy());
1025
        }
1026
1027 136
        $hydrator = $this->em->newHydrator(Query::HYDRATE_OBJECT);
1028
        $hints    = [
1029 136
            UnitOfWork::HINT_DEFEREAGERLOAD => true,
1030 136
            'collection' => $collection,
1031
        ];
1032
1033 136
        return $hydrator->hydrateAll($stmt, $rsm, $hints);
1034
    }
1035
1036
    /**
1037
     * @param object   $sourceEntity
1038
     * @param int|null $offset
1039
     * @param int|null $limit
1040
     *
1041
     * @return DriverStatement
1042
     *
1043
     * @throws MappingException
1044
     */
1045 80
    private function getManyToManyStatement(
1046
        ManyToManyAssociationMetadata $association,
1047
        $sourceEntity,
1048
        $offset = null,
1049
        $limit = null
1050
    ) {
1051 80
        $this->switchPersisterContext($offset, $limit);
1052
1053
        /** @var ClassMetadata $sourceClass */
1054 80
        $sourceClass = $this->em->getClassMetadata($association->getSourceEntity());
1055 80
        $class       = $sourceClass;
1056 80
        $owningAssoc = $association;
1057 80
        $criteria    = [];
1058 80
        $parameters  = [];
1059
1060 80
        if (! $association->isOwningSide()) {
1061 12
            $class       = $this->em->getClassMetadata($association->getTargetEntity());
1062 12
            $owningAssoc = $class->getProperty($association->getMappedBy());
1063
        }
1064
1065 80
        $joinTable     = $owningAssoc->getJoinTable();
1066 80
        $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
1067 80
        $joinColumns   = $association->isOwningSide()
1068 73
            ? $joinTable->getJoinColumns()
1069 80
            : $joinTable->getInverseJoinColumns();
1070
1071 80
        foreach ($joinColumns as $joinColumn) {
1072 80
            $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName());
1073 80
            $fieldName        = $sourceClass->fieldNames[$joinColumn->getReferencedColumnName()];
1074 80
            $property         = $sourceClass->getProperty($fieldName);
1075 80
            $value            = null;
1076
1077 80
            if ($property instanceof FieldMetadata) {
1078 79
                $value = $property->getValue($sourceEntity);
1079 4
            } elseif ($property instanceof AssociationMetadata) {
1080 4
                $property    = $sourceClass->getProperty($fieldName);
1081 4
                $targetClass = $this->em->getClassMetadata($property->getTargetEntity());
1082 4
                $value       = $property->getValue($sourceEntity);
1083
1084 4
                $value = $this->em->getUnitOfWork()->getEntityIdentifier($value);
1085 4
                $value = $value[$targetClass->identifier[0]];
1086
            }
1087
1088 80
            $criteria[$joinTableName . '.' . $quotedColumnName] = $value;
1089 80
            $parameters[]                                       = [
1090 80
                'value' => $value,
1091 80
                'field' => $fieldName,
1092 80
                'class' => $sourceClass,
1093
            ];
1094
        }
1095
1096 80
        $sql = $this->getSelectSQL($criteria, $association, null, $limit, $offset);
1097
1098 80
        [$params, $types] = $this->expandToManyParameters($parameters);
1099
1100 80
        return $this->conn->executeQuery($sql, $params, $types);
1101
    }
1102
1103
    /**
1104
     * {@inheritdoc}
1105
     */
1106 521
    public function getSelectSQL(
1107
        $criteria,
1108
        ?AssociationMetadata $association = null,
1109
        $lockMode = null,
1110
        $limit = null,
1111
        $offset = null,
1112
        array $orderBy = []
1113
    ) {
1114 521
        $this->switchPersisterContext($offset, $limit);
1115
1116 521
        $lockSql    = '';
1117 521
        $joinSql    = '';
1118 521
        $orderBySql = '';
1119
1120 521
        if ($association instanceof ManyToManyAssociationMetadata) {
1121 81
            $joinSql = $this->getSelectManyToManyJoinSQL($association);
1122
        }
1123
1124 521
        if ($association instanceof ToManyAssociationMetadata && $association->getOrderBy()) {
1125 5
            $orderBy = $association->getOrderBy();
1126
        }
1127
1128 521
        if ($orderBy) {
1129 11
            $orderBySql = $this->getOrderBySQL($orderBy, $this->getSQLTableAlias($this->class->getTableName()));
1130
        }
1131
1132 519
        $conditionSql = $criteria instanceof Criteria
1133 8
            ? $this->getSelectConditionCriteriaSQL($criteria)
1134 517
            : $this->getSelectConditionSQL($criteria, $association);
1135
1136
        switch ($lockMode) {
1137 514
            case LockMode::PESSIMISTIC_READ:
1138
                $lockSql = ' ' . $this->platform->getReadLockSQL();
1139
                break;
1140
1141 514
            case LockMode::PESSIMISTIC_WRITE:
1142
                $lockSql = ' ' . $this->platform->getWriteLockSQL();
1143
                break;
1144
        }
1145
1146 514
        $columnList = $this->getSelectColumnsSQL();
1147 514
        $tableAlias = $this->getSQLTableAlias($this->class->getTableName());
1148 514
        $filterSql  = $this->generateFilterConditionSQL($this->class, $tableAlias);
1149 514
        $tableName  = $this->class->table->getQuotedQualifiedName($this->platform);
1150
1151 514
        if ($filterSql !== '') {
1152 12
            $conditionSql = $conditionSql
1153 11
                ? $conditionSql . ' AND ' . $filterSql
1154 12
                : $filterSql;
1155
        }
1156
1157 514
        $select = 'SELECT ' . $columnList;
1158 514
        $from   = ' FROM ' . $tableName . ' ' . $tableAlias;
1159 514
        $join   = $this->currentPersisterContext->selectJoinSql . $joinSql;
1160 514
        $where  = ($conditionSql ? ' WHERE ' . $conditionSql : '');
1161 514
        $lock   = $this->platform->appendLockHint($from, $lockMode);
1162
        $query  = $select
1163 514
            . $lock
1164 514
            . $join
1165 514
            . $where
1166 514
            . $orderBySql;
1167
1168 514
        return $this->platform->modifyLimitQuery($query, $limit, $offset) . $lockSql;
1169
    }
1170
1171
    /**
1172
     * {@inheritDoc}
1173
     */
1174 41
    public function getCountSQL($criteria = [])
1175
    {
1176 41
        $tableName  = $this->class->table->getQuotedQualifiedName($this->platform);
1177 41
        $tableAlias = $this->getSQLTableAlias($this->class->getTableName());
1178
1179 41
        $conditionSql = $criteria instanceof Criteria
1180 25
            ? $this->getSelectConditionCriteriaSQL($criteria)
1181 41
            : $this->getSelectConditionSQL($criteria);
1182
1183 41
        $filterSql = $this->generateFilterConditionSQL($this->class, $tableAlias);
1184
1185 41
        if ($filterSql !== '') {
1186 2
            $conditionSql = $conditionSql
1187 2
                ? $conditionSql . ' AND ' . $filterSql
1188 2
                : $filterSql;
1189
        }
1190
1191
        return 'SELECT COUNT(*) '
1192 41
            . 'FROM ' . $tableName . ' ' . $tableAlias
1193 41
            . (empty($conditionSql) ? '' : ' WHERE ' . $conditionSql);
1194
    }
1195
1196
    /**
1197
     * Gets the ORDER BY SQL snippet for ordered collections.
1198
     *
1199
     * @param mixed[] $orderBy
1200
     * @param string  $baseTableAlias
1201
     *
1202
     * @return string
1203
     *
1204
     * @throws ORMException
1205
     */
1206 80
    final protected function getOrderBySQL(array $orderBy, $baseTableAlias)
1207
    {
1208 80
        if (! $orderBy) {
1209 68
            return '';
1210
        }
1211
1212 12
        $orderByList = [];
1213
1214 12
        foreach ($orderBy as $fieldName => $orientation) {
1215 12
            $orientation = strtoupper(trim($orientation));
1216
1217 12
            if (! in_array($orientation, ['ASC', 'DESC'], true)) {
1218 1
                throw InvalidOrientation::fromClassNameAndField($this->class->getClassName(), $fieldName);
1219
            }
1220
1221 11
            $property = $this->class->getProperty($fieldName);
1222
1223 11
            if ($property instanceof FieldMetadata) {
1224 9
                $tableAlias = $this->getSQLTableAlias($property->getTableName());
1225 9
                $columnName = $this->platform->quoteIdentifier($property->getColumnName());
1226
1227 9
                $orderByList[] = $tableAlias . '.' . $columnName . ' ' . $orientation;
1228
1229 9
                continue;
1230 2
            } elseif ($property instanceof AssociationMetadata) {
1231 2
                if (! $property->isOwningSide()) {
1232 1
                    throw InvalidFindByCall::fromInverseSideUsage(
1233 1
                        $this->class->getClassName(),
1234 1
                        $fieldName
1235
                    );
1236
                }
1237
1238 1
                $class      = $this->class->isInheritedProperty($fieldName)
1239
                    ? $property->getDeclaringClass()
1240 1
                    : $this->class;
1241 1
                $tableAlias = $this->getSQLTableAlias($class->getTableName());
1242
1243 1
                foreach ($property->getJoinColumns() as $joinColumn) {
1244
                    /** @var JoinColumnMetadata $joinColumn */
1245 1
                    $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName());
1246
1247 1
                    $orderByList[] = $tableAlias . '.' . $quotedColumnName . ' ' . $orientation;
1248
                }
1249
1250 1
                continue;
1251
            }
1252
1253
            throw UnrecognizedField::byName($fieldName);
1254
        }
1255
1256 10
        return ' ORDER BY ' . implode(', ', $orderByList);
1257
    }
1258
1259
    /**
1260
     * Gets the SQL fragment with the list of columns to select when querying for
1261
     * an entity in this persister.
1262
     *
1263
     * Subclasses should override this method to alter or change the select column
1264
     * list SQL fragment. Note that in the implementation of BasicEntityPersister
1265
     * the resulting SQL fragment is generated only once and cached in {@link selectColumnListSql}.
1266
     * Subclasses may or may not do the same.
1267
     *
1268
     * @return string The SQL fragment.
1269
     */
1270 515
    protected function getSelectColumnsSQL()
1271
    {
1272 515
        if ($this->currentPersisterContext->selectColumnListSql !== null) {
1273 99
            return $this->currentPersisterContext->selectColumnListSql;
1274
        }
1275
1276 515
        $this->currentPersisterContext->rsm->addEntityResult($this->class->getClassName(), 'r'); // r for root
1277 515
        $this->currentPersisterContext->selectJoinSql = '';
1278
1279 515
        $eagerAliasCounter = 0;
1280 515
        $columnList        = [];
1281
1282 515
        foreach ($this->class->getDeclaredPropertiesIterator() as $fieldName => $property) {
1283
            switch (true) {
1284 515
                case $property instanceof FieldMetadata:
1285 513
                    $columnList[] = $this->getSelectColumnSQL($fieldName, $this->class);
1286 513
                    break;
1287
1288 462
                case $property instanceof AssociationMetadata:
1289 458
                    $assocColumnSQL = $this->getSelectColumnAssociationSQL($fieldName, $property, $this->class);
1290
1291 458
                    if ($assocColumnSQL) {
1292 387
                        $columnList[] = $assocColumnSQL;
1293
                    }
1294
1295 458
                    $isAssocToOneInverseSide = $property instanceof ToOneAssociationMetadata && ! $property->isOwningSide();
1296 458
                    $isAssocFromOneEager     = ! $property instanceof ManyToManyAssociationMetadata && $property->getFetchMode() === FetchMode::EAGER;
1297
1298 458
                    if (! ($isAssocFromOneEager || $isAssocToOneInverseSide)) {
1299 436
                        break;
1300
                    }
1301
1302 178
                    if ($property instanceof ToManyAssociationMetadata && $this->currentPersisterContext->handlesLimits) {
1303 3
                        break;
1304
                    }
1305
1306 175
                    $targetEntity = $property->getTargetEntity();
1307 175
                    $eagerEntity  = $this->em->getClassMetadata($targetEntity);
1308
1309 175
                    if ($eagerEntity->inheritanceType !== InheritanceType::NONE) {
1310 5
                        break; // now this is why you shouldn't use inheritance
1311
                    }
1312
1313 170
                    $assocAlias = 'e' . ($eagerAliasCounter++);
1314
1315 170
                    $this->currentPersisterContext->rsm->addJoinedEntityResult($targetEntity, $assocAlias, 'r', $fieldName);
1316
1317 170
                    foreach ($eagerEntity->getDeclaredPropertiesIterator() as $eagerProperty) {
1318
                        switch (true) {
1319 170
                            case $eagerProperty instanceof FieldMetadata:
1320 167
                                $columnList[] = $this->getSelectColumnSQL($eagerProperty->getName(), $eagerEntity, $assocAlias);
0 ignored issues
show
$eagerEntity of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Persisters\...r::getSelectColumnSQL(). ( Ignorable by Annotation )

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

1320
                                $columnList[] = $this->getSelectColumnSQL($eagerProperty->getName(), /** @scrutinizer ignore-type */ $eagerEntity, $assocAlias);
Loading history...
1321 167
                                break;
1322
1323 167
                            case $eagerProperty instanceof ToOneAssociationMetadata && $eagerProperty->isOwningSide():
1324 164
                                $columnList[] = $this->getSelectColumnAssociationSQL(
1325 164
                                    $eagerProperty->getName(),
1326 164
                                    $eagerProperty,
1327 164
                                    $eagerEntity,
0 ignored issues
show
$eagerEntity of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Persisters\...tColumnAssociationSQL(). ( Ignorable by Annotation )

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

1327
                                    /** @scrutinizer ignore-type */ $eagerEntity,
Loading history...
1328 164
                                    $assocAlias
1329
                                );
1330 170
                                break;
1331
                        }
1332
                    }
1333
1334 170
                    $owningAssociation = $property;
1335 170
                    $joinCondition     = [];
1336
1337 170
                    if ($property instanceof ToManyAssociationMetadata && $property->getIndexedBy()) {
1338 1
                        $this->currentPersisterContext->rsm->addIndexBy($assocAlias, $property->getIndexedBy());
1339
                    }
1340
1341 170
                    if (! $property->isOwningSide()) {
1342 163
                        $owningAssociation = $eagerEntity->getProperty($property->getMappedBy());
1343
                    }
1344
1345 170
                    $joinTableAlias = $this->getSQLTableAlias($eagerEntity->getTableName(), $assocAlias);
1346 170
                    $joinTableName  = $eagerEntity->table->getQuotedQualifiedName($this->platform);
1347
1348 170
                    $this->currentPersisterContext->selectJoinSql .= ' ' . $this->getJoinSQLForAssociation($property);
1349
1350 170
                    $sourceClass      = $this->em->getClassMetadata($owningAssociation->getSourceEntity());
1351 170
                    $targetClass      = $this->em->getClassMetadata($owningAssociation->getTargetEntity());
1352 170
                    $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName(), $property->isOwningSide() ? $assocAlias : '');
1353 170
                    $sourceTableAlias = $this->getSQLTableAlias($sourceClass->getTableName(), $property->isOwningSide() ? '' : $assocAlias);
1354
1355 170
                    foreach ($owningAssociation->getJoinColumns() as $joinColumn) {
1356 170
                        $joinCondition[] = sprintf(
1357 170
                            '%s.%s = %s.%s',
1358 170
                            $sourceTableAlias,
1359 170
                            $this->platform->quoteIdentifier($joinColumn->getColumnName()),
1360 170
                            $targetTableAlias,
1361 170
                            $this->platform->quoteIdentifier($joinColumn->getReferencedColumnName())
1362
                        );
1363
                    }
1364
1365 170
                    $filterSql = $this->generateFilterConditionSQL($eagerEntity, $targetTableAlias);
0 ignored issues
show
$eagerEntity of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $targetEntity of Doctrine\ORM\Persisters\...ateFilterConditionSQL(). ( Ignorable by Annotation )

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

1365
                    $filterSql = $this->generateFilterConditionSQL(/** @scrutinizer ignore-type */ $eagerEntity, $targetTableAlias);
Loading history...
1366
1367
                    // Add filter SQL
1368 170
                    if ($filterSql) {
1369
                        $joinCondition[] = $filterSql;
1370
                    }
1371
1372 170
                    $this->currentPersisterContext->selectJoinSql .= ' ' . $joinTableName . ' ' . $joinTableAlias . ' ON ';
1373 170
                    $this->currentPersisterContext->selectJoinSql .= implode(' AND ', $joinCondition);
1374
1375 515
                    break;
1376
            }
1377
        }
1378
1379 515
        $this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList);
1380
1381 515
        return $this->currentPersisterContext->selectColumnListSql;
1382
    }
1383
1384
    /**
1385
     * Gets the SQL join fragment used when selecting entities from an association.
1386
     *
1387
     * @param string $field
1388
     * @param string $alias
1389
     *
1390
     * @return string
1391
     */
1392 458
    protected function getSelectColumnAssociationSQL($field, AssociationMetadata $association, ClassMetadata $class, $alias = 'r')
1393
    {
1394 458
        if (! ($association->isOwningSide() && $association instanceof ToOneAssociationMetadata)) {
1395 370
            return '';
1396
        }
1397
1398 404
        $columnList    = [];
1399 404
        $targetClass   = $this->em->getClassMetadata($association->getTargetEntity());
1400 404
        $sqlTableAlias = $this->getSQLTableAlias($class->getTableName(), ($alias === 'r' ? '' : $alias));
1401
1402 404
        foreach ($association->getJoinColumns() as $joinColumn) {
1403
            /** @var JoinColumnMetadata $joinColumn */
1404 404
            $columnName           = $joinColumn->getColumnName();
1405 404
            $quotedColumnName     = $this->platform->quoteIdentifier($columnName);
1406 404
            $referencedColumnName = $joinColumn->getReferencedColumnName();
1407 404
            $resultColumnName     = $this->getSQLColumnAlias();
1408
1409 404
            if (! $joinColumn->getType()) {
1410 10
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
0 ignored issues
show
$targetClass of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Utility\Per...lper::getTypeOfColumn(). ( Ignorable by Annotation )

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

1410
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, /** @scrutinizer ignore-type */ $targetClass, $this->em));
Loading history...
1411
            }
1412
1413 404
            $this->currentPersisterContext->rsm->addMetaResult(
1414 404
                $alias,
1415 404
                $resultColumnName,
1416 404
                $columnName,
1417 404
                $association->isPrimaryKey(),
1418 404
                $joinColumn->getType()
1419
            );
1420
1421 404
            $columnList[] = sprintf('%s.%s AS %s', $sqlTableAlias, $quotedColumnName, $resultColumnName);
1422
        }
1423
1424 404
        return implode(', ', $columnList);
1425
    }
1426
1427
    /**
1428
     * Gets the SQL join fragment used when selecting entities from a
1429
     * many-to-many association.
1430
     *
1431
     * @return string
1432
     */
1433 83
    protected function getSelectManyToManyJoinSQL(ManyToManyAssociationMetadata $association)
1434
    {
1435 83
        $conditions        = [];
1436 83
        $owningAssociation = $association;
1437 83
        $sourceTableAlias  = $this->getSQLTableAlias($this->class->getTableName());
1438
1439 83
        if (! $association->isOwningSide()) {
1440 13
            $targetEntity      = $this->em->getClassMetadata($association->getTargetEntity());
1441 13
            $owningAssociation = $targetEntity->getProperty($association->getMappedBy());
1442
        }
1443
1444 83
        $joinTable     = $owningAssociation->getJoinTable();
1445 83
        $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
1446 83
        $joinColumns   = $association->isOwningSide()
1447 75
            ? $joinTable->getInverseJoinColumns()
1448 83
            : $joinTable->getJoinColumns();
1449
1450 83
        foreach ($joinColumns as $joinColumn) {
1451 83
            $conditions[] = sprintf(
1452 83
                '%s.%s = %s.%s',
1453 83
                $sourceTableAlias,
1454 83
                $this->platform->quoteIdentifier($joinColumn->getReferencedColumnName()),
1455 83
                $joinTableName,
1456 83
                $this->platform->quoteIdentifier($joinColumn->getColumnName())
1457
            );
1458
        }
1459
1460 83
        return ' INNER JOIN ' . $joinTableName . ' ON ' . implode(' AND ', $conditions);
1461
    }
1462
1463
    /**
1464
     * {@inheritdoc}
1465
     */
1466 999
    public function getInsertSQL()
1467
    {
1468 999
        if ($this->insertSql !== null) {
1469 658
            return $this->insertSql;
1470
        }
1471
1472 999
        $columns   = $this->getInsertColumnList();
1473 999
        $tableName = $this->class->table->getQuotedQualifiedName($this->platform);
1474
1475 999
        if (empty($columns)) {
1476 108
            $property       = $this->class->getProperty($this->class->identifier[0]);
1477 108
            $identityColumn = $this->platform->quoteIdentifier($property->getColumnName());
1478
1479 108
            $this->insertSql = $this->platform->getEmptyIdentityInsertSQL($tableName, $identityColumn);
1480
1481 108
            return $this->insertSql;
1482
        }
1483
1484 976
        $quotedColumns = [];
1485 976
        $values        = [];
1486
1487 976
        foreach ($columns as $columnName) {
1488 976
            $column = $this->columns[$columnName];
1489
1490 976
            $quotedColumns[] = $this->platform->quoteIdentifier($column->getColumnName());
1491 976
            $values[]        = $column->getType()->convertToDatabaseValueSQL('?', $this->platform);
1492
        }
1493
1494 976
        $quotedColumns = implode(', ', $quotedColumns);
1495 976
        $values        = implode(', ', $values);
1496
1497 976
        $this->insertSql = sprintf('INSERT INTO %s (%s) VALUES (%s)', $tableName, $quotedColumns, $values);
1498
1499 976
        return $this->insertSql;
1500
    }
1501
1502
    /**
1503
     * Gets the list of columns to put in the INSERT SQL statement.
1504
     *
1505
     * Subclasses should override this method to alter or change the list of
1506
     * columns placed in the INSERT statements used by the persister.
1507
     *
1508
     * @return string[] The list of columns.
1509
     */
1510 912
    protected function getInsertColumnList()
1511
    {
1512 912
        $columns             = [];
1513 912
        $versionPropertyName = $this->class->isVersioned()
1514 198
            ? $this->class->versionProperty->getName()
1515 912
            : null;
1516
1517 912
        foreach ($this->class->getDeclaredPropertiesIterator() as $name => $property) {
1518
            /*if (isset($this->class->embeddedClasses[$name])) {
1519
                continue;
1520
            }*/
1521
1522
            switch (true) {
1523 912
                case $property instanceof VersionFieldMetadata:
1524
                    // Do nothing
1525 198
                    break;
1526
1527 912
                case $property instanceof LocalColumnMetadata:
1528 912
                    if (($property instanceof FieldMetadata
1529
                            && (
1530 912
                                ! $property->hasValueGenerator()
1531 912
                                || $property->getValueGenerator()->getType() !== GeneratorType::IDENTITY
1532
                            )
1533
                        )
1534 912
                        || $this->class->identifier[0] !== $name
1535
                    ) {
1536 845
                        $columnName = $property->getColumnName();
1537
1538 845
                        $columns[] = $columnName;
1539
1540 845
                        $this->columns[$columnName] = $property;
1541
                    }
1542
1543 912
                    break;
1544
1545 803
                case $property instanceof AssociationMetadata:
1546 799
                    if ($property->isOwningSide() && $property instanceof ToOneAssociationMetadata) {
1547 757
                        $targetClass = $this->em->getClassMetadata($property->getTargetEntity());
1548
1549 757
                        foreach ($property->getJoinColumns() as $joinColumn) {
1550
                            /** @var JoinColumnMetadata $joinColumn */
1551 757
                            $columnName           = $joinColumn->getColumnName();
1552 757
                            $referencedColumnName = $joinColumn->getReferencedColumnName();
1553
1554 757
                            if (! $joinColumn->getType()) {
1555 120
                                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
0 ignored issues
show
$targetClass of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Utility\Per...lper::getTypeOfColumn(). ( Ignorable by Annotation )

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

1555
                                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, /** @scrutinizer ignore-type */ $targetClass, $this->em));
Loading history...
1556
                            }
1557
1558 757
                            $columns[] = $columnName;
1559
1560 757
                            $this->columns[$columnName] = $joinColumn;
1561
                        }
1562
                    }
1563
1564 912
                    break;
1565
            }
1566
        }
1567
1568 912
        return $columns;
1569
    }
1570
1571
    /**
1572
     * Gets the SQL snippet of a qualified column name for the given field name.
1573
     *
1574
     * @param string        $field The field name.
1575
     * @param ClassMetadata $class The class that declares this field. The table this class is
1576
     *                             mapped to must own the column for the given field.
1577
     * @param string        $alias
1578
     *
1579
     * @return string
1580
     */
1581 550
    protected function getSelectColumnSQL($field, ClassMetadata $class, $alias = 'r')
1582
    {
1583 550
        $property    = $class->getProperty($field);
1584 550
        $columnAlias = $this->getSQLColumnAlias();
1585 550
        $sql         = sprintf(
1586 550
            '%s.%s',
1587 550
            $this->getSQLTableAlias($property->getTableName(), ($alias === 'r' ? '' : $alias)),
1588 550
            $this->platform->quoteIdentifier($property->getColumnName())
1589
        );
1590
1591 550
        $this->currentPersisterContext->rsm->addFieldResult($alias, $columnAlias, $field, $class->getClassName());
1592
1593 550
        return $property->getType()->convertToPHPValueSQL($sql, $this->platform) . ' AS ' . $columnAlias;
1594
    }
1595
1596
    /**
1597
     * Gets the SQL table alias for the given class name.
1598
     *
1599
     * @param string $tableName
1600
     * @param string $assocName
1601
     *
1602
     * @return string The SQL table alias.
1603
     */
1604 584
    protected function getSQLTableAlias($tableName, $assocName = '')
1605
    {
1606 584
        if ($tableName) {
1607 584
            $tableName .= '#' . $assocName;
1608
        }
1609
1610 584
        if (isset($this->currentPersisterContext->sqlTableAliases[$tableName])) {
1611 576
            return $this->currentPersisterContext->sqlTableAliases[$tableName];
1612
        }
1613
1614 584
        $tableAlias = 't' . $this->currentPersisterContext->sqlAliasCounter++;
1615
1616 584
        $this->currentPersisterContext->sqlTableAliases[$tableName] = $tableAlias;
1617
1618 584
        return $tableAlias;
1619
    }
1620
1621
    /**
1622
     * {@inheritdoc}
1623
     */
1624
    public function lock(array $criteria, $lockMode)
1625
    {
1626
        $lockSql      = '';
1627
        $conditionSql = $this->getSelectConditionSQL($criteria);
1628
1629
        switch ($lockMode) {
1630
            case LockMode::PESSIMISTIC_READ:
1631
                $lockSql = $this->platform->getReadLockSQL();
1632
1633
                break;
1634
            case LockMode::PESSIMISTIC_WRITE:
1635
                $lockSql = $this->platform->getWriteLockSQL();
1636
                break;
1637
        }
1638
1639
        $lock  = $this->getLockTablesSql($lockMode);
1640
        $where = ($conditionSql ? ' WHERE ' . $conditionSql : '') . ' ';
1641
        $sql   = 'SELECT 1 '
1642
             . $lock
1643
             . $where
1644
             . $lockSql;
1645
1646
        [$params, $types] = $this->expandParameters($criteria);
1647
1648
        $this->conn->executeQuery($sql, $params, $types);
1649
    }
1650
1651
    /**
1652
     * Gets the FROM and optionally JOIN conditions to lock the entity managed by this persister.
1653
     *
1654
     * @param int $lockMode One of the Doctrine\DBAL\LockMode::* constants.
1655
     *
1656
     * @return string
1657
     */
1658 13
    protected function getLockTablesSql($lockMode)
1659
    {
1660 13
        $tableName = $this->class->table->getQuotedQualifiedName($this->platform);
1661
1662 13
        return $this->platform->appendLockHint(
1663 13
            'FROM ' . $tableName . ' ' . $this->getSQLTableAlias($this->class->getTableName()),
1664 13
            $lockMode
1665
        );
1666
    }
1667
1668
    /**
1669
     * Gets the Select Where Condition from a Criteria object.
1670
     *
1671
     * @return string
1672
     */
1673 39
    protected function getSelectConditionCriteriaSQL(Criteria $criteria)
1674
    {
1675 39
        $expression = $criteria->getWhereExpression();
1676
1677 39
        if ($expression === null) {
1678 2
            return '';
1679
        }
1680
1681 38
        $visitor = new SqlExpressionVisitor($this, $this->class);
1682
1683 38
        return $visitor->dispatch($expression);
1684
    }
1685
1686
    /**
1687
     * {@inheritdoc}
1688
     */
1689 564
    public function getSelectConditionStatementSQL(
1690
        $field,
1691
        $value,
1692
        ?AssociationMetadata $association = null,
1693
        $comparison = null
1694
    ) {
1695 564
        $selectedColumns = [];
1696 564
        $columns         = $this->getSelectConditionStatementColumnSQL($field, $association);
1697
1698 560
        if (in_array($comparison, [Comparison::IN, Comparison::NIN], true) && isset($columns[1])) {
1699
            // @todo try to support multi-column IN expressions. Example: (col1, col2) IN (('val1A', 'val2A'), ...)
1700 1
            throw CantUseInOperatorOnCompositeKeys::create();
1701
        }
1702
1703 559
        foreach ($columns as $column) {
1704 559
            $property    = $this->class->getProperty($field);
1705 559
            $placeholder = '?';
1706
1707 559
            if ($property instanceof FieldMetadata) {
1708 474
                $placeholder = $property->getType()->convertToDatabaseValueSQL($placeholder, $this->platform);
1709
            }
1710
1711 559
            if ($comparison !== null) {
1712
                // special case null value handling
1713 42
                if (($comparison === Comparison::EQ || $comparison === Comparison::IS) && $value ===null) {
1714 6
                    $selectedColumns[] = $column . ' IS NULL';
1715
1716 6
                    continue;
1717
                }
1718
1719 36
                if ($comparison === Comparison::NEQ && $value === null) {
1720 3
                    $selectedColumns[] = $column . ' IS NOT NULL';
1721
1722 3
                    continue;
1723
                }
1724
1725 33
                $selectedColumns[] = $column . ' ' . sprintf(self::$comparisonMap[$comparison], $placeholder);
1726
1727 33
                continue;
1728
            }
1729
1730 532
            if (is_array($value)) {
1731 14
                $in = sprintf('%s IN (%s)', $column, $placeholder);
1732
1733 14
                if (in_array(null, $value, true)) {
1734 4
                    $selectedColumns[] = sprintf('(%s OR %s IS NULL)', $in, $column);
1735
1736 4
                    continue;
1737
                }
1738
1739 10
                $selectedColumns[] = $in;
1740
1741 10
                continue;
1742
            }
1743
1744 521
            if ($value === null) {
1745 9
                $selectedColumns[] = sprintf('%s IS NULL', $column);
1746
1747 9
                continue;
1748
            }
1749
1750 513
            $selectedColumns[] = sprintf('%s = %s', $column, $placeholder);
1751
        }
1752
1753 559
        return implode(' AND ', $selectedColumns);
1754
    }
1755
1756
    /**
1757
     * Builds the left-hand-side of a where condition statement.
1758
     *
1759
     * @param string $field
1760
     *
1761
     * @return string[]
1762
     *
1763
     * @throws ORMException
1764
     */
1765 564
    private function getSelectConditionStatementColumnSQL($field, ?AssociationMetadata $association = null)
1766
    {
1767 564
        $property = $this->class->getProperty($field);
1768
1769 564
        if ($property instanceof FieldMetadata) {
1770 474
            $tableAlias = $this->getSQLTableAlias($property->getTableName());
1771 474
            $columnName = $this->platform->quoteIdentifier($property->getColumnName());
1772
1773 474
            return [$tableAlias . '.' . $columnName];
1774
        }
1775
1776 279
        if ($property instanceof AssociationMetadata) {
1777 145
            $owningAssociation = $property;
1778 145
            $columns           = [];
1779
1780
            // Many-To-Many requires join table check for joinColumn
1781 145
            if ($owningAssociation instanceof ManyToManyAssociationMetadata) {
1782 3
                if (! $owningAssociation->isOwningSide()) {
1783 2
                    $owningAssociation = $association;
1784
                }
1785
1786 3
                $joinTable     = $owningAssociation->getJoinTable();
1787 3
                $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
1788 3
                $joinColumns   = $association->isOwningSide()
1789 2
                    ? $joinTable->getJoinColumns()
1790 3
                    : $joinTable->getInverseJoinColumns();
1791
1792 3
                foreach ($joinColumns as $joinColumn) {
1793 3
                    $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName());
1794
1795 3
                    $columns[] = $joinTableName . '.' . $quotedColumnName;
1796
                }
1797
            } else {
1798 143
                if (! $owningAssociation->isOwningSide()) {
1799 1
                    throw InvalidFindByCall::fromInverseSideUsage(
1800 1
                        $this->class->getClassName(),
1801 1
                        $field
1802
                    );
1803
                }
1804
1805 142
                $class      = $this->class->isInheritedProperty($field)
1806 11
                    ? $owningAssociation->getDeclaringClass()
1807 142
                    : $this->class;
1808 142
                $tableAlias = $this->getSQLTableAlias($class->getTableName());
1809
1810 142
                foreach ($owningAssociation->getJoinColumns() as $joinColumn) {
1811 142
                    $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName());
1812
1813 142
                    $columns[] = $tableAlias . '.' . $quotedColumnName;
1814
                }
1815
            }
1816
1817 144
            return $columns;
1818
        }
1819
1820 155
        if ($association !== null && strpos($field, ' ') === false && strpos($field, '(') === false) {
1821
            // very careless developers could potentially open up this normally hidden api for userland attacks,
1822
            // therefore checking for spaces and function calls which are not allowed.
1823
1824
            // found a join column condition, not really a "field"
1825 152
            return [$field];
1826
        }
1827
1828 3
        throw UnrecognizedField::byName($field);
1829
    }
1830
1831
    /**
1832
     * Gets the conditional SQL fragment used in the WHERE clause when selecting
1833
     * entities in this persister.
1834
     *
1835
     * Subclasses are supposed to override this method if they intend to change
1836
     * or alter the criteria by which entities are selected.
1837
     *
1838
     * @param mixed[] $criteria
1839
     *
1840
     * @return string
1841
     */
1842 557
    protected function getSelectConditionSQL(array $criteria, ?AssociationMetadata $association = null)
1843
    {
1844 557
        $conditions = [];
1845
1846 557
        foreach ($criteria as $field => $value) {
1847 534
            $conditions[] = $this->getSelectConditionStatementSQL($field, $value, $association);
1848
        }
1849
1850 554
        return implode(' AND ', $conditions);
1851
    }
1852
1853
    /**
1854
     * {@inheritdoc}
1855
     */
1856 5
    public function getOneToManyCollection(
1857
        OneToManyAssociationMetadata $association,
1858
        $sourceEntity,
1859
        $offset = null,
1860
        $limit = null
1861
    ) {
1862 5
        $this->switchPersisterContext($offset, $limit);
1863
1864 5
        $stmt = $this->getOneToManyStatement($association, $sourceEntity, $offset, $limit);
1865
1866 5
        return $this->loadArrayFromStatement($association, $stmt);
1867
    }
1868
1869
    /**
1870
     * {@inheritdoc}
1871
     */
1872 73
    public function loadOneToManyCollection(
1873
        OneToManyAssociationMetadata $association,
1874
        $sourceEntity,
1875
        PersistentCollection $collection
1876
    ) {
1877 73
        $stmt = $this->getOneToManyStatement($association, $sourceEntity);
1878
1879 73
        return $this->loadCollectionFromStatement($association, $stmt, $collection);
1880
    }
1881
1882
    /**
1883
     * Builds criteria and execute SQL statement to fetch the one to many entities from.
1884
     *
1885
     * @param object   $sourceEntity
1886
     * @param int|null $offset
1887
     * @param int|null $limit
1888
     *
1889
     * @return Statement
1890
     */
1891 78
    private function getOneToManyStatement(
1892
        OneToManyAssociationMetadata $association,
1893
        $sourceEntity,
1894
        $offset = null,
1895
        $limit = null
1896
    ) {
1897 78
        $this->switchPersisterContext($offset, $limit);
1898
1899 78
        $criteria    = [];
1900 78
        $parameters  = [];
1901 78
        $owningAssoc = $this->class->getProperty($association->getMappedBy());
1902 78
        $sourceClass = $this->em->getClassMetadata($association->getSourceEntity());
1903 78
        $class       = $owningAssoc->getDeclaringClass();
1904 78
        $tableAlias  = $this->getSQLTableAlias($class->getTableName());
1905
1906 78
        foreach ($owningAssoc->getJoinColumns() as $joinColumn) {
1907 78
            $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName());
1908 78
            $fieldName        = $sourceClass->fieldNames[$joinColumn->getReferencedColumnName()];
1909 78
            $property         = $sourceClass->getProperty($fieldName);
1910 78
            $value            = null;
1911
1912 78
            if ($property instanceof FieldMetadata) {
1913 77
                $value = $property->getValue($sourceEntity);
1914 4
            } elseif ($property instanceof AssociationMetadata) {
1915 4
                $targetClass = $this->em->getClassMetadata($property->getTargetEntity());
1916 4
                $value       = $property->getValue($sourceEntity);
1917
1918 4
                $value = $this->em->getUnitOfWork()->getEntityIdentifier($value);
1919 4
                $value = $value[$targetClass->identifier[0]];
1920
            }
1921
1922 78
            $criteria[$tableAlias . '.' . $quotedColumnName] = $value;
1923 78
            $parameters[]                                    = [
1924 78
                'value' => $value,
1925 78
                'field' => $fieldName,
1926 78
                'class' => $sourceClass,
1927
            ];
1928
        }
1929
1930 78
        $sql              = $this->getSelectSQL($criteria, $association, null, $limit, $offset);
1931 78
        [$params, $types] = $this->expandToManyParameters($parameters);
1932
1933 78
        return $this->conn->executeQuery($sql, $params, $types);
1934
    }
1935
1936
    /**
1937
     * {@inheritdoc}
1938
     */
1939 534
    public function expandParameters($criteria)
1940
    {
1941 534
        $params = [];
1942 534
        $types  = [];
1943
1944 534
        foreach ($criteria as $field => $value) {
1945 511
            if ($value === null) {
1946 3
                continue; // skip null values.
1947
            }
1948
1949 509
            $types  = array_merge($types, $this->getTypes($field, $value, $this->class));
1950 509
            $params = array_merge($params, $this->getValues($value));
1951
        }
1952
1953 534
        return [$params, $types];
1954
    }
1955
1956
    /**
1957
     * Expands the parameters from the given criteria and use the correct binding types if found,
1958
     * specialized for OneToMany or ManyToMany associations.
1959
     *
1960
     * @param mixed[][] $criteria an array of arrays containing following:
1961
     *                             - field to which each criterion will be bound
1962
     *                             - value to be bound
1963
     *                             - class to which the field belongs to
1964
     *
1965
     * @return mixed[][]
1966
     */
1967 148
    private function expandToManyParameters($criteria)
1968
    {
1969 148
        $params = [];
1970 148
        $types  = [];
1971
1972 148
        foreach ($criteria as $criterion) {
1973 148
            if ($criterion['value'] === null) {
1974 6
                continue; // skip null values.
1975
            }
1976
1977 142
            $types  = array_merge($types, $this->getTypes($criterion['field'], $criterion['value'], $criterion['class']));
1978 142
            $params = array_merge($params, $this->getValues($criterion['value']));
1979
        }
1980
1981 148
        return [$params, $types];
1982
    }
1983
1984
    /**
1985
     * Infers field types to be used by parameter type casting.
1986
     *
1987
     * @param string $field
1988
     * @param mixed  $value
1989
     *
1990
     * @return mixed[]
1991
     *
1992
     * @throws QueryException
1993
     */
1994 667
    private function getTypes($field, $value, ClassMetadata $class)
1995
    {
1996 667
        $property = $class->getProperty($field);
1997 667
        $types    = [];
1998
1999
        switch (true) {
2000 667
            case $property instanceof FieldMetadata:
2001 613
                $types = array_merge($types, [$property->getType()]);
2002 613
                break;
2003
2004 144
            case $property instanceof AssociationMetadata:
2005 143
                $class = $this->em->getClassMetadata($property->getTargetEntity());
2006
2007 143
                if (! $property->isOwningSide()) {
2008 2
                    $property = $class->getProperty($property->getMappedBy());
2009 2
                    $class    = $this->em->getClassMetadata($property->getTargetEntity());
2010
                }
2011
2012 143
                $joinColumns = $property instanceof ManyToManyAssociationMetadata
2013 3
                    ? $property->getJoinTable()->getInverseJoinColumns()
2014 143
                    : $property->getJoinColumns();
2015
2016 143
                foreach ($joinColumns as $joinColumn) {
2017
                    /** @var JoinColumnMetadata $joinColumn */
2018 143
                    $referencedColumnName = $joinColumn->getReferencedColumnName();
2019
2020 143
                    if (! $joinColumn->getType()) {
2021 1
                        $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $class, $this->em));
0 ignored issues
show
$class of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Utility\Per...lper::getTypeOfColumn(). ( Ignorable by Annotation )

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

2021
                        $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, /** @scrutinizer ignore-type */ $class, $this->em));
Loading history...
2022
                    }
2023
2024 143
                    $types[] = $joinColumn->getType();
2025
                }
2026
2027 143
                break;
2028
2029
            default:
2030 1
                $types[] = null;
2031 1
                break;
2032
        }
2033
2034 667
        if (is_array($value)) {
2035
            return array_map(static function ($type) {
2036 16
                return $type->getBindingType() + Connection::ARRAY_PARAM_OFFSET;
2037 16
            }, $types);
2038
        }
2039
2040 657
        return $types;
2041
    }
2042
2043
    /**
2044
     * Retrieves the parameters that identifies a value.
2045
     *
2046
     * @param mixed $value
2047
     *
2048
     * @return mixed[]
2049
     */
2050 541
    private function getValues($value)
2051
    {
2052 541
        if (is_array($value)) {
2053 16
            $newValue = [];
2054
2055 16
            foreach ($value as $itemValue) {
2056 16
                $newValue = array_merge($newValue, $this->getValues($itemValue));
2057
            }
2058
2059 16
            return [$newValue];
2060
        }
2061
2062 541
        $metadataFactory = $this->em->getMetadataFactory();
2063 541
        $unitOfWork      = $this->em->getUnitOfWork();
2064
2065 541
        if (is_object($value) && $metadataFactory->hasMetadataFor(StaticClassNameConverter::getClass($value))) {
2066 45
            $class     = $metadataFactory->getMetadataFor(get_class($value));
2067 45
            $persister = $unitOfWork->getEntityPersister($class->getClassName());
2068
2069 45
            if ($class->isIdentifierComposite()) {
2070 3
                $newValue = [];
2071
2072 3
                foreach ($persister->getIdentifier($value) as $innerValue) {
2073 3
                    $newValue = array_merge($newValue, $this->getValues($innerValue));
2074
                }
2075
2076 3
                return $newValue;
2077
            }
2078
        }
2079
2080 541
        return [$this->getIndividualValue($value)];
2081
    }
2082
2083
    /**
2084
     * Retrieves an individual parameter value.
2085
     *
2086
     * @param mixed $value
2087
     *
2088
     * @return mixed
2089
     */
2090 541
    private function getIndividualValue($value)
2091
    {
2092 541
        if (! is_object($value) || ! $this->em->getMetadataFactory()->hasMetadataFor(StaticClassNameConverter::getClass($value))) {
2093 539
            return $value;
2094
        }
2095
2096 45
        return $this->em->getUnitOfWork()->getSingleIdentifierValue($value);
2097
    }
2098
2099
    /**
2100
     * {@inheritdoc}
2101
     */
2102 14
    public function exists($entity, ?Criteria $extraConditions = null)
2103
    {
2104 14
        $criteria = $this->getIdentifier($entity);
2105
2106 14
        if (! $criteria) {
2107 2
            return false;
2108
        }
2109
2110 13
        $alias = $this->getSQLTableAlias($this->class->getTableName());
2111
2112
        $sql = 'SELECT 1 '
2113 13
             . $this->getLockTablesSql(null)
2114 13
             . ' WHERE ' . $this->getSelectConditionSQL($criteria);
2115
2116 13
        [$params, $types] = $this->expandParameters($criteria);
2117
2118 13
        if ($extraConditions !== null) {
2119 9
            $sql                             .= ' AND ' . $this->getSelectConditionCriteriaSQL($extraConditions);
2120 9
            [$criteriaParams, $criteriaTypes] = $this->expandCriteriaParameters($extraConditions);
2121
2122 9
            $params = array_merge($params, $criteriaParams);
2123 9
            $types  = array_merge($types, $criteriaTypes);
2124
        }
2125
2126 13
        $filterSql = $this->generateFilterConditionSQL($this->class, $alias);
2127
2128 13
        if ($filterSql) {
2129 3
            $sql .= ' AND ' . $filterSql;
2130
        }
2131
2132 13
        return (bool) $this->conn->fetchColumn($sql, $params, 0, $types);
2133
    }
2134
2135
    /**
2136
     * Generates the appropriate join SQL for the given association.
2137
     *
2138
     * @return string LEFT JOIN if one of the columns is nullable, INNER JOIN otherwise.
2139
     */
2140 170
    protected function getJoinSQLForAssociation(AssociationMetadata $association)
2141
    {
2142 170
        if (! $association->isOwningSide()) {
2143 163
            return 'LEFT JOIN';
2144
        }
2145
2146
        // if one of the join columns is nullable, return left join
2147 13
        foreach ($association->getJoinColumns() as $joinColumn) {
2148 13
            if (! $joinColumn->isNullable()) {
2149 5
                continue;
2150
            }
2151
2152 11
            return 'LEFT JOIN';
2153
        }
2154
2155 5
        return 'INNER JOIN';
2156
    }
2157
2158
    /**
2159
     * Gets an SQL column alias for a column name.
2160
     *
2161
     * @return string
2162
     */
2163 551
    public function getSQLColumnAlias()
2164
    {
2165 551
        return $this->platform->getSQLResultCasing('c' . $this->currentPersisterContext->sqlAliasCounter++);
2166
    }
2167
2168
    /**
2169
     * Generates the filter SQL for a given entity and table alias.
2170
     *
2171
     * @param ClassMetadata $targetEntity     Metadata of the target entity.
2172
     * @param string        $targetTableAlias The table alias of the joined/selected table.
2173
     *
2174
     * @return string The SQL query part to add to a query.
2175
     */
2176 575
    protected function generateFilterConditionSQL(ClassMetadata $targetEntity, $targetTableAlias)
2177
    {
2178 575
        $filterClauses = [];
2179
2180 575
        foreach ($this->em->getFilters()->getEnabledFilters() as $filter) {
2181 22
            $filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias);
2182
2183 22
            if ($filterExpr !== '') {
2184 22
                $filterClauses[] = '(' . $filterExpr . ')';
2185
            }
2186
        }
2187
2188 575
        $sql = implode(' AND ', $filterClauses);
2189
2190 575
        return $sql ? '(' . $sql . ')' : ''; // Wrap again to avoid "X or Y and FilterConditionSQL"
2191
    }
2192
2193
    /**
2194
     * Switches persister context according to current query offset/limits
2195
     *
2196
     * This is due to the fact that to-many associations cannot be fetch-joined when a limit is involved
2197
     *
2198
     * @param int|null $offset
2199
     * @param int|null $limit
2200
     */
2201 557
    protected function switchPersisterContext($offset, $limit)
2202
    {
2203 557
        if ($offset === null && $limit === null) {
2204 544
            $this->currentPersisterContext = $this->noLimitsContext;
2205
2206 544
            return;
2207
        }
2208
2209 40
        $this->currentPersisterContext = $this->limitsHandlingContext;
2210 40
    }
2211
}
2212