BasicEntityPersister   F
last analyzed

Complexity

Total Complexity 273

Size/Duplication

Total Lines 2072
Duplicated Lines 0 %

Test Coverage

Coverage 94.97%

Importance

Changes 0
Metric Value
eloc 872
dl 0
loc 2072
ccs 849
cts 894
cp 0.9497
rs 1.728
c 0
b 0
f 0
wmc 273

58 Methods

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

How to fix   Complexity   

Complex Class

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

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

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

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

280
        $versionValue    = $this->fetchVersionValue(/** @scrutinizer ignore-type */ $versionProperty, $id);
Loading history...
281
282 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

282
        $versionProperty->/** @scrutinizer ignore-call */ 
283
                          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...
283 208
    }
284
285
    /**
286
     * Fetches the current version value of a versioned entity.
287
     *
288
     * @param mixed[] $id
289
     *
290
     * @return mixed
291
     */
292 208
    protected function fetchVersionValue(FieldMetadata $versionProperty, array $id)
293
    {
294 208
        $versionedClass = $versionProperty->getDeclaringClass();
295 208
        $tableName      = $versionedClass->table->getQuotedQualifiedName($this->platform);
296 208
        $columnName     = $this->platform->quoteIdentifier($versionProperty->getColumnName());
297 208
        $identifier     = array_map(
298
            function ($columnName) {
299 208
                return $this->platform->quoteIdentifier($columnName);
300 208
            },
301 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

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

393
            $targetClass = $this->em->getClassMetadata($property->/** @scrutinizer ignore-call */ getTargetEntity());
Loading history...
394 5
            $joinColumns = $property instanceof ManyToManyAssociationMetadata
395
                ? $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

395
                ? $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...
396 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

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

476
                        $value                = $targetPersister->getColumnValue($identifier[$idField], /** @scrutinizer ignore-type */ $referencedColumnName);
Loading history...
477
478 6
                        $where[]  = $quotedColumnName;
479 6
                        $params[] = $value;
480 6
                        $types[]  = $joinColumn->getType();
481
                    }
482 6
                    break;
483
            }
484
        }
485
486 102
        if ($versioned) {
487 20
            $versionProperty   = $this->class->versionProperty;
488 20
            $versionColumnType = $versionProperty->getType();
489 20
            $versionColumnName = $this->platform->quoteIdentifier($versionProperty->getColumnName());
490
491 20
            $where[]  = $versionColumnName;
492 20
            $types[]  = $versionColumnType;
493 20
            $params[] = $versionProperty->getValue($entity);
494
495 20
            switch ($versionColumnType->getName()) {
496
                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

496
                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...
497
                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

497
                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...
498
                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

498
                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...
499 18
                    $set[] = $versionColumnName . ' = ' . $versionColumnName . ' + 1';
500 18
                    break;
501
502
                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

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

541
                /** @scrutinizer ignore-call */ 
542
                $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...
542
            }
543
544 23
            $joinTable     = $owningAssociation->getJoinTable();
545 23
            $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
546 23
            $joinColumns   = $association->isOwningSide()
547 19
                ? $joinTable->getJoinColumns()
548 23
                : $joinTable->getInverseJoinColumns();
549
550 23
            if ($selfReferential) {
551 1
                $otherColumns = ! $association->isOwningSide()
552
                    ? $joinTable->getJoinColumns()
553 1
                    : $joinTable->getInverseJoinColumns();
554
            }
555
556 23
            $isOnDeleteCascade = false;
557
558 23
            foreach ($joinColumns as $joinColumn) {
559 23
                $keys[] = $this->platform->quoteIdentifier($joinColumn->getColumnName());
560
561 23
                if ($joinColumn->isOnDeleteCascade()) {
562 23
                    $isOnDeleteCascade = true;
563
                }
564
            }
565
566 23
            foreach ($otherColumns as $joinColumn) {
567 1
                $otherKeys[] = $this->platform->quoteIdentifier($joinColumn->getColumnName());
568
569 1
                if ($joinColumn->isOnDeleteCascade()) {
570 1
                    $isOnDeleteCascade = true;
571
                }
572
            }
573
574 23
            if ($isOnDeleteCascade) {
575 23
                continue;
576
            }
577
578
            $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

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

663
            $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...
664
665 829
            foreach ($property->getJoinColumns() as $joinColumn) {
666
                /** @var JoinColumnMetadata $joinColumn */
667 829
                $referencedColumnName = $joinColumn->getReferencedColumnName();
668
669
                // @todo guilhermeblanco Please remove this in the future for good...
670 829
                $this->columns[$joinColumn->getColumnName()] = $joinColumn;
671
672 829
                $result[$joinColumn->getTableName()][$joinColumn->getColumnName()] = $newValue !== null
673 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

673
                    ? $targetPersister->getColumnValue($newValue, /** @scrutinizer ignore-type */ $referencedColumnName)
Loading history...
674 570
                    : null;
675
            }
676
        }
