Failed Conditions
Push — master ( a3e53b...559253 )
by Guilherme
14:58
created

getSelectColumnAssociationSQL()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 28
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 17
nc 3
nop 4
dl 0
loc 28
ccs 16
cts 16
cp 1
crap 5
rs 9.3888
c 0
b 0
f 0
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\OptimisticLockException;
31
use Doctrine\ORM\PersistentCollection;
32
use Doctrine\ORM\Persisters\Exception\CantUseInOperatorOnCompositeKeys;
33
use Doctrine\ORM\Persisters\Exception\InvalidOrientation;
34
use Doctrine\ORM\Persisters\Exception\UnrecognizedField;
35
use Doctrine\ORM\Persisters\SqlExpressionVisitor;
36
use Doctrine\ORM\Persisters\SqlValueVisitor;
37
use Doctrine\ORM\Query;
38
use Doctrine\ORM\Query\QueryException;
39
use Doctrine\ORM\Repository\Exception\InvalidFindByCall;
40
use Doctrine\ORM\UnitOfWork;
41
use Doctrine\ORM\Utility\StaticClassNameConverter;
42
use function array_combine;
43
use function array_keys;
44
use function array_map;
45
use function array_merge;
46
use function array_values;
47
use function get_class;
48
use function implode;
49
use function in_array;
50
use function is_array;
51
use function is_object;
52
use function sprintf;
53
use function strpos;
54
use function strtoupper;
55
use function trim;
56
57
/**
58
 * A BasicEntityPersister maps an entity to a single table in a relational database.
59
 *
60
 * A persister is always responsible for a single entity type.
61
 *
62
 * EntityPersisters are used during a UnitOfWork to apply any changes to the persistent
63
 * state of entities onto a relational database when the UnitOfWork is committed,
64
 * as well as for basic querying of entities and their associations (not DQL).
65
 *
66
 * The persisting operations that are invoked during a commit of a UnitOfWork to
67
 * persist the persistent entity state are:
68
 *
69
 *   - {@link insert} : To insert the persistent state of an entity.
70
 *   - {@link update} : To update the persistent state of an entity.
71
 *   - {@link delete} : To delete the persistent state of an entity.
72
 *
73
 * As can be seen from the above list, insertions are batched and executed all at once
74
 * for increased efficiency.
75
 *
76
 * The querying operations invoked during a UnitOfWork, either through direct find
77
 * requests or lazy-loading, are the following:
78
 *
79
 *   - {@link load} : Loads (the state of) a single, managed entity.
80
 *   - {@link loadAll} : Loads multiple, managed entities.
81
 *   - {@link loadToOneEntity} : Loads a one/many-to-one entity association (lazy-loading).
82
 *   - {@link loadOneToManyCollection} : Loads a one-to-many entity association (lazy-loading).
83
 *   - {@link loadManyToManyCollection} : Loads a many-to-many entity association (lazy-loading).
84
 *
85
 * The BasicEntityPersister implementation provides the default behavior for
86
 * persisting and querying entities that are mapped to a single database table.
87
 *
88
 * Subclasses can be created to provide custom persisting and querying strategies,
89
 * i.e. spanning multiple tables.
90
 */
91
class BasicEntityPersister implements EntityPersister
92
{
93
    /** @var string[] */
94
    private static $comparisonMap = [
95
        Comparison::IS          => '= %s',
96
        Comparison::NEQ         => '!= %s',
97
        Comparison::GT          => '> %s',
98
        Comparison::GTE         => '>= %s',
99
        Comparison::LT          => '< %s',
100
        Comparison::LTE         => '<= %s',
101
        Comparison::IN          => 'IN (%s)',
102
        Comparison::NIN         => 'NOT IN (%s)',
103
        Comparison::CONTAINS    => 'LIKE %s',
104
        Comparison::STARTS_WITH => 'LIKE %s',
105
        Comparison::ENDS_WITH   => 'LIKE %s',
106
    ];
107
108
    /**
109
     * Metadata object that describes the mapping of the mapped entity class.
110
     *
111
     * @var ClassMetadata
112
     */
113
    protected $class;
114
115
    /**
116
     * The underlying DBAL Connection of the used EntityManager.
117
     *
118
     * @var Connection
119
     */
120
    protected $conn;
121
122
    /**
123
     * The database platform.
124
     *
125
     * @var AbstractPlatform
126
     */
127
    protected $platform;
128
129
    /**
130
     * The EntityManager instance.
131
     *
132
     * @var EntityManagerInterface
133
     */
134
    protected $em;
135
136
    /**
137
     * The map of column names to DBAL columns used when INSERTing or UPDATEing an entity.
138
     *
139
     * @see prepareInsertData($entity)
140
     * @see prepareUpdateData($entity)
141
     *
142
     * @var ColumnMetadata[]
143
     */
144
    protected $columns = [];
145
146
    /**
147
     * The INSERT SQL statement used for entities handled by this persister.
148
     * This SQL is only generated once per request, if at all.
149
     *
150
     * @var string
151
     */
152
    private $insertSql;
153
154
    /** @var CachedPersisterContext */
155
    protected $currentPersisterContext;
156
157
    /** @var CachedPersisterContext */
158
    private $limitsHandlingContext;
159
160
    /** @var CachedPersisterContext */
161
    private $noLimitsContext;
162
163
    /**
164
     * Initializes a new <tt>BasicEntityPersister</tt> that uses the given EntityManager
165
     * and persists instances of the class described by the given ClassMetadata descriptor.
166
     */
167 1131
    public function __construct(EntityManagerInterface $em, ClassMetadata $class)
168
    {
169 1131
        $this->em                    = $em;
170 1131
        $this->class                 = $class;
171 1131
        $this->conn                  = $em->getConnection();
172 1131
        $this->platform              = $this->conn->getDatabasePlatform();
173 1131
        $this->noLimitsContext       = $this->currentPersisterContext = new CachedPersisterContext(
174 1131
            $class,
175 1131
            new Query\ResultSetMapping(),
176 1131
            false
177
        );
178 1131
        $this->limitsHandlingContext = new CachedPersisterContext(
179 1131
            $class,
180 1131
            new Query\ResultSetMapping(),
181 1131
            true
182
        );
183 1131
    }
184
185
    /**
186
     * {@inheritdoc}
187
     */
188 15
    public function getClassMetadata()
189
    {
190 15
        return $this->class;
191
    }
192
193
    /**
194
     * {@inheritdoc}
195
     */
196 11
    public function getResultSetMapping()
197
    {
198 11
        return $this->currentPersisterContext->rsm;
199
    }
200
201
    /**
202
     * {@inheritdoc}
203
     */
204 1054
    public function getIdentifier($entity) : array
205
    {
206 1054
        $id = [];
207
208 1054
        foreach ($this->class->getIdentifier() as $fieldName) {
209 1054
            $property = $this->class->getProperty($fieldName);
210 1054
            $value    = $property->getValue($entity);
211
212 1054
            if ($value !== null) {
213 1042
                $id[$fieldName] = $value;
214
            }
215
        }
216
217 1054
        return $id;
218
    }
219
220
    /**
221
     * Populates the entity identifier of an entity.
222
     *
223
     * @param object  $entity
224
     * @param mixed[] $id
225
     */
226 218
    public function setIdentifier($entity, array $id) : void
227
    {
228 218
        foreach ($id as $idField => $idValue) {
229 218
            $property = $this->class->getProperty($idField);
230
231 218
            $property->setValue($entity, $idValue);
232
        }
233 218
    }
234
235
    /**
236
     * {@inheritdoc}
237
     */
238 913
    public function insert($entity)
239
    {
240 913
        $stmt           = $this->conn->prepare($this->getInsertSQL());
241 913
        $tableName      = $this->class->getTableName();
242 913
        $insertData     = $this->prepareInsertData($entity);
243 913
        $generationPlan = $this->class->getValueGenerationPlan();
244
245 913
        if (isset($insertData[$tableName])) {
246 887
            $paramIndex = 1;
247
248 887
            foreach ($insertData[$tableName] as $columnName => $value) {
249 887
                $type = $this->columns[$columnName]->getType();
250
251 887
                $stmt->bindValue($paramIndex++, $value, $type);
252
            }
253
        }
254
255 913
        $stmt->execute();
256
257 912
        if ($generationPlan->containsDeferred()) {
258 856
            $generationPlan->executeDeferred($this->em, $entity);
259
        }
260
261 912
        if ($this->class->isVersioned()) {
262 199
            $this->assignDefaultVersionValue($entity, $this->getIdentifier($entity));
263
        }
264
265 912
        $stmt->closeCursor();
266 912
    }
267
268
    /**
269
     * Retrieves the default version value which was created
270
     * by the preceding INSERT statement and assigns it back in to the
271
     * entities version field.
272
     *
273
     * @param object  $entity
274
     * @param mixed[] $id
275
     */
276 208
    protected function assignDefaultVersionValue($entity, array $id)
277
    {
278 208
        $versionProperty = $this->class->versionProperty;
279 208
        $versionValue    = $this->fetchVersionValue($versionProperty, $id);
0 ignored issues
show
Bug introduced by
It seems like $versionProperty can also be of type null; however, parameter $versionProperty of Doctrine\ORM\Persisters\...er::fetchVersionValue() does only seem to accept Doctrine\ORM\Mapping\FieldMetadata, maybe add an additional type check? ( Ignorable by Annotation )

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

279
        $versionValue    = $this->fetchVersionValue(/** @scrutinizer ignore-type */ $versionProperty, $id);
Loading history...
280
281 208
        $versionProperty->setValue($entity, $versionValue);
0 ignored issues
show
Bug introduced by
The method setValue() does not exist on null. ( Ignorable by Annotation )

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

281
        $versionProperty->/** @scrutinizer ignore-call */ 
282
                          setValue($entity, $versionValue);

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

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

Loading history...
282 208
    }
283
284
    /**
285
     * Fetches the current version value of a versioned entity.
286
     *
287
     * @param mixed[] $id
288
     *
289
     * @return mixed
290
     */
291 208
    protected function fetchVersionValue(FieldMetadata $versionProperty, array $id)
292
    {
293 208
        $versionedClass = $versionProperty->getDeclaringClass();
294 208
        $tableName      = $versionedClass->table->getQuotedQualifiedName($this->platform);
295 208
        $columnName     = $this->platform->quoteIdentifier($versionProperty->getColumnName());
296 208
        $identifier     = array_map(
297
            function ($columnName) {
298 208
                return $this->platform->quoteIdentifier($columnName);
299 208
            },
300 208
            array_keys($versionedClass->getIdentifierColumns($this->em))
0 ignored issues
show
Bug introduced by
The method getIdentifierColumns() does not exist on Doctrine\ORM\Mapping\ComponentMetadata. It seems like you code against a sub-type of Doctrine\ORM\Mapping\ComponentMetadata such as Doctrine\ORM\Mapping\ClassMetadata. ( Ignorable by Annotation )

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

300
            array_keys($versionedClass->/** @scrutinizer ignore-call */ getIdentifierColumns($this->em))
Loading history...
301
        );
302
303
        // FIXME: Order with composite keys might not be correct
304 208
        $sql = 'SELECT ' . $columnName
305 208
             . ' FROM ' . $tableName
306 208
             . ' WHERE ' . implode(' = ? AND ', $identifier) . ' = ?';
307
308 208
        $flattenedId = $this->em->getIdentifierFlattener()->flattenIdentifier($versionedClass, $id);
309 208
        $versionType = $versionProperty->getType();
310
311 208
        $value = $this->conn->fetchColumn(
312 208
            $sql,
313 208
            array_values($flattenedId),
314 208
            $this->extractIdentifierTypes($id, $versionedClass)
315
        );
316
317 208
        return $versionType->convertToPHPValue($value, $this->platform);
318
    }
319
320
    /**
321
     * @param mixed[] $id
322
     *
323
     * @return mixed[]
324
     */
325 208
    private function extractIdentifierTypes(array $id, ClassMetadata $versionedClass) : array
326
    {
327 208
        $types = [];
328
329 208
        foreach ($id as $field => $value) {
330 208
            $types = array_merge($types, $this->getTypes($field, $value, $versionedClass));
331
        }
332
333 208
        return $types;
334
    }
335
336
    /**
337
     * {@inheritdoc}
338
     */
339 79
    public function update($entity)
340
    {
341 79
        $tableName  = $this->class->getTableName();
342 79
        $updateData = $this->prepareUpdateData($entity);
343
344 79
        if (! isset($updateData[$tableName])) {
345 8
            return;
346
        }
347
348 71
        $data = $updateData[$tableName];
349
350 71
        if (! $data) {
351
            return;
352
        }
353
354 71
        $isVersioned     = $this->class->isVersioned();
355 71
        $quotedTableName = $this->class->table->getQuotedQualifiedName($this->platform);
356
357 71
        $this->updateTable($entity, $quotedTableName, $data, $isVersioned);
358
359 69
        if ($isVersioned) {
360 12
            $id = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
361
362 12
            $this->assignDefaultVersionValue($entity, $id);
363
        }
364 69
    }
365
366
    /**
367
     * {@inheritdoc}
368
     */
369 58
    public function delete($entity)
370
    {
371 58
        $class      = $this->class;
372 58
        $unitOfWork = $this->em->getUnitOfWork();
373 58
        $identifier = $unitOfWork->getEntityIdentifier($entity);
374 58
        $tableName  = $class->table->getQuotedQualifiedName($this->platform);
375
376 58
        $types = [];
377 58
        $id    = [];
378
379 58
        foreach ($class->identifier as $field) {
380 58
            $property = $class->getProperty($field);
381
382 58
            if ($property instanceof FieldMetadata) {
383 56
                $columnName       = $property->getColumnName();
384 56
                $quotedColumnName = $this->platform->quoteIdentifier($columnName);
385
386 56
                $id[$quotedColumnName] = $identifier[$field];
387 56
                $types[]               = $property->getType();
388
389 56
                continue;
390
            }
391
392 5
            $targetClass = $this->em->getClassMetadata($property->getTargetEntity());
0 ignored issues
show
Bug introduced by
The method getTargetEntity() does not exist on Doctrine\ORM\Mapping\Property. It seems like you code against a sub-type of Doctrine\ORM\Mapping\Property such as Doctrine\ORM\Mapping\EmbeddedMetadata or Doctrine\ORM\Mapping\AssociationMetadata. ( Ignorable by Annotation )

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

392
            $targetClass = $this->em->getClassMetadata($property->/** @scrutinizer ignore-call */ getTargetEntity());
Loading history...
393 5
            $joinColumns = $property instanceof ManyToManyAssociationMetadata
394
                ? $property->getTable()->getJoinColumns()
0 ignored issues
show
Bug introduced by
The method getTable() does not exist on Doctrine\ORM\Mapping\ManyToManyAssociationMetadata. Did you maybe mean getJoinTable()? ( Ignorable by Annotation )

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

394
                ? $property->/** @scrutinizer ignore-call */ getTable()->getJoinColumns()

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

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

Loading history...
395 5
                : $property->getJoinColumns();
0 ignored issues
show
Bug introduced by
The method getJoinColumns() does not exist on Doctrine\ORM\Mapping\Property. It seems like you code against a sub-type of Doctrine\ORM\Mapping\Property such as Doctrine\ORM\Mapping\ToOneAssociationMetadata. ( Ignorable by Annotation )

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

395
                : $property->/** @scrutinizer ignore-call */ getJoinColumns();
Loading history...
396
397 5
            $associationValue = null;
398 5
            $value            = $identifier[$field];
399
400 5
            if ($value !== null) {
401
                // @todo guilhermeblanco Make sure we do not have flat association values.
402 5
                if (! is_array($value)) {
403 5
                    $value = [$targetClass->identifier[0] => $value];
0 ignored issues
show
Bug introduced by
Accessing identifier on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
404
                }
405
406 5
                $associationValue = $value;
407
            }
408
409 5
            foreach ($joinColumns as $joinColumn) {
410
                /** @var JoinColumnMetadata $joinColumn */
411 5
                $quotedColumnName     = $this->platform->quoteIdentifier($joinColumn->getColumnName());
412 5
                $referencedColumnName = $joinColumn->getReferencedColumnName();
413 5
                $targetField          = $targetClass->fieldNames[$referencedColumnName];
0 ignored issues
show
Bug introduced by
Accessing fieldNames on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
414
415 5
                $id[$quotedColumnName] = $associationValue ? $associationValue[$targetField] : null;
416 5
                $types[]               = $joinColumn->getType();
417
            }
418
        }
419
420 58
        $this->deleteJoinTableRecords($identifier);
421
422 58
        return (bool) $this->conn->delete($tableName, $id, $types);
423
    }
424
425
    /**
426
     * Performs an UPDATE statement for an entity on a specific table.
427
     * The UPDATE can optionally be versioned, which requires the entity to have a version field.
428
     *
429
     * @param object  $entity          The entity object being updated.
430
     * @param string  $quotedTableName The quoted name of the table to apply the UPDATE on.
431
     * @param mixed[] $updateData      The map of columns to update (column => value).
432
     * @param bool    $versioned       Whether the UPDATE should be versioned.
433
     *
434
     * @throws ORMException
435
     * @throws OptimisticLockException
436
     */
437 102
    final protected function updateTable($entity, $quotedTableName, array $updateData, $versioned = false)
438
    {
439 102
        $set    = [];
440 102
        $types  = [];
441 102
        $params = [];
442
443 102
        foreach ($updateData as $columnName => $value) {
444 102
            $column           = $this->columns[$columnName];
445 102
            $quotedColumnName = $this->platform->quoteIdentifier($column->getColumnName());
446 102
            $type             = $column->getType();
447 102
            $placeholder      = $type->convertToDatabaseValueSQL('?', $this->platform);
448
449 102
            $set[]    = sprintf('%s = %s', $quotedColumnName, $placeholder);
450 102
            $params[] = $value;
451 102
            $types[]  = $column->getType();
452
        }
453
454
        // @todo guilhermeblanco Bring this back: $this->em->getUnitOfWork()->getEntityIdentifier($entity);
455 102
        $identifier = $this->getIdentifier($entity);
456 102
        $where      = [];
457
458 102
        foreach ($this->class->identifier as $idField) {
459 102
            $property = $this->class->getProperty($idField);
460
461
            switch (true) {
462 102
                case $property instanceof FieldMetadata:
463 97
                    $where[]  = $this->platform->quoteIdentifier($property->getColumnName());
464 97
                    $params[] = $identifier[$idField];
465 97
                    $types[]  = $property->getType();
466 97
                    break;
467
468 6
                case $property instanceof ToOneAssociationMetadata:
469 6
                    $targetPersister = $this->em->getUnitOfWork()->getEntityPersister($property->getTargetEntity());
470
471 6
                    foreach ($property->getJoinColumns() as $joinColumn) {
472
                        /** @var JoinColumnMetadata $joinColumn */
473 6
                        $quotedColumnName     = $this->platform->quoteIdentifier($joinColumn->getColumnName());
474 6
                        $referencedColumnName = $joinColumn->getReferencedColumnName();
475 6
                        $value                = $targetPersister->getColumnValue($identifier[$idField], $referencedColumnName);
0 ignored issues
show
Bug introduced by
It seems like $referencedColumnName can also be of type null; however, parameter $columnName of Doctrine\ORM\Persisters\...ister::getColumnValue() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

475
                        $value                = $targetPersister->getColumnValue($identifier[$idField], /** @scrutinizer ignore-type */ $referencedColumnName);
Loading history...
476
477 6
                        $where[]  = $quotedColumnName;
478 6
                        $params[] = $value;
479 6
                        $types[]  = $joinColumn->getType();
480
                    }
481 6
                    break;
482
            }
483
        }
484
485 102
        if ($versioned) {
486 20
            $versionProperty   = $this->class->versionProperty;
487 20
            $versionColumnType = $versionProperty->getType();
488 20
            $versionColumnName = $this->platform->quoteIdentifier($versionProperty->getColumnName());
489
490 20
            $where[]  = $versionColumnName;
491 20
            $types[]  = $versionColumnType;
492 20
            $params[] = $versionProperty->getValue($entity);
493
494 20
            switch ($versionColumnType->getName()) {
495
                case Type::SMALLINT:
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Type::SMALLINT has been deprecated: Use {@see Types::SMALLINT} instead. ( Ignorable by Annotation )

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

495
                case /** @scrutinizer ignore-deprecated */ Type::SMALLINT:

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
496
                case Type::INTEGER:
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Type::INTEGER has been deprecated: Use {@see Types::INTEGER} instead. ( Ignorable by Annotation )

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

496
                case /** @scrutinizer ignore-deprecated */ Type::INTEGER:

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
497
                case Type::BIGINT:
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Type::BIGINT has been deprecated: Use {@see Types::BIGINT} instead. ( Ignorable by Annotation )

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

497
                case /** @scrutinizer ignore-deprecated */ Type::BIGINT:

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
498 18
                    $set[] = $versionColumnName . ' = ' . $versionColumnName . ' + 1';
499 18
                    break;
500
501
                case Type::DATETIME:
0 ignored issues
show
Deprecated Code introduced by
The constant Doctrine\DBAL\Types\Type::DATETIME has been deprecated: Use {@see Types::DATETIME_MUTABLE} instead. ( Ignorable by Annotation )

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

501
                case /** @scrutinizer ignore-deprecated */ Type::DATETIME:

This class constant has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.

Loading history...
502 2
                    $set[] = $versionColumnName . ' = CURRENT_TIMESTAMP';
503 2
                    break;
504
            }
505
        }
506
507 102
        $sql = 'UPDATE ' . $quotedTableName
508 102
             . ' SET ' . implode(', ', $set)
509 102
             . ' WHERE ' . implode(' = ? AND ', $where) . ' = ?';
510
511 102
        $result = $this->conn->executeUpdate($sql, $params, $types);
512
513 102
        if ($versioned && ! $result) {
514 4
            throw OptimisticLockException::lockFailed($entity);
515
        }
516 99
    }
517
518
    /**
519
     * @param mixed[] $identifier
520
     *
521
     * @todo Add check for platform if it supports foreign keys/cascading.
522
     */
523 61
    protected function deleteJoinTableRecords($identifier)
524
    {
525 61
        foreach ($this->class->getPropertiesIterator() as $association) {
526 61
            if (! ($association instanceof ManyToManyAssociationMetadata)) {
527 61
                continue;
528
            }
529
530
            // @Todo this only covers scenarios with no inheritance or of the same level. Is there something
531
            // like self-referential relationship between different levels of an inheritance hierarchy? I hope not!
532 23
            $selfReferential   = $association->getTargetEntity() === $association->getSourceEntity();
533 23
            $owningAssociation = $association;
534 23
            $otherColumns      = [];
535 23
            $otherKeys         = [];
536 23
            $keys              = [];
537
538 23
            if (! $owningAssociation->isOwningSide()) {
539 6
                $class             = $this->em->getClassMetadata($association->getTargetEntity());
540 6
                $owningAssociation = $class->getProperty($association->getMappedBy());
0 ignored issues
show
Bug introduced by
The method getProperty() does not exist on Doctrine\Persistence\Mapping\ClassMetadata. ( Ignorable by Annotation )

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

540
                /** @scrutinizer ignore-call */ 
541
                $owningAssociation = $class->getProperty($association->getMappedBy());

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

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

Loading history...
541
            }
542
543 23
            $joinTable     = $owningAssociation->getJoinTable();
544 23
            $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
545 23
            $joinColumns   = $association->isOwningSide()
546 19
                ? $joinTable->getJoinColumns()
547 23
                : $joinTable->getInverseJoinColumns();
548
549 23
            if ($selfReferential) {
550 1
                $otherColumns = ! $association->isOwningSide()
551
                    ? $joinTable->getJoinColumns()
552 1
                    : $joinTable->getInverseJoinColumns();
553
            }
554
555 23
            $isOnDeleteCascade = false;
556
557 23
            foreach ($joinColumns as $joinColumn) {
558 23
                $keys[] = $this->platform->quoteIdentifier($joinColumn->getColumnName());
559
560 23
                if ($joinColumn->isOnDeleteCascade()) {
561 23
                    $isOnDeleteCascade = true;
562
                }
563
            }
564
565 23
            foreach ($otherColumns as $joinColumn) {
566 1
                $otherKeys[] = $this->platform->quoteIdentifier($joinColumn->getColumnName());
567
568 1
                if ($joinColumn->isOnDeleteCascade()) {
569 1
                    $isOnDeleteCascade = true;
570
                }
571
            }
572
573 23
            if ($isOnDeleteCascade) {
574 23
                continue;
575
            }
576
577
            $this->conn->delete($joinTableName, array_combine($keys, $identifier));
0 ignored issues
show
Bug introduced by
It seems like array_combine($keys, $identifier) can also be of type false; however, parameter $identifier of Doctrine\DBAL\Connection::delete() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

577
            $this->conn->delete($joinTableName, /** @scrutinizer ignore-type */ array_combine($keys, $identifier));
Loading history...
578
579
            if ($selfReferential) {
580
                $this->conn->delete($joinTableName, array_combine($otherKeys, $identifier));
581
            }
582
        }
583 61
    }
584
585
    /**
586
     * Prepares the data changeset of a managed entity for database insertion (initial INSERT).
587
     * The changeset of the entity is obtained from the currently running UnitOfWork.
588
     *
589
     * The default insert data preparation is the same as for updates.
590
     *
591
     * @param object $entity The entity for which to prepare the data.
592
     *
593
     * @return mixed[] The prepared data for the tables to update.
594
     */
595 1000
    protected function prepareInsertData($entity) : array
596
    {
597 1000
        return $this->prepareUpdateData($entity);
598
    }
599
600
    /**
601
     * Prepares the changeset of an entity for database insertion (UPDATE).
602
     *
603
     * The changeset is obtained from the currently running UnitOfWork.
604
     *
605
     * During this preparation the array that is passed as the second parameter is filled with
606
     * <columnName> => <value> pairs, grouped by table name.
607
     *
608
     * Example:
609
     * <code>
610
     * array(
611
     *    'foo_table' => array('column1' => 'value1', 'column2' => 'value2', ...),
612
     *    'bar_table' => array('columnX' => 'valueX', 'columnY' => 'valueY', ...),
613
     *    ...
614
     * )
615
     * </code>
616
     *
617
     * @param object $entity The entity for which to prepare the data.
618
     *
619
     * @return mixed[] The prepared data.
620
     */
621 1005
    protected function prepareUpdateData($entity)
622
    {
623 1005
        $uow                 = $this->em->getUnitOfWork();
624 1005
        $result              = [];
625 1005
        $versionPropertyName = $this->class->isVersioned()
626 212
            ? $this->class->versionProperty->getName()
627 1005
            : null;
628
629
        // @todo guilhermeblanco This should check column insertability/updateability instead of field changeset
630 1005
        foreach ($uow->getEntityChangeSet($entity) as $propertyName => $propertyChangeSet) {
631 970
            if ($versionPropertyName === $propertyName) {
632
                continue;
633
            }
634
635 970
            $property = $this->class->getProperty($propertyName);
636 970
            $newValue = $propertyChangeSet[1];
637
638 970
            if ($property instanceof FieldMetadata) {
639
                // @todo guilhermeblanco Please remove this in the future for good...
640 934
                $this->columns[$property->getColumnName()] = $property;
641
642 934
                $result[$property->getTableName()][$property->getColumnName()] = $newValue;
643
644 934
                continue;
645
            }
646
647
            // Only owning side of x-1 associations can have a FK column.
648 829
            if (! $property instanceof ToOneAssociationMetadata || ! $property->isOwningSide()) {
649 8
                continue;
650
            }
651
652
            // The associated entity $newVal is not yet persisted, so we must
653
            // set $newVal = null, in order to insert a null value and schedule an
654
            // extra update on the UnitOfWork.
655 829
            if ($newValue !== null && $uow->isScheduledForInsert($newValue)) {
656 28
                $uow->scheduleExtraUpdate($entity, [$propertyName => [null, $newValue]]);
657
658 28
                $newValue = null;
659
            }
660
661 829
            $targetClass     = $this->em->getClassMetadata($property->getTargetEntity());
662 829
            $targetPersister = $uow->getEntityPersister($targetClass->getClassName());
0 ignored issues
show
Bug introduced by
The method getClassName() does not exist on Doctrine\Persistence\Mapping\ClassMetadata. ( Ignorable by Annotation )

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

662
            $targetPersister = $uow->getEntityPersister($targetClass->/** @scrutinizer ignore-call */ getClassName());

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

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

Loading history...
663
664 829
            foreach ($property->getJoinColumns() as $joinColumn) {
665
                /** @var JoinColumnMetadata $joinColumn */
666 829
                $referencedColumnName = $joinColumn->getReferencedColumnName();
667
668
                // @todo guilhermeblanco Please remove this in the future for good...
669 829
                $this->columns[$joinColumn->getColumnName()] = $joinColumn;
670
671 829
                $result[$joinColumn->getTableName()][$joinColumn->getColumnName()] = $newValue !== null
672 611
                    ? $targetPersister->getColumnValue($newValue, $referencedColumnName)
0 ignored issues
show
Bug introduced by
It seems like $referencedColumnName can also be of type null; however, parameter $columnName of Doctrine\ORM\Persisters\...ister::getColumnValue() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

672
                    ? $targetPersister->getColumnValue($newValue, /** @scrutinizer ignore-type */ $referencedColumnName)
Loading history...
673 570
                    : null;
674
            }
675
        }
676
677 1005
        return $result;
678
    }
679
680
    /**
681
     * @param object $entity
682
     *
683
     * @return mixed|null
684
     */
685 611
    public function getColumnValue($entity, string $columnName)
686
    {
687
        // Looking for fields by column is the easiest way to look at local columns or x-1 owning side associations
688 611
        $propertyName = $this->class->fieldNames[$columnName];
689 611
        $property     = $this->class->getProperty($propertyName);
690
691 611
        if (! $property) {
692
            return null;
693
        }
694
695 611
        $propertyValue = $property->getValue($entity);
696
697 611
        if ($property instanceof LocalColumnMetadata) {
698 611
            return $propertyValue;
699
        }
700
701
        /** @var ToOneAssociationMetadata $property */
702 20
        $unitOfWork      = $this->em->getUnitOfWork();
703 20
        $targetClass     = $this->em->getClassMetadata($property->getTargetEntity());
0 ignored issues
show
Unused Code introduced by
The assignment to $targetClass is dead and can be removed.
Loading history...
704 20
        $targetPersister = $unitOfWork->getEntityPersister($property->getTargetEntity());
705
706 20
        foreach ($property->getJoinColumns() as $joinColumn) {
707
            /** @var JoinColumnMetadata $joinColumn */
708 20
            $referencedColumnName = $joinColumn->getReferencedColumnName();
709
710 20
            if ($joinColumn->getColumnName() !== $columnName) {
711
                continue;
712
            }
713
714 20
            return $targetPersister->getColumnValue($propertyValue, $referencedColumnName);
0 ignored issues
show
Bug introduced by
It seems like $referencedColumnName can also be of type null; however, parameter $columnName of Doctrine\ORM\Persisters\...ister::getColumnValue() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

714
            return $targetPersister->getColumnValue($propertyValue, /** @scrutinizer ignore-type */ $referencedColumnName);
Loading history...
715
        }
716
717
        return null;
718
    }
719
720
    /**
721
     * {@inheritdoc}
722
     */
723 470
    public function load(
724
        array $criteria,
725
        $entity = null,
726
        ?AssociationMetadata $association = null,
727
        array $hints = [],
728
        $lockMode = null,
729
        $limit = null,
730
        array $orderBy = []
731
    ) {
732 470
        $this->switchPersisterContext(null, $limit);
733
734 470
        $sql = $this->getSelectSQL($criteria, $association, $lockMode, $limit, null, $orderBy);
735
736 469
        [$params, $types] = $this->expandParameters($criteria);
737
738 469
        $stmt = $this->conn->executeQuery($sql, $params, $types);
739
740 469
        if ($entity !== null) {
741 62
            $hints[Query::HINT_REFRESH]        = true;
742 62
            $hints[Query::HINT_REFRESH_ENTITY] = $entity;
743
        }
744
745 469
        $hydrator = $this->em->newHydrator($this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
746 469
        $entities = $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, $hints);
747
748 469
        return $entities ? $entities[0] : null;
749
    }
750
751
    /**
752
     * {@inheritdoc}
753
     */
754 397
    public function loadById(array $identifier, $entity = null)
755
    {
756 397
        return $this->load($identifier, $entity);
757
    }
758
759
    /**
760
     * {@inheritdoc}
761
     */
762 92
    public function loadToOneEntity(ToOneAssociationMetadata $association, $sourceEntity, array $identifier = [])
763
    {
764 92
        $unitOfWork   = $this->em->getUnitOfWork();
765 92
        $targetEntity = $association->getTargetEntity();
766 92
        $foundEntity  = $unitOfWork->tryGetById($identifier, $targetEntity);
767
768 92
        if ($foundEntity !== false) {
769
            return $foundEntity;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $foundEntity also could return the type true which is incompatible with the return type mandated by Doctrine\ORM\Persisters\...ster::loadToOneEntity() of object.
Loading history...
770
        }
771
772 92
        $targetClass = $this->em->getClassMetadata($targetEntity);
773
774 92
        if ($association->isOwningSide()) {
775 29
            $inversedBy            = $association->getInversedBy();
776 29
            $targetProperty        = $inversedBy ? $targetClass->getProperty($inversedBy) : null;
777 29
            $isInverseSingleValued = $targetProperty && $targetProperty instanceof ToOneAssociationMetadata;
778
779
            // Mark inverse side as fetched in the hints, otherwise the UoW would
780
            // try to load it in a separate query (remember: to-one inverse sides can not be lazy).
781 29
            $hints = [];
782
783 29
            if ($isInverseSingleValued) {
784
                $hints['fetched']['r'][$inversedBy] = true;
785
            }
786
787
            /* cascade read-only status
788
            if ($this->em->getUnitOfWork()->isReadOnly($sourceEntity)) {
789
                $hints[Query::HINT_READ_ONLY] = true;
790
            }
791
            */
792
793 29
            $entity = $this->load($identifier, null, $association, $hints);
794
795
            // Complete bidirectional association, if necessary
796 29
            if ($entity !== null && $isInverseSingleValued) {
797
                $targetProperty->setValue($entity, $sourceEntity);
798
            }
799
800 29
            return $entity;
801
        }
802
803 63
        $sourceClass       = $association->getDeclaringClass();
804 63
        $owningAssociation = $targetClass->getProperty($association->getMappedBy());
805 63
        $targetTableAlias  = $this->getSQLTableAlias($targetClass->getTableName());
0 ignored issues
show
Unused Code introduced by
The assignment to $targetTableAlias is dead and can be removed.
Loading history...
Bug introduced by
The method getTableName() does not exist on Doctrine\Persistence\Mapping\ClassMetadata. ( Ignorable by Annotation )

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

805
        $targetTableAlias  = $this->getSQLTableAlias($targetClass->/** @scrutinizer ignore-call */ getTableName());

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

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

Loading history...
806
807 63
        foreach ($owningAssociation->getJoinColumns() as $joinColumn) {
808 63
            $sourceKeyColumn = $joinColumn->getReferencedColumnName();
809 63
            $targetKeyColumn = $joinColumn->getColumnName();
810
811 63
            if (! isset($sourceClass->fieldNames[$sourceKeyColumn])) {
812
                throw MappingException::joinColumnMustPointToMappedField(
813
                    $sourceClass->getClassName(),
814
                    $sourceKeyColumn
815
                );
816
            }
817
818 63
            $property = $sourceClass->getProperty($sourceClass->fieldNames[$sourceKeyColumn]);
819 63
            $value    = $property->getValue($sourceEntity);
820
821
            // unset the old value and set the new sql aliased value here. By definition
822
            // unset($identifier[$targetKeyColumn] works here with how UnitOfWork::createEntity() calls this method.
823
            // @todo guilhermeblanco In master we have: $identifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
824 63
            unset($identifier[$targetKeyColumn]);
825
826 63
            $identifier[$targetClass->fieldNames[$targetKeyColumn]] = $value;
0 ignored issues
show
Bug introduced by
Accessing fieldNames on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
827
        }
828
829 63
        $entity = $this->load($identifier, null, $association);
830
831 63
        if ($entity !== null) {
832 17
            $owningAssociation->setValue($entity, $sourceEntity);
833
        }
834
835 63
        return $entity;
836
    }
837
838
    /**
839
     * {@inheritdoc}
840
     */
841 15
    public function refresh(array $id, $entity, $lockMode = null)
842
    {
843 15
        $sql              = $this->getSelectSQL($id, null, $lockMode);
844 15
        [$params, $types] = $this->expandParameters($id);
845 15
        $stmt             = $this->conn->executeQuery($sql, $params, $types);
846
847 15
        $hydrator = $this->em->newHydrator(Query::HYDRATE_OBJECT);
848 15
        $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [Query::HINT_REFRESH => true]);
849 15
    }