677
678 1005
        return $result;
679
    }
680
681
    /**
682
     * @param object $entity
683
     *
684
     * @return mixed|null
685
     */
686 611
    public function getColumnValue($entity, string $columnName)
687
    {
688
        // Looking for fields by column is the easiest way to look at local columns or x-1 owning side associations
689 611
        $propertyName = $this->class->fieldNames[$columnName];
690 611
        $property     = $this->class->getProperty($propertyName);
691
692 611
        if (! $property) {
693
            return null;
694
        }
695
696 611
        $propertyValue = $property->getValue($entity);
697
698 611
        if ($property instanceof LocalColumnMetadata) {
699 611
            return $propertyValue;
700
        }
701
702
        /** @var ToOneAssociationMetadata $property */
703 20
        $unitOfWork      = $this->em->getUnitOfWork();
704 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...
705 20
        $targetPersister = $unitOfWork->getEntityPersister($property->getTargetEntity());
706
707 20
        foreach ($property->getJoinColumns() as $joinColumn) {
708
            /** @var JoinColumnMetadata $joinColumn */
709 20
            $referencedColumnName = $joinColumn->getReferencedColumnName();
710
711 20
            if ($joinColumn->getColumnName() !== $columnName) {
712
                continue;
713
            }
714
715 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

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

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

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

1296
                    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...
1297
                        switch (true) {
1298 170
                            case $eagerProperty instanceof FieldMetadata:
1299 167
                                $columnList[] = $this->getSelectColumnSQL($eagerProperty->getName(), $eagerEntity, $assocAlias);
1300 167
                                break;
1301
1302 167
                            case $eagerProperty instanceof ToOneAssociationMetadata && $eagerProperty->isOwningSide():
1303 164
                                $columnList[] = $this->getSelectColumnAssociationSQL(
1304 164
                                    $eagerProperty->getName(),
1305
                                    $eagerProperty,
1306
                                    $eagerEntity,
1307
                                    $assocAlias
1308
                                );
1309 164
                                break;
1310
                        }
1311
                    }
1312
1313 170
                    $owningAssociation = $property;
1314 170
                    $joinCondition     = [];
1315
1316 170
                    if ($property instanceof ToManyAssociationMetadata && $property->getIndexedBy()) {
1317 1
                        $this->currentPersisterContext->rsm->addIndexBy($assocAlias, $property->getIndexedBy());
1318
                    }
1319
1320 170
                    if (! $property->isOwningSide()) {
1321 163
                        $owningAssociation = $eagerEntity->getProperty($property->getMappedBy());
1322
                    }
1323
1324 170
                    $joinTableAlias = $this->getSQLTableAlias($eagerEntity->getTableName(), $assocAlias);
1325 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...
1326
1327 170
                    $this->currentPersisterContext->selectJoinSql .= ' ' . $this->getJoinSQLForAssociation($property);
1328
1329 170
                    $sourceClass      = $this->em->getClassMetadata($owningAssociation->getSourceEntity());
1330 170
                    $targetClass      = $this->em->getClassMetadata($owningAssociation->getTargetEntity());
1331 170
                    $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName(), $property->isOwningSide() ? $assocAlias : '');