850
851
    /**
852
     * {@inheritDoc}
853
     */
854 46
    public function count($criteria = [])
855
    {
856 46
        $sql = $this->getCountSQL($criteria);
857
858 46
        [$params, $types] = $criteria instanceof Criteria
859 25
            ? $this->expandCriteriaParameters($criteria)
860 46
            : $this->expandParameters($criteria);
861
862 46
        return (int) $this->conn->executeQuery($sql, $params, $types)->fetchColumn();
863
    }
864
865
    /**
866
     * {@inheritdoc}
867
     */
868 8
    public function loadCriteria(Criteria $criteria)
869
    {
870 8
        $orderBy = $criteria->getOrderings();
871 8
        $limit   = $criteria->getMaxResults();
872 8
        $offset  = $criteria->getFirstResult();
873 8
        $query   = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy);
874
875 6
        [$params, $types] = $this->expandCriteriaParameters($criteria);
876
877 6
        $stmt         = $this->conn->executeQuery($query, $params, $types);
878 6
        $rsm          = $this->currentPersisterContext->rsm;
879 6
        $hints        = [UnitOfWork::HINT_DEFEREAGERLOAD => true];
880 6
        $hydratorType = $this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT;
881 6
        $hydrator     = $this->em->newHydrator($hydratorType);
882
883 6
        return $hydrator->hydrateAll($stmt, $rsm, $hints);
884
    }
885
886
    /**
887
     * {@inheritdoc}
888
     */
889 37
    public function expandCriteriaParameters(Criteria $criteria)
890
    {
891 37
        $expression = $criteria->getWhereExpression();
892 37
        $sqlParams  = [];
893 37
        $sqlTypes   = [];
894
895 37
        if ($expression === null) {
896 2
            return [$sqlParams, $sqlTypes];
897
        }
898
899 36
        $valueVisitor = new SqlValueVisitor();
900
901 36
        $valueVisitor->dispatch($expression);
902
903 36
        [$params, $types] = $valueVisitor->getParamsAndTypes();
904
905 36
        foreach ($params as $param) {
906 32
            $sqlParams = array_merge($sqlParams, $this->getValues($param));
907
        }
908
909 36
        foreach ($types as $type) {
910 32
            [$field, $value] = $type;
911 32
            $sqlTypes        = array_merge($sqlTypes, $this->getTypes($field, $value, $this->class));
912
        }
913
914 36
        return [$sqlParams, $sqlTypes];
915
    }
916
917
    /**
918
     * {@inheritdoc}
919
     */
920 70
    public function loadAll(array $criteria = [], array $orderBy = [], $limit = null, $offset = null)
921
    {
922 70
        $this->switchPersisterContext($offset, $limit);
923
924 70
        $sql = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy);
925
926 66
        [$params, $types] = $this->expandParameters($criteria);
927
928 66
        $stmt         = $this->conn->executeQuery($sql, $params, $types);
929 66
        $rsm          = $this->currentPersisterContext->rsm;
930 66
        $hints        = [UnitOfWork::HINT_DEFEREAGERLOAD => true];
931 66
        $hydratorType = $this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT;
932 66
        $hydrator     = $this->em->newHydrator($hydratorType);
933
934 66
        return $hydrator->hydrateAll($stmt, $rsm, $hints);
935
    }
936
937
    /**
938
     * {@inheritdoc}
939
     */
940 8
    public function getManyToManyCollection(
941
        ManyToManyAssociationMetadata $association,
942
        $sourceEntity,
943
        $offset = null,
944
        $limit = null
945
    ) {
946 8
        $this->switchPersisterContext($offset, $limit);
947
948 8
        $stmt = $this->getManyToManyStatement($association, $sourceEntity, $offset, $limit);
949
950 8
        return $this->loadArrayFromStatement($association, $stmt);
951
    }
952
953
    /**
954
     * {@inheritdoc}
955
     */
956 73
    public function loadManyToManyCollection(
957
        ManyToManyAssociationMetadata $association,
958
        $sourceEntity,
959
        PersistentCollection $collection
960
    ) {
961 73
        $stmt = $this->getManyToManyStatement($association, $sourceEntity);
962
963 73
        return $this->loadCollectionFromStatement($association, $stmt, $collection);
964
    }
965
966
    /**
967
     * Loads an array of entities from a given DBAL statement.
968
     *
969
     * @param Statement $stmt
970
     *
971
     * @return mixed[]
972
     */
973 13
    private function loadArrayFromStatement(ToManyAssociationMetadata $association, $stmt)
974
    {
975 13
        $rsm = $this->currentPersisterContext->rsm;
976
977 13
        if ($association->getIndexedBy()) {
978 7
            $rsm = clone $this->currentPersisterContext->rsm; // this is necessary because the "default rsm" should be changed.
979 7
            $rsm->addIndexBy('r', $association->getIndexedBy());
980
        }
981
982 13
        $hydrator = $this->em->newHydrator(Query::HYDRATE_OBJECT);
983 13
        $hints    = [UnitOfWork::HINT_DEFEREAGERLOAD => true];
984
985 13
        return $hydrator->hydrateAll($stmt, $rsm, $hints);
986
    }
987
988
    /**
989
     * Hydrates a collection from a given DBAL statement.
990
     *
991
     * @param Statement            $stmt
992
     * @param PersistentCollection $collection
993
     *
994
     * @return mixed[]
995
     */
996 136
    private function loadCollectionFromStatement(ToManyAssociationMetadata $association, $stmt, $collection)
997
    {
998 136
        $rsm = $this->currentPersisterContext->rsm;
999
1000 136
        if ($association->getIndexedBy()) {
1001 10
            $rsm = clone $this->currentPersisterContext->rsm; // this is necessary because the "default rsm" should be changed.
1002 10
            $rsm->addIndexBy('r', $association->getIndexedBy());
1003
        }
1004
1005 136
        $hydrator = $this->em->newHydrator(Query::HYDRATE_OBJECT);
1006
        $hints    = [
1007 136
            UnitOfWork::HINT_DEFEREAGERLOAD => true,
1008 136
            'collection' => $collection,
1009
        ];
1010
1011 136
        return $hydrator->hydrateAll($stmt, $rsm, $hints);
1012
    }
1013
1014
    /**
1015
     * @param object   $sourceEntity
1016
     * @param int|null $offset
1017
     * @param int|null $limit
1018
     *
1019
     * @return DriverStatement
1020
     *
1021
     * @throws MappingException
1022
     */