1332 170
                    $sourceTableAlias = $this->getSQLTableAlias($sourceClass->getTableName(), $property->isOwningSide() ? '' : $assocAlias);
1333
1334 170
                    foreach ($owningAssociation->getJoinColumns() as $joinColumn) {
1335 170
                        $joinCondition[] = sprintf(
1336 170
                            '%s.%s = %s.%s',
1337 170
                            $sourceTableAlias,
1338 170
                            $this->platform->quoteIdentifier($joinColumn->getColumnName()),
1339 170
                            $targetTableAlias,
1340 170
                            $this->platform->quoteIdentifier($joinColumn->getReferencedColumnName())
1341
                        );
1342
                    }
1343
1344 170
                    $filterSql = $this->generateFilterConditionSQL($eagerEntity, $targetTableAlias);
1345
1346
                    // Add filter SQL
1347 170
                    if ($filterSql) {
1348
                        $joinCondition[] = $filterSql;
1349
                    }
1350
1351 170
                    $this->currentPersisterContext->selectJoinSql .= ' ' . $joinTableName . ' ' . $joinTableAlias . ' ON ';
1352 170
                    $this->currentPersisterContext->selectJoinSql .= implode(' AND ', $joinCondition);
1353
1354 170
                    break;
1355
            }
1356
        }
1357
1358 515
        $this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList);
1359
1360 515
        return $this->currentPersisterContext->selectColumnListSql;
1361
    }
1362
1363
    /**
1364
     * Gets the SQL join fragment used when selecting entities from an association.
1365
     *
1366
     * @param string $field
1367
     * @param string $alias
1368
     *
1369
     * @return string
1370
     */
1371 457
    protected function getSelectColumnAssociationSQL($field, AssociationMetadata $association, ClassMetadata $class, $alias = 'r')