1023 80
    private function getManyToManyStatement(
1024
        ManyToManyAssociationMetadata $association,
1025
        $sourceEntity,
1026
        $offset = null,
1027
        $limit = null
1028
    ) {
1029 80
        $this->switchPersisterContext($offset, $limit);
1030
1031
        /** @var ClassMetadata $sourceClass */
1032 80
        $sourceClass = $this->em->getClassMetadata($association->getSourceEntity());
1033 80
        $class       = $sourceClass;
0 ignored issues
show
Unused Code introduced by
The assignment to $class is dead and can be removed.
Loading history...
1034 80
        $owningAssoc = $association;
1035 80
        $criteria    = [];
1036 80
        $parameters  = [];
1037
1038 80
        if (! $association->isOwningSide()) {
1039 12
            $class       = $this->em->getClassMetadata($association->getTargetEntity());
1040 12
            $owningAssoc = $class->getProperty($association->getMappedBy());
1041
        }
1042
1043 80
        $joinTable     = $owningAssoc->getJoinTable();
1044 80
        $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
1045 80
        $joinColumns   = $association->isOwningSide()
1046 73
            ? $joinTable->getJoinColumns()
1047 80
            : $joinTable->getInverseJoinColumns();
1048
1049 80
        foreach ($joinColumns as $joinColumn) {
1050 80
            $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName());
1051 80
            $fieldName        = $sourceClass->fieldNames[$joinColumn->getReferencedColumnName()];
1052 80
            $property         = $sourceClass->getProperty($fieldName);
1053 80
            $value            = null;
1054
1055 80
            if ($property instanceof FieldMetadata) {
1056 79
                $value = $property->getValue($sourceEntity);
1057 4
            } elseif ($property instanceof AssociationMetadata) {
1058 4
                $property    = $sourceClass->getProperty($fieldName);
1059 4
                $targetClass = $this->em->getClassMetadata($property->getTargetEntity());
1060 4
                $value       = $property->getValue($sourceEntity);
1061
1062 4
                $value = $this->em->getUnitOfWork()->getEntityIdentifier($value);
1063 4
                $value = $value[$targetClass->identifier[0]];
0 ignored issues
show
Bug introduced by
Accessing identifier on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1064
            }
1065
1066 80
            $criteria[$joinTableName . '.' . $quotedColumnName] = $value;
1067 80
            $parameters[]                                       = [
1068 80
                'value' => $value,
1069 80
                'field' => $fieldName,
1070 80
                'class' => $sourceClass,
1071
            ];
1072
        }
1073
1074 80
        $sql = $this->getSelectSQL($criteria, $association, null, $limit, $offset);
1075
1076 80
        [$params, $types] = $this->expandToManyParameters($parameters);
1077
1078 80
        return $this->conn->executeQuery($sql, $params, $types);
1079
    }
1080
1081
    /**
1082
     * {@inheritdoc}
1083
     */
1084 521
    public function getSelectSQL(
1085
        $criteria,
1086
        ?AssociationMetadata $association = null,
1087
        $lockMode = null,
1088
        $limit = null,
1089
        $offset = null,
1090
        array $orderBy = []
1091
    ) {
1092 521
        $this->switchPersisterContext($offset, $limit);
1093
1094 521
        $lockSql    = '';
1095 521
        $joinSql    = '';
1096 521
        $orderBySql = '';
1097
1098 521
        if ($association instanceof ManyToManyAssociationMetadata) {
1099 81
            $joinSql = $this->getSelectManyToManyJoinSQL($association);
1100
        }
1101
1102 521
        if ($association instanceof ToManyAssociationMetadata && $association->getOrderBy()) {
1103 5
            $orderBy = $association->getOrderBy();
1104
        }
1105
1106 521
        if ($orderBy) {
1107 11
            $orderBySql = $this->getOrderBySQL($orderBy, $this->getSQLTableAlias($this->class->getTableName()));
1108
        }
1109
1110 519
        $conditionSql = $criteria instanceof Criteria
1111 8
            ? $this->getSelectConditionCriteriaSQL($criteria)
1112 517
            : $this->getSelectConditionSQL($criteria, $association);
1113
1114
        switch ($lockMode) {
1115 514
            case LockMode::PESSIMISTIC_READ:
1116
                $lockSql = ' ' . $this->platform->getReadLockSQL();
1117
                break;
1118
1119 514
            case LockMode::PESSIMISTIC_WRITE:
1120
                $lockSql = ' ' . $this->platform->getWriteLockSQL();
1121
                break;
1122
        }
1123
1124 514
        $columnList = $this->getSelectColumnsSQL();
1125 514
        $tableAlias = $this->getSQLTableAlias($this->class->getTableName());
1126 514
        $filterSql  = $this->generateFilterConditionSQL($this->class, $tableAlias);
1127 514
        $tableName  = $this->class->table->getQuotedQualifiedName($this->platform);
1128
1129 514
        if ($filterSql !== '') {
1130 12
            $conditionSql = $conditionSql
1131 11
                ? $conditionSql . ' AND ' . $filterSql
1132 12
                : $filterSql;
1133
        }
1134
1135 514
        $select = 'SELECT ' . $columnList;
1136 514
        $from   = ' FROM ' . $tableName . ' ' . $tableAlias;
1137 514
        $join   = $this->currentPersisterContext->selectJoinSql . $joinSql;
1138 514
        $where  = ($conditionSql ? ' WHERE ' . $conditionSql : '');
1139 514
        $lock   = $this->platform->appendLockHint($from, $lockMode);
1140
        $query  = $select
1141 514
            . $lock
1142 514
            . $join
1143 514
            . $where
1144 514
            . $orderBySql;
1145
1146 514
        return $this->platform->modifyLimitQuery($query, $limit, $offset ?? 0) . $lockSql;
1147
    }
1148
1149
    /**
1150
     * {@inheritDoc}
1151
     */
1152 41
    public function getCountSQL($criteria = [])
1153
    {
1154 41
        $tableName  = $this->class->table->getQuotedQualifiedName($this->platform);
1155 41
        $tableAlias = $this->getSQLTableAlias($this->class->getTableName());
1156
1157 41
        $conditionSql = $criteria instanceof Criteria
1158 25
            ? $this->getSelectConditionCriteriaSQL($criteria)
1159 41
            : $this->getSelectConditionSQL($criteria);
1160
1161 41
        $filterSql = $this->generateFilterConditionSQL($this->class, $tableAlias);
1162
1163 41
        if ($filterSql !== '') {
1164 2
            $conditionSql = $conditionSql
1165 2
                ? $conditionSql . ' AND ' . $filterSql
1166 2
                : $filterSql;
1167
        }
1168
1169
        return 'SELECT COUNT(*) '
1170 41
            . 'FROM ' . $tableName . ' ' . $tableAlias
1171 41
            . (empty($conditionSql) ? '' : ' WHERE ' . $conditionSql);
1172
    }
1173
1174
    /**
1175
     * Gets the ORDER BY SQL snippet for ordered collections.
1176
     *
1177
     * @param mixed[] $orderBy
1178
     * @param string  $baseTableAlias
1179
     *
1180
     * @return string
1181
     *
1182
     * @throws ORMException
1183
     */
1184 80
    final protected function getOrderBySQL(array $orderBy, $baseTableAlias)
1185
    {
1186 80
        if (! $orderBy) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $orderBy of type array<mixed,mixed> is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1187 68
            return '';
1188
        }
1189
1190 12
        $orderByList = [];
1191
1192 12
        foreach ($orderBy as $fieldName => $orientation) {
1193 12
            $orientation = strtoupper(trim($orientation));
1194
1195 12
            if (! in_array($orientation, ['ASC', 'DESC'], true)) {
1196 1
                throw InvalidOrientation::fromClassNameAndField($this->class->getClassName(), $fieldName);
1197
            }
1198
1199 11
            $property = $this->class->getProperty($fieldName);
1200
1201 11
            if ($property instanceof FieldMetadata) {
1202 9
                $tableAlias = $this->getSQLTableAlias($property->getTableName());
1203 9
                $columnName = $this->platform->quoteIdentifier($property->getColumnName());
1204
1205 9
                $orderByList[] = $tableAlias . '.' . $columnName . ' ' . $orientation;
1206
1207 9
                continue;
1208 2
            } elseif ($property instanceof AssociationMetadata) {
1209 2
                if (! $property->isOwningSide()) {
1210 1
                    throw InvalidFindByCall::fromInverseSideUsage(
1211 1
                        $this->class->getClassName(),
1212
                        $fieldName
1213
                    );
1214
                }
1215
1216 1
                $class      = $this->class->isInheritedProperty($fieldName)
1217
                    ? $property->getDeclaringClass()
1218 1
                    : $this->class;
1219 1
                $tableAlias = $this->getSQLTableAlias($class->getTableName());
1220
1221 1
                foreach ($property->getJoinColumns() as $joinColumn) {
0 ignored issues
show
Bug introduced by
The method getJoinColumns() does not exist on Doctrine\ORM\Mapping\AssociationMetadata. It seems like you code against a sub-type of Doctrine\ORM\Mapping\AssociationMetadata such as Doctrine\ORM\Mapping\ToOneAssociationMetadata. ( Ignorable by Annotation )

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

1221
                foreach ($property->/** @scrutinizer ignore-call */ getJoinColumns() as $joinColumn) {
Loading history...
1222
                    /** @var JoinColumnMetadata $joinColumn */
1223 1
                    $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName());
1224
1225 1
                    $orderByList[] = $tableAlias . '.' . $quotedColumnName . ' ' . $orientation;
1226
                }
1227
1228 1
                continue;
1229
            }
1230
1231
            throw UnrecognizedField::byName($fieldName);
1232
        }
1233
1234 10
        return ' ORDER BY ' . implode(', ', $orderByList);
1235
    }
1236
1237
    /**
1238
     * Gets the SQL fragment with the list of columns to select when querying for
1239
     * an entity in this persister.
1240
     *
1241
     * Subclasses should override this method to alter or change the select column
1242
     * list SQL fragment. Note that in the implementation of BasicEntityPersister
1243
     * the resulting SQL fragment is generated only once and cached in {@link selectColumnListSql}.
1244
     * Subclasses may or may not do the same.
1245
     *
1246
     * @return string The SQL fragment.
1247
     */
1248 515
    protected function getSelectColumnsSQL()