1372
    {
1373 457
        if (! ($association->isOwningSide() && $association instanceof ToOneAssociationMetadata)) {
1374 370
            return '';
1375
        }
1376
1377 403
        $columnList    = [];
1378 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...
1379 403
        $sqlTableAlias = $this->getSQLTableAlias($class->getTableName(), ($alias === 'r' ? '' : $alias));
1380
1381 403
        foreach ($association->getJoinColumns() as $joinColumn) {
1382
            /** @var JoinColumnMetadata $joinColumn */
1383 403
            $columnName       = $joinColumn->getColumnName();
1384 403
            $quotedColumnName = $this->platform->quoteIdentifier($columnName);
1385 403
            $resultColumnName = $this->getSQLColumnAlias();
1386
1387 403
            $this->currentPersisterContext->rsm->addMetaResult(
1388 403
                $alias,
1389
                $resultColumnName,
1390
                $columnName,
1391 403
                $association->isPrimaryKey(),
1392 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

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

1451
            $identityColumn = $this->platform->quoteIdentifier($property->/** @scrutinizer ignore-call */ getColumnName());
Loading history...
1452
1453 109
            $this->insertSql = $this->platform->getEmptyIdentityInsertSQL($tableName, $identityColumn);
1454
1455 109
            return $this->insertSql;
1456
        }
1457
1458 977
        $quotedColumns = [];
1459 977
        $values        = [];
1460
1461 977
        foreach ($columns as $columnName) {
1462 977
            $column = $this->columns[$columnName];
1463
1464 977
            $quotedColumns[] = $this->platform->quoteIdentifier($column->getColumnName());
1465 977
            $values[]        = $column->getType()->convertToDatabaseValueSQL('?', $this->platform);
1466
        }
1467
1468 977
        $quotedColumns = implode(', ', $quotedColumns);
1469 977
        $values        = implode(', ', $values);
1470
1471 977
        $this->insertSql = sprintf('INSERT INTO %s (%s) VALUES (%s)', $tableName, $quotedColumns, $values);
1472
1473 977
        return $this->insertSql;
1474
    }
1475
1476
    /**
1477
     * Gets the list of columns to put in the INSERT SQL statement.
1478
     *
1479
     * Subclasses should override this method to alter or change the list of
1480
     * columns placed in the INSERT statements used by the persister.
1481
     *
1482
     * @return string[] The list of columns.
1483
     */
1484 914
    protected function getInsertColumnList()
1485
    {
1486 914
        $columns             = [];
1487 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...
1488 199
            ? $this->class->versionProperty->getName()
1489 914
            : null;
1490
1491 914
        foreach ($this->class->getPropertiesIterator() as $name => $property) {
1492
            switch (true) {
1493 914
                case $property instanceof FieldMetadata && $property->isVersioned():
1494
                    // Do nothing
1495 199
                    break;
1496
1497 914
                case $property instanceof LocalColumnMetadata:
1498 914
                    if ((! $property->hasValueGenerator() || $property->getValueGenerator()->getType() !== GeneratorType::IDENTITY)
1499 914
                        || $this->class->identifier[0] !== $name
1500
                    ) {
1501 846
                        $columnName = $property->getColumnName();
1502
1503 846
                        $columns[] = $columnName;
1504
1505 846
                        $this->columns[$columnName] = $property;
1506
                    }
1507
1508 914
                    break;
1509
1510 803
                case $property instanceof EmbeddedMetadata:
1511
                    $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...
1512
1513
                    break;
1514
1515 803
                case $property instanceof AssociationMetadata:
1516 799
                    if ($property->isOwningSide() && $property instanceof ToOneAssociationMetadata) {
1517 757
                        foreach ($property->getJoinColumns() as $joinColumn) {
1518
                            /** @var JoinColumnMetadata $joinColumn */
1519 757
                            $columnName = $joinColumn->getColumnName();
1520
1521 757
                            $columns[] = $columnName;
1522
1523 757
                            $this->columns[$columnName] = $joinColumn;
1524
                        }
1525
                    }
1526
1527 799
                    break;
1528
            }
1529
        }
1530
1531 914
        return $columns;
1532
    }
1533
1534
    /**
1535
     * Gets the SQL snippet of a qualified column name for the given field name.
1536
     *
1537
     * @param string        $field The field name.
1538
     * @param ClassMetadata $class The class that declares this field. The table this class is
1539
     *                             mapped to must own the column for the given field.
1540
     * @param string        $alias
1541
     *
1542
     * @return string
1543
     */
1544 550
    protected function getSelectColumnSQL($field, ClassMetadata $class, $alias = 'r')
1545
    {
1546 550
        $property    = $class->getProperty($field);
1547 550
        $columnAlias = $this->getSQLColumnAlias();
1548 550
        $sql         = sprintf(
1549 550
            '%s.%s',
1550 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

1550
            $this->getSQLTableAlias($property->/** @scrutinizer ignore-call */ getTableName(), ($alias === 'r' ? '' : $alias)),
Loading history...
1551 550
            $this->platform->quoteIdentifier($property->getColumnName())
1552
        );
1553
1554 550
        $this->currentPersisterContext->rsm->addFieldResult($alias, $columnAlias, $field, $class->getClassName());
1555
1556 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

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

1749
                /** @scrutinizer ignore-call */ 
1750
                $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...
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

1749
                /** @scrutinizer ignore-call */ 
1750
                $joinTable     = $owningAssociation->getJoinTable();
Loading history...
1750 3
                $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
1751 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

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

1863
        $owningAssoc = $this->class->getProperty(/** @scrutinizer ignore-type */ $association->getMappedBy());
Loading history...
1864 78
        $sourceClass = $this->em->getClassMetadata($association->getSourceEntity());
1865 78
        $class       = $owningAssoc->getDeclaringClass();
1866 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

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

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