1249
    {
1250 515
        if ($this->currentPersisterContext->selectColumnListSql !== null) {
1251 99
            return $this->currentPersisterContext->selectColumnListSql;
1252
        }
1253
1254 515
        $this->currentPersisterContext->rsm->addEntityResult($this->class->getClassName(), 'r'); // r for root
1255 515
        $this->currentPersisterContext->selectJoinSql = '';
1256
1257 515
        $eagerAliasCounter = 0;
1258 515
        $columnList        = [];
1259
1260 515
        foreach ($this->class->getPropertiesIterator() as $fieldName => $property) {
1261
            switch (true) {
1262 515
                case $property instanceof FieldMetadata:
1263 513
                    $columnList[] = $this->getSelectColumnSQL($fieldName, $this->class);
1264 513
                    break;
1265
1266 461
                case $property instanceof AssociationMetadata:
1267 457
                    $assocColumnSQL = $this->getSelectColumnAssociationSQL($fieldName, $property, $this->class);
1268
1269 457
                    if ($assocColumnSQL) {
1270 386
                        $columnList[] = $assocColumnSQL;
1271
                    }
1272
1273 457
                    $isAssocToOneInverseSide = $property instanceof ToOneAssociationMetadata && ! $property->isOwningSide();
1274 457
                    $isAssocFromOneEager     = ! $property instanceof ManyToManyAssociationMetadata && $property->getFetchMode() === FetchMode::EAGER;
1275
1276 457
                    if (! ($isAssocFromOneEager || $isAssocToOneInverseSide)) {
1277 435
                        break;
1278
                    }
1279
1280 178
                    if ($property instanceof ToManyAssociationMetadata && $this->currentPersisterContext->handlesLimits) {
1281 3
                        break;
1282
                    }
1283
1284 175
                    $targetEntity = $property->getTargetEntity();
1285 175
                    $eagerEntity  = $this->em->getClassMetadata($targetEntity);
1286
1287 175
                    if ($eagerEntity->inheritanceType !== InheritanceType::NONE) {
0 ignored issues
show
Bug introduced by
Accessing inheritanceType on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1288 5
                        break; // now this is why you shouldn't use inheritance
1289
                    }
1290
1291 170
                    $assocAlias = 'e' . ($eagerAliasCounter++);
1292
1293 170
                    $this->currentPersisterContext->rsm->addJoinedEntityResult($targetEntity, $assocAlias, 'r', $fieldName);
1294
1295 170
                    foreach ($eagerEntity->getPropertiesIterator() as $eagerProperty) {
0 ignored issues
show
Bug introduced by
The method getPropertiesIterator() does not exist on Doctrine\Persistence\Mapping\ClassMetadata. ( Ignorable by Annotation )

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

1295
                    foreach ($eagerEntity->/** @scrutinizer ignore-call */ getPropertiesIterator() as $eagerProperty) {

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

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

Loading history...
1296
                        switch (true) {
1297 170
                            case $eagerProperty instanceof FieldMetadata:
1298 167
                                $columnList[] = $this->getSelectColumnSQL($eagerProperty->getName(), $eagerEntity, $assocAlias);
1299 167
                                break;
1300
1301 167
                            case $eagerProperty instanceof ToOneAssociationMetadata && $eagerProperty->isOwningSide():
1302 164
                                $columnList[] = $this->getSelectColumnAssociationSQL(
1303 164
                                    $eagerProperty->getName(),
1304
                                    $eagerProperty,
1305
                                    $eagerEntity,
1306
                                    $assocAlias
1307
                                );
1308 164
                                break;
1309
                        }
1310
                    }
1311
1312 170
                    $owningAssociation = $property;
1313 170
                    $joinCondition     = [];
1314
1315 170
                    if ($property instanceof ToManyAssociationMetadata && $property->getIndexedBy()) {
1316 1
                        $this->currentPersisterContext->rsm->addIndexBy($assocAlias, $property->getIndexedBy());
1317
                    }
1318
1319 170
                    if (! $property->isOwningSide()) {
1320 163
                        $owningAssociation = $eagerEntity->getProperty($property->getMappedBy());
1321
                    }
1322
1323 170
                    $joinTableAlias = $this->getSQLTableAlias($eagerEntity->getTableName(), $assocAlias);
1324 170
                    $joinTableName  = $eagerEntity->table->getQuotedQualifiedName($this->platform);
0 ignored issues
show
Bug introduced by
Accessing table on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1325
1326 170
                    $this->currentPersisterContext->selectJoinSql .= ' ' . $this->getJoinSQLForAssociation($property);
1327
1328 170
                    $sourceClass      = $this->em->getClassMetadata($owningAssociation->getSourceEntity());
1329 170
                    $targetClass      = $this->em->getClassMetadata($owningAssociation->getTargetEntity());
1330 170
                    $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName(), $property->isOwningSide() ? $assocAlias : '');
1331 170
                    $sourceTableAlias = $this->getSQLTableAlias($sourceClass->getTableName(), $property->isOwningSide() ? '' : $assocAlias);
1332
1333 170
                    foreach ($owningAssociation->getJoinColumns() as $joinColumn) {
1334 170
                        $joinCondition[] = sprintf(
1335 170
                            '%s.%s = %s.%s',
1336 170
                            $sourceTableAlias,
1337 170
                            $this->platform->quoteIdentifier($joinColumn->getColumnName()),
1338 170
                            $targetTableAlias,
1339 170
                            $this->platform->quoteIdentifier($joinColumn->getReferencedColumnName())
1340
                        );
1341
                    }
1342
1343 170
                    $filterSql = $this->generateFilterConditionSQL($eagerEntity, $targetTableAlias);
1344
1345
                    // Add filter SQL
1346 170
                    if ($filterSql) {
1347
                        $joinCondition[] = $filterSql;
1348
                    }
1349
1350 170
                    $this->currentPersisterContext->selectJoinSql .= ' ' . $joinTableName . ' ' . $joinTableAlias . ' ON ';
1351 170
                    $this->currentPersisterContext->selectJoinSql .= implode(' AND ', $joinCondition);
1352
1353 170
                    break;
1354
            }
1355
        }
1356
1357 515
        $this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList);
1358
1359 515
        return $this->currentPersisterContext->selectColumnListSql;
1360
    }
1361
1362
    /**
1363
     * Gets the SQL join fragment used when selecting entities from an association.
1364
     *
1365
     * @param string $field
1366
     * @param string $alias
1367
     *
1368
     * @return string
1369
     */
1370 457
    protected function getSelectColumnAssociationSQL($field, AssociationMetadata $association, ClassMetadata $class, $alias = 'r')
1371
    {
1372 457
        if (! ($association->isOwningSide() && $association instanceof ToOneAssociationMetadata)) {
1373 370
            return '';
1374
        }
1375
1376 403
        $columnList    = [];
1377 403
        $targetClass   = $this->em->getClassMetadata($association->getTargetEntity());
0 ignored issues
show
Unused Code introduced by
The assignment to $targetClass is dead and can be removed.
Loading history...
1378 403
        $sqlTableAlias = $this->getSQLTableAlias($class->getTableName(), ($alias === 'r' ? '' : $alias));
1379
1380 403
        foreach ($association->getJoinColumns() as $joinColumn) {
1381
            /** @var JoinColumnMetadata $joinColumn */
1382 403
            $columnName       = $joinColumn->getColumnName();
1383 403
            $quotedColumnName = $this->platform->quoteIdentifier($columnName);
1384 403
            $resultColumnName = $this->getSQLColumnAlias();
1385
1386 403
            $this->currentPersisterContext->rsm->addMetaResult(
1387 403
                $alias,
1388
                $resultColumnName,
1389
                $columnName,
1390 403
                $association->isPrimaryKey(),
1391 403
                $joinColumn->getType()
0 ignored issues
show
Bug introduced by
It seems like $joinColumn->getType() can also be of type null; however, parameter $type of Doctrine\ORM\Query\Resul...apping::addMetaResult() does only seem to accept Doctrine\DBAL\Types\Type, maybe add an additional type check? ( Ignorable by Annotation )

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

1391
                /** @scrutinizer ignore-type */ $joinColumn->getType()
Loading history...
1392
            );
1393
1394 403
            $columnList[] = sprintf('%s.%s AS %s', $sqlTableAlias, $quotedColumnName, $resultColumnName);
1395
        }
1396
1397 403
        return implode(', ', $columnList);
1398
    }
1399
1400
    /**
1401
     * Gets the SQL join fragment used when selecting entities from a
1402
     * many-to-many association.
1403
     *
1404
     * @return string
1405
     */
1406 83
    protected function getSelectManyToManyJoinSQL(ManyToManyAssociationMetadata $association)
1407
    {
1408 83
        $conditions        = [];
1409 83
        $owningAssociation = $association;
1410 83
        $sourceTableAlias  = $this->getSQLTableAlias($this->class->getTableName());
1411
1412 83
        if (! $association->isOwningSide()) {
1413 13
            $targetEntity      = $this->em->getClassMetadata($association->getTargetEntity());
1414 13
            $owningAssociation = $targetEntity->getProperty($association->getMappedBy());
1415
        }
1416
1417 83
        $joinTable     = $owningAssociation->getJoinTable();
1418 83
        $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
1419 83
        $joinColumns   = $association->isOwningSide()
1420 75
            ? $joinTable->getInverseJoinColumns()
1421 83
            : $joinTable->getJoinColumns();
1422
1423 83
        foreach ($joinColumns as $joinColumn) {
1424 83
            $conditions[] = sprintf(
1425 83
                '%s.%s = %s.%s',
1426 83
                $sourceTableAlias,
1427 83
                $this->platform->quoteIdentifier($joinColumn->getReferencedColumnName()),
1428 83
                $joinTableName,
1429 83
                $this->platform->quoteIdentifier($joinColumn->getColumnName())
1430
            );
1431
        }
1432
1433 83
        return ' INNER JOIN ' . $joinTableName . ' ON ' . implode(' AND ', $conditions);
1434
    }
1435
1436
    /**
1437
     * {@inheritdoc}
1438
     */
1439 1001
    public function getInsertSQL()
1440
    {
1441 1001
        if ($this->insertSql !== null) {
1442 658
            return $this->insertSql;
1443
        }
1444
1445 1001
        $columns   = $this->getInsertColumnList();
1446 1001
        $tableName = $this->class->table->getQuotedQualifiedName($this->platform);
1447
1448 1001
        if (empty($columns)) {
1449 109
            $property       = $this->class->getProperty($this->class->identifier[0]);
1450 109
            $identityColumn = $this->platform->quoteIdentifier($property->getColumnName());
0 ignored issues
show
Bug introduced by
The method getColumnName() does not exist on Doctrine\ORM\Mapping\Property. It seems like you code against a sub-type of Doctrine\ORM\Mapping\Property such as Doctrine\ORM\Mapping\FieldMetadata. ( Ignorable by Annotation )

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

1450
            $identityColumn = $this->platform->quoteIdentifier($property->/** @scrutinizer ignore-call */ getColumnName());
Loading history...
1451
1452 109
            $this->insertSql = $this->platform->getEmptyIdentityInsertSQL($tableName, $identityColumn);
1453
1454 109
            return $this->insertSql;
1455
        }
1456
1457 977
        $quotedColumns = [];
1458 977
        $values        = [];
1459
1460 977
        foreach ($columns as $columnName) {
1461 977
            $column = $this->columns[$columnName];
1462
1463 977
            $quotedColumns[] = $this->platform->quoteIdentifier($column->getColumnName());
1464 977
            $values[]        = $column->getType()->convertToDatabaseValueSQL('?', $this->platform);
1465
        }
1466
1467 977
        $quotedColumns = implode(', ', $quotedColumns);
1468 977
        $values        = implode(', ', $values);
1469
1470 977
        $this->insertSql = sprintf('INSERT INTO %s (%s) VALUES (%s)', $tableName, $quotedColumns, $values);
1471
1472 977
        return $this->insertSql;
1473
    }
1474
1475
    /**
1476
     * Gets the list of columns to put in the INSERT SQL statement.
1477
     *
1478
     * Subclasses should override this method to alter or change the list of
1479
     * columns placed in the INSERT statements used by the persister.
1480
     *
1481
     * @return string[] The list of columns.
1482
     */
1483 914
    protected function getInsertColumnList()
1484
    {
1485 914
        $columns             = [];
1486 914
        $versionPropertyName = $this->class->isVersioned()
0 ignored issues
show
Unused Code introduced by
The assignment to $versionPropertyName is dead and can be removed.
Loading history...
1487 199
            ? $this->class->versionProperty->getName()
1488 914
            : null;
1489
1490 914
        foreach ($this->class->getPropertiesIterator() as $name => $property) {
1491
            switch (true) {
1492 914
                case $property instanceof FieldMetadata && $property->isVersioned():
1493
                    // Do nothing
1494 199
                    break;
1495
1496 914
                case $property instanceof LocalColumnMetadata:
1497 914
                    if ((! $property->hasValueGenerator() || $property->getValueGenerator()->getType() !== GeneratorType::IDENTITY)
1498 914
                        || $this->class->identifier[0] !== $name
1499
                    ) {
1500 846
                        $columnName = $property->getColumnName();
1501
1502 846
                        $columns[] = $columnName;
1503
1504 846
                        $this->columns[$columnName] = $property;
1505
                    }
1506
1507 914
                    break;
1508
1509 803
                case $property instanceof EmbeddedMetadata:
0 ignored issues
show
Bug introduced by
The type Doctrine\ORM\Persisters\Entity\EmbeddedMetadata was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
1510
                    $targetClass = $this->em->getClassMetadata($property->getTargetEntity());
0 ignored issues
show
Unused Code introduced by
The assignment to $targetClass is dead and can be removed.
Loading history...
1511
1512
                    break;
1513
1514 803
                case $property instanceof AssociationMetadata:
1515 799
                    if ($property->isOwningSide() && $property instanceof ToOneAssociationMetadata) {
1516 757
                        foreach ($property->getJoinColumns() as $joinColumn) {
1517
                            /** @var JoinColumnMetadata $joinColumn */
1518 757
                            $columnName = $joinColumn->getColumnName();
1519
1520 757
                            $columns[] = $columnName;
1521
1522 757
                            $this->columns[$columnName] = $joinColumn;
1523
                        }
1524
                    }
1525
1526 799
                    break;
1527
            }
1528
        }
1529
1530 914
        return $columns;
1531
    }
1532
1533
    /**
1534
     * Gets the SQL snippet of a qualified column name for the given field name.
1535
     *
1536
     * @param string        $field The field name.
1537
     * @param ClassMetadata $class The class that declares this field. The table this class is
1538
     *                             mapped to must own the column for the given field.
1539
     * @param string        $alias
1540
     *
1541
     * @return string
1542
     */
1543 550
    protected function getSelectColumnSQL($field, ClassMetadata $class, $alias = 'r')
1544
    {
1545 550
        $property    = $class->getProperty($field);
1546 550
        $columnAlias = $this->getSQLColumnAlias();
1547 550
        $sql         = sprintf(
1548 550
            '%s.%s',
1549 550
            $this->getSQLTableAlias($property->getTableName(), ($alias === 'r' ? '' : $alias)),
0 ignored issues
show
Bug introduced by
The method getTableName() does not exist on Doctrine\ORM\Mapping\Property. It seems like you code against a sub-type of Doctrine\ORM\Mapping\Property such as Doctrine\ORM\Mapping\FieldMetadata. ( Ignorable by Annotation )

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

1549
            $this->getSQLTableAlias($property->/** @scrutinizer ignore-call */ getTableName(), ($alias === 'r' ? '' : $alias)),
Loading history...
1550 550
            $this->platform->quoteIdentifier($property->getColumnName())
1551
        );
1552
1553 550
        $this->currentPersisterContext->rsm->addFieldResult($alias, $columnAlias, $field, $class->getClassName());
1554
1555 550
        return $property->getType()->convertToPHPValueSQL($sql, $this->platform) . ' AS ' . $columnAlias;
0 ignored issues
show
Bug introduced by
The method getType() does not exist on Doctrine\ORM\Mapping\Property. It seems like you code against a sub-type of Doctrine\ORM\Mapping\Property such as Doctrine\ORM\Mapping\FieldMetadata. ( Ignorable by Annotation )

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

1555
        return $property->/** @scrutinizer ignore-call */ getType()->convertToPHPValueSQL($sql, $this->platform) . ' AS ' . $columnAlias;
Loading history...
1556
    }
1557
1558
    /**
1559
     * Gets the SQL table alias for the given class name.
1560
     *
1561
     * @param string $tableName
1562
     * @param string $assocName
1563
     *
1564
     * @return string The SQL table alias.
1565
     */
1566 584
    protected function getSQLTableAlias($tableName, $assocName = '')
1567
    {
1568 584
        if ($tableName) {
1569 584
            $tableName .= '#' . $assocName;
1570
        }
1571
1572 584
        if (isset($this->currentPersisterContext->sqlTableAliases[$tableName])) {
1573 576
            return $this->currentPersisterContext->sqlTableAliases[$tableName];
1574
        }
1575
1576 584
        $tableAlias = 't' . $this->currentPersisterContext->sqlAliasCounter++;
1577
1578 584
        $this->currentPersisterContext->sqlTableAliases[$tableName] = $tableAlias;
1579
1580 584
        return $tableAlias;
1581
    }
1582
1583
    /**
1584
     * {@inheritdoc}
1585
     */
1586
    public function lock(array $criteria, $lockMode)
1587
    {
1588
        $lockSql      = '';
1589
        $conditionSql = $this->getSelectConditionSQL($criteria);
1590
1591
        switch ($lockMode) {
1592
            case LockMode::PESSIMISTIC_READ:
1593
                $lockSql = $this->platform->getReadLockSQL();
1594
1595
                break;
1596
            case LockMode::PESSIMISTIC_WRITE:
1597
                $lockSql = $this->platform->getWriteLockSQL();
1598
                break;
1599
        }
1600
1601
        $lock  = $this->getLockTablesSql($lockMode);
1602
        $where = ($conditionSql ? ' WHERE ' . $conditionSql : '') . ' ';
1603
        $sql   = 'SELECT 1 '
1604
             . $lock
1605
             . $where
1606
             . $lockSql;
1607
1608
        [$params, $types] = $this->expandParameters($criteria);
1609
1610
        $this->conn->executeQuery($sql, $params, $types);
1611
    }
1612
1613
    /**
1614
     * Gets the FROM and optionally JOIN conditions to lock the entity managed by this persister.
1615
     *
1616
     * @param int $lockMode One of the Doctrine\DBAL\LockMode::* constants.
1617
     *
1618
     * @return string
1619
     */
1620 13
    protected function getLockTablesSql($lockMode)
1621
    {
1622 13
        $tableName = $this->class->table->getQuotedQualifiedName($this->platform);
1623
1624 13
        return $this->platform->appendLockHint(
1625 13
            'FROM ' . $tableName . ' ' . $this->getSQLTableAlias($this->class->getTableName()),
1626
            $lockMode
1627
        );
1628
    }
1629
1630
    /**
1631
     * Gets the Select Where Condition from a Criteria object.
1632
     *
1633
     * @return string
1634
     */
1635 39
    protected function getSelectConditionCriteriaSQL(Criteria $criteria)
1636
    {
1637 39
        $expression = $criteria->getWhereExpression();
1638
1639 39
        if ($expression === null) {
1640 2
            return '';
1641
        }
1642
1643 38
        $visitor = new SqlExpressionVisitor($this, $this->class);
1644
1645 38
        return $visitor->dispatch($expression);
1646
    }
1647
1648
    /**
1649
     * {@inheritdoc}
1650
     */
1651 564
    public function getSelectConditionStatementSQL(
1652
        $field,
1653
        $value,
1654
        ?AssociationMetadata $association = null,
1655
        $comparison = null
1656
    ) {
1657 564
        $selectedColumns = [];
1658 564
        $columns         = $this->getSelectConditionStatementColumnSQL($field, $association);
1659
1660 560
        if (in_array($comparison, [Comparison::IN, Comparison::NIN], true) && isset($columns[1])) {
1661
            // @todo try to support multi-column IN expressions. Example: (col1, col2) IN (('val1A', 'val2A'), ...)
1662 1
            throw CantUseInOperatorOnCompositeKeys::create();
1663
        }
1664
1665 559
        foreach ($columns as $column) {
1666 559
            $property    = $this->class->getProperty($field);
1667 559
            $placeholder = '?';
1668
1669 559
            if ($property instanceof FieldMetadata) {
1670 475
                $placeholder = $property->getType()->convertToDatabaseValueSQL($placeholder, $this->platform);
1671
            }
1672
1673 559
            if ($comparison !== null) {
1674
                // special case null value handling
1675 42
                if (($comparison === Comparison::EQ || $comparison === Comparison::IS) && $value ===null) {
1676 6
                    $selectedColumns[] = $column . ' IS NULL';
1677
1678 6
                    continue;
1679
                }
1680
1681 36
                if ($comparison === Comparison::NEQ && $value === null) {
1682 3
                    $selectedColumns[] = $column . ' IS NOT NULL';
1683
1684 3
                    continue;
1685
                }
1686
1687 33
                $selectedColumns[] = $column . ' ' . sprintf(self::$comparisonMap[$comparison], $placeholder);
1688
1689 33
                continue;
1690
            }
1691
1692 532
            if (is_array($value)) {
1693 14
                $in = sprintf('%s IN (%s)', $column, $placeholder);
1694
1695 14
                if (in_array(null, $value, true)) {
1696 4
                    $selectedColumns[] = sprintf('(%s OR %s IS NULL)', $in, $column);
1697
1698 4
                    continue;
1699
                }
1700
1701 10
                $selectedColumns[] = $in;
1702
1703 10
                continue;
1704
            }
1705
1706 521
            if ($value === null) {
1707 9
                $selectedColumns[] = sprintf('%s IS NULL', $column);
1708
1709 9
                continue;
1710
            }
1711
1712 513
            $selectedColumns[] = sprintf('%s = %s', $column, $placeholder);
1713
        }
1714
1715 559
        return implode(' AND ', $selectedColumns);
1716
    }
1717
1718
    /**
1719
     * Builds the left-hand-side of a where condition statement.
1720
     *
1721
     * @param string $field
1722
     *
1723
     * @return string[]
1724
     *
1725
     * @throws ORMException
1726
     */
1727 564
    private function getSelectConditionStatementColumnSQL($field, ?AssociationMetadata $association = null)
1728
    {
1729 564
        $property = $this->class->getProperty($field);
1730
1731 564
        if ($property instanceof FieldMetadata) {
1732 475
            $tableAlias = $this->getSQLTableAlias($property->getTableName());
1733 475
            $columnName = $this->platform->quoteIdentifier($property->getColumnName());
1734
1735 475
            return [$tableAlias . '.' . $columnName];
1736
        }
1737
1738 278
        if ($property instanceof AssociationMetadata) {
1739 144
            $owningAssociation = $property;
1740 144
            $columns           = [];
1741
1742
            // Many-To-Many requires join table check for joinColumn
1743 144
            if ($owningAssociation instanceof ManyToManyAssociationMetadata) {
1744 3
                if (! $owningAssociation->isOwningSide()) {
1745 2
                    $owningAssociation = $association;
1746
                }
1747
1748 3
                $joinTable     = $owningAssociation->getJoinTable();
0 ignored issues
show
Bug introduced by
The method getJoinTable() does not exist on Doctrine\ORM\Mapping\AssociationMetadata. It seems like you code against a sub-type of Doctrine\ORM\Mapping\AssociationMetadata such as Doctrine\ORM\Mapping\ManyToManyAssociationMetadata. ( Ignorable by Annotation )

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

1748
                /** @scrutinizer ignore-call */ 
1749
                $joinTable     = $owningAssociation->getJoinTable();
Loading history...
Bug introduced by
The method getJoinTable() does not exist on null. ( Ignorable by Annotation )

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

1748
                /** @scrutinizer ignore-call */ 
1749
                $joinTable     = $owningAssociation->getJoinTable();

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

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

Loading history...
1749 3
                $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
1750 3
                $joinColumns   = $association->isOwningSide()
0 ignored issues
show
Bug introduced by
The method isOwningSide() does not exist on null. ( Ignorable by Annotation )

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

1750
                $joinColumns   = $association->/** @scrutinizer ignore-call */ isOwningSide()

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

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

Loading history...
1751 2
                    ? $joinTable->getJoinColumns()
1752 3
                    : $joinTable->getInverseJoinColumns();
1753
1754 3
                foreach ($joinColumns as $joinColumn) {
1755 3
                    $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName());
1756
1757 3
                    $columns[] = $joinTableName . '.' . $quotedColumnName;
1758
                }
1759
            } else {
1760 142
                if (! $owningAssociation->isOwningSide()) {
1761 1
                    throw InvalidFindByCall::fromInverseSideUsage(
1762 1
                        $this->class->getClassName(),
1763
                        $field
1764
                    );
1765
                }
1766
1767 141
                $class      = $this->class->isInheritedProperty($field)
1768 11
                    ? $owningAssociation->getDeclaringClass()
1769 141
                    : $this->class;
1770 141
                $tableAlias = $this->getSQLTableAlias($class->getTableName());
1771
1772 141
                foreach ($owningAssociation->getJoinColumns() as $joinColumn) {
1773 141
                    $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName());
1774
1775 141
                    $columns[] = $tableAlias . '.' . $quotedColumnName;
1776
                }
1777
            }
1778
1779 143
            return $columns;
1780
        }
1781
1782
        // very careless developers could potentially open up this normally hidden api for userland attacks,
1783
        // therefore checking for spaces and function calls which are not allowed.
1784
        // found a join column condition, not really a "field"
1785 155
        if ($association !== null && strpos($field, ' ') === false && strpos($field, '(') === false) {
1786 152
            return [$field];
1787
        }
1788
1789 3
        throw UnrecognizedField::byName($field);
1790
    }
1791
1792
    /**
1793
     * Gets the conditional SQL fragment used in the WHERE clause when selecting
1794
     * entities in this persister.
1795
     *
1796
     * Subclasses are supposed to override this method if they intend to change
1797
     * or alter the criteria by which entities are selected.
1798
     *
1799
     * @param mixed[] $criteria
1800
     *
1801
     * @return string
1802
     */
1803 557
    protected function getSelectConditionSQL(array $criteria, ?AssociationMetadata $association = null)
1804
    {
1805 557
        $conditions = [];
1806
1807 557
        foreach ($criteria as $field => $value) {
1808 534
            $conditions[] = $this->getSelectConditionStatementSQL($field, $value, $association);
1809
        }
1810
1811 554
        return implode(' AND ', $conditions);
1812
    }
1813
1814
    /**
1815
     * {@inheritdoc}
1816
     */
1817 5
    public function getOneToManyCollection(
1818
        OneToManyAssociationMetadata $association,
1819
        $sourceEntity,
1820
        $offset = null,
1821
        $limit = null
1822
    ) {
1823 5
        $this->switchPersisterContext($offset, $limit);
1824
1825 5
        $stmt = $this->getOneToManyStatement($association, $sourceEntity, $offset, $limit);
1826
1827 5
        return $this->loadArrayFromStatement($association, $stmt);
1828
    }
1829
1830
    /**
1831
     * {@inheritdoc}
1832
     */
1833 73
    public function loadOneToManyCollection(
1834
        OneToManyAssociationMetadata $association,
1835
        $sourceEntity,
1836
        PersistentCollection $collection
1837
    ) {
1838 73
        $stmt = $this->getOneToManyStatement($association, $sourceEntity);
1839
1840 73
        return $this->loadCollectionFromStatement($association, $stmt, $collection);
1841
    }
1842
1843
    /**
1844
     * Builds criteria and execute SQL statement to fetch the one to many entities from.
1845
     *
1846
     * @param object   $sourceEntity
1847
     * @param int|null $offset
1848
     * @param int|null $limit
1849
     *
1850
     * @return Statement
1851
     */
1852 78
    private function getOneToManyStatement(
1853
        OneToManyAssociationMetadata $association,
1854
        $sourceEntity,
1855
        $offset = null,
1856
        $limit = null
1857
    ) {
1858 78
        $this->switchPersisterContext($offset, $limit);
1859
1860 78
        $criteria    = [];
1861 78
        $parameters  = [];
1862 78
        $owningAssoc = $this->class->getProperty($association->getMappedBy());
0 ignored issues
show
Bug introduced by
It seems like $association->getMappedBy() can also be of type null; however, parameter $propertyName of Doctrine\ORM\Mapping\Com...Metadata::getProperty() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1862
        $owningAssoc = $this->class->getProperty(/** @scrutinizer ignore-type */ $association->getMappedBy());
Loading history...
1863 78
        $sourceClass = $this->em->getClassMetadata($association->getSourceEntity());
1864 78
        $class       = $owningAssoc->getDeclaringClass();
1865 78
        $tableAlias  = $this->getSQLTableAlias($class->getTableName());
0 ignored issues
show
Bug introduced by
The method getTableName() does not exist on Doctrine\ORM\Mapping\ComponentMetadata. It seems like you code against a sub-type of Doctrine\ORM\Mapping\ComponentMetadata such as Doctrine\ORM\Mapping\ClassMetadata. ( Ignorable by Annotation )

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

1865
        $tableAlias  = $this->getSQLTableAlias($class->/** @scrutinizer ignore-call */ getTableName());
Loading history...
1866
1867 78
        foreach ($owningAssoc->getJoinColumns() as $joinColumn) {
1868 78
            $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName());
1869 78
            $fieldName        = $sourceClass->fieldNames[$joinColumn->getReferencedColumnName()];
0 ignored issues
show
Bug introduced by
Accessing fieldNames on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1870 78
            $property         = $sourceClass->getProperty($fieldName);
1871 78
            $value            = null;
1872
1873 78
            if ($property instanceof FieldMetadata) {
1874 77
                $value = $property->getValue($sourceEntity);
1875 4
            } elseif ($property instanceof AssociationMetadata) {
1876 4
                $targetClass = $this->em->getClassMetadata($property->getTargetEntity());
1877 4
                $value       = $property->getValue($sourceEntity);
1878
1879 4
                $value = $this->em->getUnitOfWork()->getEntityIdentifier($value);
1880 4
                $value = $value[$targetClass->identifier[0]];
0 ignored issues
show
Bug introduced by
Accessing identifier on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1881
            }
1882
1883 78
            $criteria[$tableAlias . '.' . $quotedColumnName] = $value;
1884 78
            $parameters[]                                    = [
1885 78
                'value' => $value,
1886 78
                'field' => $fieldName,
1887 78
                'class' => $sourceClass,
1888
            ];
1889
        }
1890
1891 78
        $sql              = $this->getSelectSQL($criteria, $association, null, $limit, $offset);
1892 78
        [$params, $types] = $this->expandToManyParameters($parameters);
1893
1894 78
        return $this->conn->executeQuery($sql, $params, $types);
1895
    }
1896
1897
    /**
1898
     * {@inheritdoc}
1899
     */
1900 534
    public function expandParameters($criteria)
1901
    {
1902 534
        $params = [];
1903 534
        $types  = [];
1904
1905 534
        foreach ($criteria as $field => $value) {
1906 511
            if ($value === null) {
1907 3
                continue; // skip null values.
1908
            }
1909
1910 509
            $types  = array_merge($types, $this->getTypes($field, $value, $this->class));
1911 509
            $params = array_merge($params, $this->getValues($value));
1912
        }
1913
1914 534
        return [$params, $types];
1915
    }
1916
1917
    /**
1918
     * Expands the parameters from the given criteria and use the correct binding types if found,
1919
     * specialized for OneToMany or ManyToMany associations.
1920
     *
1921
     * @param mixed[][] $criteria an array of arrays containing following:
1922
     *                             - field to which each criterion will be bound
1923
     *                             - value to be bound
1924
     *                             - class to which the field belongs to
1925
     *
1926
     * @return mixed[][]
1927
     */
1928 148
    private function expandToManyParameters($criteria)
1929
    {
1930 148
        $params = [];
1931 148
        $types  = [];
1932
1933 148
        foreach ($criteria as $criterion) {
1934 148
            if ($criterion['value'] === null) {
1935 6
                continue; // skip null values.
1936
            }
1937
1938 142
            $types  = array_merge($types, $this->getTypes($criterion['field'], $criterion['value'], $criterion['class']));
1939 142
            $params = array_merge($params, $this->getValues($criterion['value']));
1940
        }
1941
1942 148
        return [$params, $types];
1943
    }
1944
1945
    /**
1946
     * Infers field types to be used by parameter type casting.
1947
     *
1948
     * @param string $field
1949
     * @param mixed  $value
1950
     *
1951
     * @return mixed[]
1952
     *
1953
     * @throws QueryException
1954
     */
1955 668
    private function getTypes($field, $value, ClassMetadata $class)
1956
    {
1957 668
        $property = $class->getProperty($field);
1958 668
        $types    = [];
1959
1960
        switch (true) {
1961 668
            case $property instanceof FieldMetadata:
1962 615
                $types = array_merge($types, [$property->getType()]);
1963 615
                break;
1964
1965 143
            case $property instanceof AssociationMetadata:
1966 142
                $class = $this->em->getClassMetadata($property->getTargetEntity());
1967
1968 142
                if (! $property->isOwningSide()) {
1969 2
                    $property = $class->getProperty($property->getMappedBy());
1970
                }
1971
1972 142
                $joinColumns = $property instanceof ManyToManyAssociationMetadata
1973 3
                    ? $property->getJoinTable()->getInverseJoinColumns()
1974 142
                    : $property->getJoinColumns();
1975
1976 142
                foreach ($joinColumns as $joinColumn) {
1977 142
                    $types[] = $joinColumn->getType();
1978
                }
1979
1980 142
                break;
1981
1982
            default:
1983 1
                $types[] = null;
1984 1
                break;
1985
        }
1986
1987 668
        if (is_array($value)) {
1988
            return array_map(static function ($type) {
1989 16
                return $type->getBindingType() + Connection::ARRAY_PARAM_OFFSET;
1990 16
            }, $types);
1991
        }
1992
1993 658
        return $types;
1994
    }
1995
1996
    /**
1997
     * Retrieves the parameters that identifies a value.
1998
     *
1999
     * @param mixed $value
2000
     *
2001
     * @return mixed[]
2002
     */
2003 541
    private function getValues($value)
2004
    {
2005 541
        if (is_array($value)) {
2006 16
            $newValue = [];
2007
2008 16
            foreach ($value as $itemValue) {
2009 16
                $newValue = array_merge($newValue, $this->getValues($itemValue));
2010
            }
2011
2012 16
            return [$newValue];
2013
        }
2014
2015 541
        $metadataFactory = $this->em->getMetadataFactory();
2016 541
        $unitOfWork      = $this->em->getUnitOfWork();
2017
2018 541
        if (is_object($value) && $metadataFactory->hasMetadataFor(StaticClassNameConverter::getClass($value))) {
2019 45
            $class     = $metadataFactory->getMetadataFor(get_class($value));
2020 45
            $persister = $unitOfWork->getEntityPersister($class->getClassName());
2021
2022 45
            if ($class->isIdentifierComposite()) {
0 ignored issues
show
Bug introduced by
The method isIdentifierComposite() does not exist on Doctrine\Persistence\Mapping\ClassMetadata. Did you maybe mean isIdentifier()? ( Ignorable by Annotation )

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

2022
            if ($class->/** @scrutinizer ignore-call */ isIdentifierComposite()) {

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

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

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