Failed Conditions
Push — master ( 8be1e3...e3936d )
by Marco
14s
created

BasicEntityPersister::update()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 4.0058

Importance

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

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

270
        $versionProperty->/** @scrutinizer ignore-call */ 
271
                          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...
271 205
    }
272
273
    /**
274
     * Fetches the current version value of a versioned entity.
275
     *
276
     * @param mixed[] $id
277
     *
278
     * @return mixed
279
     */
280 205
    protected function fetchVersionValue(VersionFieldMetadata $versionProperty, array $id)
281
    {
282 205
        $versionedClass = $versionProperty->getDeclaringClass();
283 205
        $tableName      = $versionedClass->table->getQuotedQualifiedName($this->platform);
284 205
        $columnName     = $this->platform->quoteIdentifier($versionProperty->getColumnName());
285 205
        $identifier     = array_map(
286 205
            function ($columnName) {
287 205
                return $this->platform->quoteIdentifier($columnName);
288 205
            },
289 205
            array_keys($versionedClass->getIdentifierColumns($this->em))
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

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

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

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

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

384
                ? $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...
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

384
                ? $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...
385 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

385
                : $property->/** @scrutinizer ignore-call */ getJoinColumns()
Loading history...
386
            ;
387
388 5
            $associationValue = null;
389 5
            $value            = $identifier[$field];
390
391 5
            if ($value !== null) {
392
                // @todo guilhermeblanco Make sure we do not have flat association values.
393 5
                if (! is_array($value)) {
394 5
                    $value = [$targetClass->identifier[0] => $value];
0 ignored issues
show
Bug introduced by
Accessing identifier on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
395
                }
396
397 5
                $associationValue = $value;
398
            }
399
400 5
            foreach ($joinColumns as $joinColumn) {
401
                /** @var JoinColumnMetadata $joinColumn */
402 5
                $quotedColumnName     = $this->platform->quoteIdentifier($joinColumn->getColumnName());
403 5
                $referencedColumnName = $joinColumn->getReferencedColumnName();
404 5
                $targetField          = $targetClass->fieldNames[$referencedColumnName];
0 ignored issues
show
Bug introduced by
Accessing fieldNames on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
405
406 5
                if (! $joinColumn->getType()) {
407
                    $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
0 ignored issues
show
Bug introduced by
$targetClass of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Utility\Per...lper::getTypeOfColumn(). ( Ignorable by Annotation )

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

407
                    $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, /** @scrutinizer ignore-type */ $targetClass, $this->em));
Loading history...
408
                }
409
410 5
                $id[$quotedColumnName] = $associationValue ? $associationValue[$targetField] : null;
411 5
                $types[]               = $joinColumn->getType();
412
            }
413
        }
414
415 58
        $this->deleteJoinTableRecords($identifier);
416
417 58
        return (bool) $this->conn->delete($tableName, $id, $types);
418
    }
419
420
    /**
421
     * Performs an UPDATE statement for an entity on a specific table.
422
     * The UPDATE can optionally be versioned, which requires the entity to have a version field.
423
     *
424
     * @param object  $entity          The entity object being updated.
425
     * @param string  $quotedTableName The quoted name of the table to apply the UPDATE on.
426
     * @param mixed[] $updateData      The map of columns to update (column => value).
427
     * @param bool    $versioned       Whether the UPDATE should be versioned.
428
     *
429
     * @throws \Doctrine\ORM\ORMException
430
     * @throws \Doctrine\ORM\OptimisticLockException
431
     */
432 102
    final protected function updateTable($entity, $quotedTableName, array $updateData, $versioned = false)
433
    {
434 102
        $set    = [];
435 102
        $types  = [];
436 102
        $params = [];
437
438 102
        foreach ($updateData as $columnName => $value) {
439 102
            $column           = $this->columns[$columnName];
440 102
            $quotedColumnName = $this->platform->quoteIdentifier($column->getColumnName());
441 102
            $type             = $column->getType();
442 102
            $placeholder      = $type->convertToDatabaseValueSQL('?', $this->platform);
443
444 102
            $set[]    = sprintf('%s = %s', $quotedColumnName, $placeholder);
445 102
            $params[] = $value;
446 102
            $types[]  = $column->getType();
447
        }
448
449
        // @todo guilhermeblanco Bring this back: $this->em->getUnitOfWork()->getEntityIdentifier($entity);
0 ignored issues
show
Unused Code Comprehensibility introduced by
42% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
450 102
        $identifier = $this->getIdentifier($entity);
451 102
        $where      = [];
452
453 102
        foreach ($this->class->identifier as $idField) {
454 102
            $property = $this->class->getProperty($idField);
455
456
            switch (true) {
457 102
                case ($property instanceof FieldMetadata):
458 99
                    $where[]  = $this->platform->quoteIdentifier($property->getColumnName());
459 99
                    $params[] = $identifier[$idField];
460 99
                    $types[]  = $property->getType();
461 99
                    break;
462
463 4
                case ($property instanceof ToOneAssociationMetadata):
464 4
                    $targetClass     = $this->em->getClassMetadata($property->getTargetEntity());
465 4
                    $targetPersister = $this->em->getUnitOfWork()->getEntityPersister($property->getTargetEntity());
466
467 4
                    foreach ($property->getJoinColumns() as $joinColumn) {
468
                        /** @var JoinColumnMetadata $joinColumn */
469 4
                        $quotedColumnName     = $this->platform->quoteIdentifier($joinColumn->getColumnName());
470 4
                        $referencedColumnName = $joinColumn->getReferencedColumnName();
471
472 4
                        if (! $joinColumn->getType()) {
473
                            $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
0 ignored issues
show
Bug introduced by
$targetClass of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Utility\Per...lper::getTypeOfColumn(). ( Ignorable by Annotation )

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

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

666
            $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...
667
668 832
            foreach ($property->getJoinColumns() as $joinColumn) {
669
                /** @var JoinColumnMetadata $joinColumn */
670 832
                $referencedColumnName = $joinColumn->getReferencedColumnName();
671
672 832
                if (! $joinColumn->getType()) {
673 9
                    $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
0 ignored issues
show
Bug introduced by
$targetClass of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Utility\Per...lper::getTypeOfColumn(). ( Ignorable by Annotation )

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

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

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

720
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, /** @scrutinizer ignore-type */ $targetClass, $this->em));
Loading history...
721
            }
722
723 19
            if ($joinColumn->getColumnName() !== $columnName) {
724
                continue;
725
            }
726
727 19
            return $targetPersister->getColumnValue($propertyValue, $referencedColumnName);
728
        }
729
730
        return null;
731
    }
732
733
    /**
734
     * {@inheritdoc}
735
     */
736 471
    public function load(
737
        array $criteria,
738
        $entity = null,
739
        ?AssociationMetadata $association = null,
740
        array $hints = [],
741
        $lockMode = null,
742
        $limit = null,
743
        array $orderBy = []
744
    ) {
745 471
        $this->switchPersisterContext(null, $limit);
746
747 471
        $sql = $this->getSelectSQL($criteria, $association, $lockMode, $limit, null, $orderBy);
748
749 470
        list($params, $types) = $this->expandParameters($criteria);
750
751 470
        $stmt = $this->conn->executeQuery($sql, $params, $types);
752
753 470
        if ($entity !== null) {
754 63
            $hints[Query::HINT_REFRESH]        = true;
755 63
            $hints[Query::HINT_REFRESH_ENTITY] = $entity;
756
        }
757
758 470
        $hydrator = $this->em->newHydrator($this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
759 470
        $entities = $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, $hints);
760
761 470
        return $entities ? $entities[0] : null;
762
    }
763
764
    /**
765
     * {@inheritdoc}
766
     */
767 395
    public function loadById(array $identifier, $entity = null)
768
    {
769 395
        return $this->load($identifier, $entity);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->load($identifier, $entity) targeting Doctrine\ORM\Persisters\...EntityPersister::load() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
770
    }
771
772
    /**
773
     * {@inheritdoc}
774
     */
775 92
    public function loadToOneEntity(ToOneAssociationMetadata $association, $sourceEntity, array $identifier = [])
776
    {
777 92
        $unitOfWork   = $this->em->getUnitOfWork();
778 92
        $targetEntity = $association->getTargetEntity();
779 92
        $foundEntity  = $unitOfWork->tryGetById($identifier, $targetEntity);
780
781 92
        if ($foundEntity !== false) {
782
            return $foundEntity;
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...
783
        }
784
785 92
        $targetClass = $this->em->getClassMetadata($targetEntity);
786
787 92
        if ($association->isOwningSide()) {
788 30
            $inversedBy            = $association->getInversedBy();
789 30
            $targetProperty        = $inversedBy ? $targetClass->getProperty($inversedBy) : null;
790 30
            $isInverseSingleValued = $targetProperty && $targetProperty instanceof ToOneAssociationMetadata;
791
792
            // Mark inverse side as fetched in the hints, otherwise the UoW would
793
            // try to load it in a separate query (remember: to-one inverse sides can not be lazy).
794 30
            $hints = [];
795
796 30
            if ($isInverseSingleValued) {
797
                $hints['fetched']['r'][$inversedBy] = true;
798
            }
799
800
            /* cascade read-only status
0 ignored issues
show
Unused Code Comprehensibility introduced by
49% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
801
            if ($this->em->getUnitOfWork()->isReadOnly($sourceEntity)) {
802
                $hints[Query::HINT_READ_ONLY] = true;
803
            }
804
            */
805
806 30
            $entity = $this->load($identifier, null, $association, $hints);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $entity is correct as $this->load($identifier,..., $association, $hints) targeting Doctrine\ORM\Persisters\...EntityPersister::load() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
807
808
            // Complete bidirectional association, if necessary
809 30
            if ($entity !== null && $isInverseSingleValued) {
810
                $targetProperty->setValue($entity, $sourceEntity);
811
            }
812
813 30
            return $entity;
814
        }
815
816 62
        $sourceClass       = $association->getDeclaringClass();
817 62
        $owningAssociation = $targetClass->getProperty($association->getMappedBy());
818 62
        $targetTableAlias  = $this->getSQLTableAlias($targetClass->getTableName());
0 ignored issues
show
Unused Code introduced by
The assignment to $targetTableAlias is dead and can be removed.
Loading history...
Bug introduced by
The method getTableName() does not exist on Doctrine\Common\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

818
        $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...
819
820 62
        foreach ($owningAssociation->getJoinColumns() as $joinColumn) {
821 62
            $sourceKeyColumn = $joinColumn->getReferencedColumnName();
822 62
            $targetKeyColumn = $joinColumn->getColumnName();
823
824 62
            if (! isset($sourceClass->fieldNames[$sourceKeyColumn])) {
825
                throw MappingException::joinColumnMustPointToMappedField(
826
                    $sourceClass->getClassName(),
827
                    $sourceKeyColumn
828
                );
829
            }
830
831 62
            $property = $sourceClass->getProperty($sourceClass->fieldNames[$sourceKeyColumn]);
832 62
            $value    = $property->getValue($sourceEntity);
833
834
            // unset the old value and set the new sql aliased value here. By definition
835
            // unset($identifier[$targetKeyColumn] works here with how UnitOfWork::createEntity() calls this method.
836
            // @todo guilhermeblanco In master we have: $identifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
837 62
            unset($identifier[$targetKeyColumn]);
838
839 62
            $identifier[$targetClass->fieldNames[$targetKeyColumn]] = $value;
0 ignored issues
show
Bug introduced by
Accessing fieldNames on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
840
        }
841
842 62
        $entity = $this->load($identifier, null, $association);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $entity is correct as $this->load($identifier, null, $association) targeting Doctrine\ORM\Persisters\...EntityPersister::load() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
843
844 62
        if ($entity !== null) {
845 16
            $owningAssociation->setValue($entity, $sourceEntity);
846
        }
847
848 62
        return $entity;
849
    }
850
851
    /**
852
     * {@inheritdoc}
853
     */
854 15
    public function refresh(array $id, $entity, $lockMode = null)
855
    {
856 15
        $sql                  = $this->getSelectSQL($id, null, $lockMode);
857 15
        list($params, $types) = $this->expandParameters($id);
858 15
        $stmt                 = $this->conn->executeQuery($sql, $params, $types);
859
860 15
        $hydrator = $this->em->newHydrator(Query::HYDRATE_OBJECT);
861 15
        $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [Query::HINT_REFRESH => true]);
862 15
    }
863
864
    /**
865
     * {@inheritDoc}
866
     */
867 46
    public function count($criteria = [])
868
    {
869 46
        $sql = $this->getCountSQL($criteria);
870
871 46
        list($params, $types) = ($criteria instanceof Criteria)
872 25
            ? $this->expandCriteriaParameters($criteria)
873 46
            : $this->expandParameters($criteria);
874
875 46
        return (int) $this->conn->executeQuery($sql, $params, $types)->fetchColumn();
876
    }
877
878
    /**
879
     * {@inheritdoc}
880
     */
881 8
    public function loadCriteria(Criteria $criteria)
882
    {
883 8
        $orderBy = $criteria->getOrderings();
884 8
        $limit   = $criteria->getMaxResults();
885 8
        $offset  = $criteria->getFirstResult();
886 8
        $query   = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy);
887
888 6
        list($params, $types) = $this->expandCriteriaParameters($criteria);
889
890 6
        $stmt         = $this->conn->executeQuery($query, $params, $types);
891 6
        $rsm          = $this->currentPersisterContext->rsm;
892 6
        $hints        = [UnitOfWork::HINT_DEFEREAGERLOAD => true];
893 6
        $hydratorType = $this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT;
894 6
        $hydrator     = $this->em->newHydrator($hydratorType);
895
896 6
        return $hydrator->hydrateAll($stmt, $rsm, $hints);
897
    }
898
899
    /**
900
     * {@inheritdoc}
901
     */
902 37
    public function expandCriteriaParameters(Criteria $criteria)
903
    {
904 37
        $expression = $criteria->getWhereExpression();
905 37
        $sqlParams  = [];
906 37
        $sqlTypes   = [];
907
908 37
        if ($expression === null) {
909 2
            return [$sqlParams, $sqlTypes];
910
        }
911
912 36
        $valueVisitor = new SqlValueVisitor();
913
914 36
        $valueVisitor->dispatch($expression);
915
916 36
        list($params, $types) = $valueVisitor->getParamsAndTypes();
917
918 36
        foreach ($params as $param) {
919 32
            $sqlParams = array_merge($sqlParams, $this->getValues($param));
920
        }
921
922 36
        foreach ($types as $type) {
923 32
            list ($field, $value) = $type;
924 32
            $sqlTypes             = array_merge($sqlTypes, $this->getTypes($field, $value, $this->class));
925
        }
926
927 36
        return [$sqlParams, $sqlTypes];
928
    }
929
930
    /**
931
     * {@inheritdoc}
932
     */
933 71
    public function loadAll(array $criteria = [], array $orderBy = [], $limit = null, $offset = null)
934
    {
935 71
        $this->switchPersisterContext($offset, $limit);
936
937 71
        $sql = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy);
938
939 67
        list($params, $types) = $this->expandParameters($criteria);
940
941 67
        $stmt         = $this->conn->executeQuery($sql, $params, $types);
942 67
        $rsm          = $this->currentPersisterContext->rsm;
943 67
        $hints        = [UnitOfWork::HINT_DEFEREAGERLOAD => true];
944 67
        $hydratorType = $this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT;
945 67
        $hydrator     = $this->em->newHydrator($hydratorType);
946
947 67
        return $hydrator->hydrateAll($stmt, $rsm, $hints);
948
    }
949
950
    /**
951
     * {@inheritdoc}
952
     */
953 8
    public function getManyToManyCollection(
954
        ManyToManyAssociationMetadata $association,
955
        $sourceEntity,
956
        $offset = null,
957
        $limit = null
958
    ) {
959 8
        $this->switchPersisterContext($offset, $limit);
960
961 8
        $stmt = $this->getManyToManyStatement($association, $sourceEntity, $offset, $limit);
962
963 8
        return $this->loadArrayFromStatement($association, $stmt);
964
    }
965
966
    /**
967
     * {@inheritdoc}
968
     */
969 73
    public function loadManyToManyCollection(
970
        ManyToManyAssociationMetadata $association,
971
        $sourceEntity,
972
        PersistentCollection $collection
973
    ) {
974 73
        $stmt = $this->getManyToManyStatement($association, $sourceEntity);
975
976 73
        return $this->loadCollectionFromStatement($association, $stmt, $collection);
977
    }
978
979
    /**
980
     * Loads an array of entities from a given DBAL statement.
981
     *
982
     * @param \Doctrine\DBAL\Statement $stmt
983
     *
984
     * @return mixed[]
985
     */
986 13
    private function loadArrayFromStatement(ToManyAssociationMetadata $association, $stmt)
987
    {
988 13
        $rsm = $this->currentPersisterContext->rsm;
989
990 13
        if ($association->getIndexedBy()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $association->getIndexedBy() of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
991 7
            $rsm = clone ($this->currentPersisterContext->rsm); // this is necessary because the "default rsm" should be changed.
992 7
            $rsm->addIndexBy('r', $association->getIndexedBy());
993
        }
994
995 13
        $hydrator = $this->em->newHydrator(Query::HYDRATE_OBJECT);
996 13
        $hints    = [UnitOfWork::HINT_DEFEREAGERLOAD => true];
997
998 13
        return $hydrator->hydrateAll($stmt, $rsm, $hints);
999
    }
1000
1001
    /**
1002
     * Hydrates a collection from a given DBAL statement.
1003
     *
1004
     * @param \Doctrine\DBAL\Statement $stmt
1005
     * @param PersistentCollection     $collection
1006
     *
1007
     * @return mixed[]
1008
     */
1009 135
    private function loadCollectionFromStatement(ToManyAssociationMetadata $association, $stmt, $collection)
1010
    {
1011 135
        $rsm = $this->currentPersisterContext->rsm;
1012
1013 135
        if ($association->getIndexedBy()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $association->getIndexedBy() of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1014 10
            $rsm = clone ($this->currentPersisterContext->rsm); // this is necessary because the "default rsm" should be changed.
1015 10
            $rsm->addIndexBy('r', $association->getIndexedBy());
1016
        }
1017
1018 135
        $hydrator = $this->em->newHydrator(Query::HYDRATE_OBJECT);
1019
        $hints    = [
1020 135
            UnitOfWork::HINT_DEFEREAGERLOAD => true,
1021 135
            'collection' => $collection,
1022
        ];
1023
1024 135
        return $hydrator->hydrateAll($stmt, $rsm, $hints);
1025
    }
1026
1027
    /**
1028
     * @param object   $sourceEntity
1029
     * @param int|null $offset
1030
     * @param int|null $limit
1031
     *
1032
     * @return \Doctrine\DBAL\Driver\Statement
1033
     *
1034
     * @throws \Doctrine\ORM\Mapping\MappingException
1035
     */
1036 80
    private function getManyToManyStatement(
1037
        ManyToManyAssociationMetadata $association,
1038
        $sourceEntity,
1039
        $offset = null,
1040
        $limit = null
1041
    ) {
1042 80
        $this->switchPersisterContext($offset, $limit);
1043
1044
        /** @var ClassMetadata $sourceClass */
1045 80
        $sourceClass = $this->em->getClassMetadata($association->getSourceEntity());
1046 80
        $class       = $sourceClass;
0 ignored issues
show
Unused Code introduced by
The assignment to $class is dead and can be removed.
Loading history...
1047 80
        $owningAssoc = $association;
1048 80
        $criteria    = [];
1049 80
        $parameters  = [];
1050
1051 80
        if (! $association->isOwningSide()) {
1052 12
            $class       = $this->em->getClassMetadata($association->getTargetEntity());
1053 12
            $owningAssoc = $class->getProperty($association->getMappedBy());
1054
        }
1055
1056 80
        $joinTable     = $owningAssoc->getJoinTable();
1057 80
        $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
1058 80
        $joinColumns   = $association->isOwningSide()
1059 73
            ? $joinTable->getJoinColumns()
1060 80
            : $joinTable->getInverseJoinColumns()
1061
        ;
1062
1063 80
        foreach ($joinColumns as $joinColumn) {
1064 80
            $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName());
1065 80
            $fieldName        = $sourceClass->fieldNames[$joinColumn->getReferencedColumnName()];
1066 80
            $property         = $sourceClass->getProperty($fieldName);
1067
1068 80
            if ($property instanceof FieldMetadata) {
1069 79
                $value = $property->getValue($sourceEntity);
1070 4
            } elseif ($property instanceof AssociationMetadata) {
1071 4
                $property    = $sourceClass->getProperty($fieldName);
1072 4
                $targetClass = $this->em->getClassMetadata($property->getTargetEntity());
1073 4
                $value       = $property->getValue($sourceEntity);
1074
1075 4
                $value = $this->em->getUnitOfWork()->getEntityIdentifier($value);
1076 4
                $value = $value[$targetClass->identifier[0]];
0 ignored issues
show
Bug introduced by
Accessing identifier on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1077
            }
1078
1079 80
            $criteria[$joinTableName . '.' . $quotedColumnName] = $value;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $value does not seem to be defined for all execution paths leading up to this point.
Loading history...
1080 80
            $parameters[]                                       = [
1081 80
                'value' => $value,
1082 80
                'field' => $fieldName,
1083 80
                'class' => $sourceClass,
1084
            ];
1085
        }
1086
1087 80
        $sql = $this->getSelectSQL($criteria, $association, null, $limit, $offset);
1088
1089 80
        list($params, $types) = $this->expandToManyParameters($parameters);
1090
1091 80
        return $this->conn->executeQuery($sql, $params, $types);
1092
    }
1093
1094
    /**
1095
     * {@inheritdoc}
1096
     */
1097 524
    public function getSelectSQL(
1098
        $criteria,
1099
        ?AssociationMetadata $association = null,
1100
        $lockMode = null,
1101
        $limit = null,
1102
        $offset = null,
1103
        array $orderBy = []
1104
    ) {
1105 524
        $this->switchPersisterContext($offset, $limit);
1106
1107 524
        $lockSql    = '';
1108 524
        $joinSql    = '';
1109 524
        $orderBySql = '';
1110
1111 524
        if ($association instanceof ManyToManyAssociationMetadata) {
1112 81
            $joinSql = $this->getSelectManyToManyJoinSQL($association);
1113
        }
1114
1115 524
        if ($association instanceof ToManyAssociationMetadata && $association->getOrderBy()) {
1116 5
            $orderBy = $association->getOrderBy();
1117
        }
1118
1119 524
        if ($orderBy) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $orderBy of type array 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...
1120 11
            $orderBySql = $this->getOrderBySQL($orderBy, $this->getSQLTableAlias($this->class->getTableName()));
1121
        }
1122
1123 522
        $conditionSql = ($criteria instanceof Criteria)
1124 8
            ? $this->getSelectConditionCriteriaSQL($criteria)
1125 520
            : $this->getSelectConditionSQL($criteria, $association);
1126
1127
        switch ($lockMode) {
1128 517
            case LockMode::PESSIMISTIC_READ:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

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

Loading history...
1129
                $lockSql = ' ' . $this->platform->getReadLockSQL();
1130
                break;
1131
1132 517
            case LockMode::PESSIMISTIC_WRITE:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

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

Loading history...
1133
                $lockSql = ' ' . $this->platform->getWriteLockSQL();
1134
                break;
1135
        }
1136
1137 517
        $columnList = $this->getSelectColumnsSQL();
1138 517
        $tableAlias = $this->getSQLTableAlias($this->class->getTableName());
1139 517
        $filterSql  = $this->generateFilterConditionSQL($this->class, $tableAlias);
1140 517
        $tableName  = $this->class->table->getQuotedQualifiedName($this->platform);
1141
1142 517
        if ($filterSql !== '') {
1143 12
            $conditionSql = $conditionSql
1144 11
                ? $conditionSql . ' AND ' . $filterSql
1145 12
                : $filterSql;
1146
        }
1147
1148 517
        $select = 'SELECT ' . $columnList;
1149 517
        $from   = ' FROM ' . $tableName . ' ' . $tableAlias;
1150 517
        $join   = $this->currentPersisterContext->selectJoinSql . $joinSql;
1151 517
        $where  = ($conditionSql ? ' WHERE ' . $conditionSql : '');
1152 517
        $lock   = $this->platform->appendLockHint($from, $lockMode);
1153
        $query  = $select
1154 517
            . $lock
1155 517
            . $join
1156 517
            . $where
1157 517
            . $orderBySql;
1158
1159 517
        return $this->platform->modifyLimitQuery($query, $limit, $offset) . $lockSql;
1160
    }
1161
1162
    /**
1163
     * {@inheritDoc}
1164
     */
1165 41
    public function getCountSQL($criteria = [])
1166
    {
1167 41
        $tableName  = $this->class->table->getQuotedQualifiedName($this->platform);
1168 41
        $tableAlias = $this->getSQLTableAlias($this->class->getTableName());
1169
1170 41
        $conditionSql = ($criteria instanceof Criteria)
1171 25
            ? $this->getSelectConditionCriteriaSQL($criteria)
1172 41
            : $this->getSelectConditionSQL($criteria);
1173
1174 41
        $filterSql = $this->generateFilterConditionSQL($this->class, $tableAlias);
1175
1176 41
        if ($filterSql !== '') {
1177 2
            $conditionSql = $conditionSql
1178 2
                ? $conditionSql . ' AND ' . $filterSql
1179 2
                : $filterSql;
1180
        }
1181
1182
        $sql = 'SELECT COUNT(*) '
1183 41
            . 'FROM ' . $tableName . ' ' . $tableAlias
1184 41
            . (empty($conditionSql) ? '' : ' WHERE ' . $conditionSql);
1185
1186 41
        return $sql;
1187
    }
1188
1189
    /**
1190
     * Gets the ORDER BY SQL snippet for ordered collections.
1191
     *
1192
     * @param mixed[] $orderBy
1193
     * @param string  $baseTableAlias
1194
     *
1195
     * @return string
1196
     *
1197
     * @throws \Doctrine\ORM\ORMException
1198
     */
1199 79
    final protected function getOrderBySQL(array $orderBy, $baseTableAlias)
1200
    {
1201 79
        if (! $orderBy) {
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...
1202 67
            return '';
1203
        }
1204
1205 12
        $orderByList = [];
1206
1207 12
        foreach ($orderBy as $fieldName => $orientation) {
1208 12
            $orientation = strtoupper(trim($orientation));
1209
1210 12
            if (! in_array($orientation, ['ASC', 'DESC'])) {
1211 1
                throw ORMException::invalidOrientation($this->class->getClassName(), $fieldName);
1212
            }
1213
1214 11
            $property = $this->class->getProperty($fieldName);
1215
1216 11
            if ($property instanceof FieldMetadata) {
1217 9
                $tableAlias = $this->getSQLTableAlias($property->getTableName());
1218 9
                $columnName = $this->platform->quoteIdentifier($property->getColumnName());
1219
1220 9
                $orderByList[] = $tableAlias . '.' . $columnName . ' ' . $orientation;
1221
1222 9
                continue;
1223 2
            } elseif ($property instanceof AssociationMetadata) {
1224 2
                if (! $property->isOwningSide()) {
1225 1
                    throw ORMException::invalidFindByInverseAssociation($this->class->getClassName(), $fieldName);
1226
                }
1227
1228 1
                $class      = $this->class->isInheritedProperty($fieldName)
1229
                    ? $property->getDeclaringClass()
1230 1
                    : $this->class;
1231 1
                $tableAlias = $this->getSQLTableAlias($class->getTableName());
1232
1233 1
                foreach ($property->getJoinColumns() as $joinColumn) {
1234
                    /* @var JoinColumnMetadata $joinColumn */
1235 1
                    $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName());
1236
1237 1
                    $orderByList[] = $tableAlias . '.' . $quotedColumnName . ' ' . $orientation;
1238
                }
1239
1240 1
                continue;
1241
            }
1242
1243
            throw ORMException::unrecognizedField($fieldName);
1244
        }
1245
1246 10
        return ' ORDER BY ' . implode(', ', $orderByList);
1247
    }
1248
1249
    /**
1250
     * Gets the SQL fragment with the list of columns to select when querying for
1251
     * an entity in this persister.
1252
     *
1253
     * Subclasses should override this method to alter or change the select column
1254
     * list SQL fragment. Note that in the implementation of BasicEntityPersister
1255
     * the resulting SQL fragment is generated only once and cached in {@link selectColumnListSql}.
1256
     * Subclasses may or may not do the same.
1257
     *
1258
     * @return string The SQL fragment.
1259
     */
1260 518
    protected function getSelectColumnsSQL()
1261
    {
1262 518
        if ($this->currentPersisterContext->selectColumnListSql !== null) {
1263 100
            return $this->currentPersisterContext->selectColumnListSql;
1264
        }
1265
1266 518
        $this->currentPersisterContext->rsm->addEntityResult($this->class->getClassName(), 'r'); // r for root
1267 518
        $this->currentPersisterContext->selectJoinSql = '';
1268
1269 518
        $eagerAliasCounter = 0;
1270 518
        $columnList        = [];
1271
1272 518
        foreach ($this->class->getDeclaredPropertiesIterator() as $fieldName => $property) {
1273
            switch (true) {
1274 518
                case ($property instanceof FieldMetadata):
1275 516
                    $columnList[] = $this->getSelectColumnSQL($fieldName, $this->class);
1276 516
                    break;
1277
1278 465
                case ($property instanceof AssociationMetadata):
1279 461
                    $assocColumnSQL = $this->getSelectColumnAssociationSQL($fieldName, $property, $this->class);
1280
1281 461
                    if ($assocColumnSQL) {
1282 390
                        $columnList[] = $assocColumnSQL;
1283
                    }
1284
1285 461
                    $isAssocToOneInverseSide = $property instanceof ToOneAssociationMetadata && ! $property->isOwningSide();
1286 461
                    $isAssocFromOneEager     = ! $property instanceof ManyToManyAssociationMetadata && $property->getFetchMode() === FetchMode::EAGER;
1287
1288 461
                    if (! ($isAssocFromOneEager || $isAssocToOneInverseSide)) {
1289 439
                        break;
1290
                    }
1291
1292 178
                    if ($property instanceof ToManyAssociationMetadata && $this->currentPersisterContext->handlesLimits) {
1293 3
                        break;
1294
                    }
1295
1296 175
                    $targetEntity = $property->getTargetEntity();
1297 175
                    $eagerEntity  = $this->em->getClassMetadata($targetEntity);
1298
1299 175
                    if ($eagerEntity->inheritanceType !== InheritanceType::NONE) {
0 ignored issues
show
Bug introduced by
Accessing inheritanceType on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1300 5
                        break; // now this is why you shouldn't use inheritance
1301
                    }
1302
1303 170
                    $assocAlias = 'e' . ($eagerAliasCounter++);
1304
1305 170
                    $this->currentPersisterContext->rsm->addJoinedEntityResult($targetEntity, $assocAlias, 'r', $fieldName);
1306
1307 170
                    foreach ($eagerEntity->getDeclaredPropertiesIterator() as $eagerProperty) {
0 ignored issues
show
Bug introduced by
The method getDeclaredPropertiesIterator() does not exist on Doctrine\Common\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

1307
                    foreach ($eagerEntity->/** @scrutinizer ignore-call */ getDeclaredPropertiesIterator() 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...
1308
                        switch (true) {
1309 170
                            case ($eagerProperty instanceof FieldMetadata):
1310 168
                                $columnList[] = $this->getSelectColumnSQL($eagerProperty->getName(), $eagerEntity, $assocAlias);
0 ignored issues
show
Bug introduced by
$eagerEntity of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Persisters\...r::getSelectColumnSQL(). ( Ignorable by Annotation )

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

1310
                                $columnList[] = $this->getSelectColumnSQL($eagerProperty->getName(), /** @scrutinizer ignore-type */ $eagerEntity, $assocAlias);
Loading history...
1311 168
                                break;
1312
1313 167
                            case ($eagerProperty instanceof ToOneAssociationMetadata && $eagerProperty->isOwningSide()):
1314 164
                                $columnList[] = $this->getSelectColumnAssociationSQL(
1315 164
                                    $eagerProperty->getName(),
1316 164
                                    $eagerProperty,
1317 164
                                    $eagerEntity,
0 ignored issues
show
Bug introduced by
$eagerEntity of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Persisters\...tColumnAssociationSQL(). ( Ignorable by Annotation )

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

1317
                                    /** @scrutinizer ignore-type */ $eagerEntity,
Loading history...
1318 164
                                    $assocAlias
1319
                                );
1320 170
                                break;
1321
                        }
1322
                    }
1323
1324 170
                    $owningAssociation = $property;
1325 170
                    $joinCondition     = [];
1326
1327 170
                    if ($property instanceof ToManyAssociationMetadata && $property->getIndexedBy()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $property->getIndexedBy() of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1328 1
                        $this->currentPersisterContext->rsm->addIndexBy($assocAlias, $property->getIndexedBy());
1329
                    }
1330
1331 170
                    if (! $property->isOwningSide()) {
1332 163
                        $owningAssociation = $eagerEntity->getProperty($property->getMappedBy());
1333
                    }
1334
1335 170
                    $joinTableAlias = $this->getSQLTableAlias($eagerEntity->getTableName(), $assocAlias);
1336 170
                    $joinTableName  = $eagerEntity->table->getQuotedQualifiedName($this->platform);
0 ignored issues
show
Bug introduced by
Accessing table on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1337
1338 170
                    $this->currentPersisterContext->selectJoinSql .= ' ' . $this->getJoinSQLForAssociation($property);
1339
1340 170
                    $sourceClass      = $this->em->getClassMetadata($owningAssociation->getSourceEntity());
1341 170
                    $targetClass      = $this->em->getClassMetadata($owningAssociation->getTargetEntity());
1342 170
                    $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName(), $property->isOwningSide() ? $assocAlias : '');
1343 170
                    $sourceTableAlias = $this->getSQLTableAlias($sourceClass->getTableName(), $property->isOwningSide() ? '' : $assocAlias);
1344
1345 170
                    foreach ($owningAssociation->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

1345
                    foreach ($owningAssociation->/** @scrutinizer ignore-call */ getJoinColumns() as $joinColumn) {
Loading history...
1346 170
                        $joinCondition[] = sprintf(
1347 170
                            '%s.%s = %s.%s',
1348 170
                            $sourceTableAlias,
1349 170
                            $this->platform->quoteIdentifier($joinColumn->getColumnName()),
1350 170
                            $targetTableAlias,
1351 170
                            $this->platform->quoteIdentifier($joinColumn->getReferencedColumnName())
1352
                        );
1353
                    }
1354
1355 170
                    $filterSql = $this->generateFilterConditionSQL($eagerEntity, $targetTableAlias);
0 ignored issues
show
Bug introduced by
$eagerEntity of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $targetEntity of Doctrine\ORM\Persisters\...ateFilterConditionSQL(). ( Ignorable by Annotation )

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

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

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

1400
                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, /** @scrutinizer ignore-type */ $targetClass, $this->em));
Loading history...
1401
            }
1402
1403 407
            $this->currentPersisterContext->rsm->addMetaResult(
1404 407
                $alias,
1405 407
                $resultColumnName,
1406 407
                $columnName,
1407 407
                $association->isPrimaryKey(),
1408 407
                $joinColumn->getType()
1409
            );
1410
1411 407
            $columnList[] = sprintf('%s.%s AS %s', $sqlTableAlias, $quotedColumnName, $resultColumnName);
1412
        }
1413
1414 407
        return implode(', ', $columnList);
1415
    }
1416
1417
    /**
1418
     * Gets the SQL join fragment used when selecting entities from a
1419
     * many-to-many association.
1420
     *
1421
     * @param ManyToManyAssociationMetadata $manyToMany
1422
     *
1423
     * @return string
1424
     */
1425 83
    protected function getSelectManyToManyJoinSQL(ManyToManyAssociationMetadata $association)
1426
    {
1427 83
        $conditions        = [];
1428 83
        $owningAssociation = $association;
1429 83
        $sourceTableAlias  = $this->getSQLTableAlias($this->class->getTableName());
1430
1431 83
        if (! $association->isOwningSide()) {
1432 13
            $targetEntity      = $this->em->getClassMetadata($association->getTargetEntity());
1433 13
            $owningAssociation = $targetEntity->getProperty($association->getMappedBy());
1434
        }
1435
1436 83
        $joinTable     = $owningAssociation->getJoinTable();
1437 83
        $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
1438 83
        $joinColumns   = $association->isOwningSide()
1439 75
            ? $joinTable->getInverseJoinColumns()
1440 83
            : $joinTable->getJoinColumns()
1441
        ;
1442
1443 83
        foreach ($joinColumns as $joinColumn) {
1444 83
            $conditions[] = sprintf(
1445 83
                '%s.%s = %s.%s',
1446 83
                $sourceTableAlias,
1447 83
                $this->platform->quoteIdentifier($joinColumn->getReferencedColumnName()),
1448 83
                $joinTableName,
1449 83
                $this->platform->quoteIdentifier($joinColumn->getColumnName())
1450
            );
1451
        }
1452
1453 83
        return ' INNER JOIN ' . $joinTableName . ' ON ' . implode(' AND ', $conditions);
1454
    }
1455
1456
    /**
1457
     * {@inheritdoc}
1458
     */
1459 997
    public function getInsertSQL()
1460
    {
1461 997
        if ($this->insertSql !== null) {
1462 660
            return $this->insertSql;
1463
        }
1464
1465 997
        $columns   = $this->getInsertColumnList();
1466 997
        $tableName = $this->class->table->getQuotedQualifiedName($this->platform);
1467
1468 997
        if (empty($columns)) {
1469 104
            $property       = $this->class->getProperty($this->class->identifier[0]);
1470 104
            $identityColumn = $this->platform->quoteIdentifier($property->getColumnName());
1471
1472 104
            $this->insertSql = $this->platform->getEmptyIdentityInsertSQL($tableName, $identityColumn);
1473
1474 104
            return $this->insertSql;
1475
        }
1476
1477 975
        $quotedColumns = [];
1478 975
        $values        = [];
1479
1480 975
        foreach ($columns as $columnName) {
1481 975
            $column = $this->columns[$columnName];
1482
1483 975
            $quotedColumns[] = $this->platform->quoteIdentifier($column->getColumnName());
1484 975
            $values[]        = $column->getType()->convertToDatabaseValueSQL('?', $this->platform);
1485
        }
1486
1487 975
        $quotedColumns = implode(', ', $quotedColumns);
1488 975
        $values        = implode(', ', $values);
1489
1490 975
        $this->insertSql = sprintf('INSERT INTO %s (%s) VALUES (%s)', $tableName, $quotedColumns, $values);
1491
1492 975
        return $this->insertSql;
1493
    }
1494
1495
    /**
1496
     * Gets the list of columns to put in the INSERT SQL statement.
1497
     *
1498
     * Subclasses should override this method to alter or change the list of
1499
     * columns placed in the INSERT statements used by the persister.
1500
     *
1501
     * @return string[] The list of columns.
1502
     */
1503 913
    protected function getInsertColumnList()
1504
    {
1505 913
        $columns             = [];
1506 913
        $versionPropertyName = $this->class->isVersioned()
0 ignored issues
show
Unused Code introduced by
The assignment to $versionPropertyName is dead and can be removed.
Loading history...
1507 196
            ? $this->class->versionProperty->getName()
1508 913
            : null
1509
        ;
1510
1511 913
        foreach ($this->class->getDeclaredPropertiesIterator() as $name => $property) {
1512
            /*if (isset($this->class->embeddedClasses[$name])) {
0 ignored issues
show
Unused Code Comprehensibility introduced by
73% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1513
                continue;
1514
            }*/
1515
1516
            switch (true) {
1517 913
                case ($property instanceof VersionFieldMetadata):
1518
                    // Do nothing
1519 196
                    break;
1520
1521 913
                case ($property instanceof LocalColumnMetadata):
1522 913
                    if (($property instanceof FieldMetadata
1523
                            && (
1524 913
                                ! $property->hasValueGenerator()
1525 913
                                || $property->getValueGenerator()->getType() !== GeneratorType::IDENTITY
1526
                            )
1527
                        )
1528 913
                        || $this->class->identifier[0] !== $name
1529
                    ) {
1530 850
                        $columnName = $property->getColumnName();
1531
1532 850
                        $columns[] = $columnName;
1533
1534 850
                        $this->columns[$columnName] = $property;
1535
                    }
1536
1537 913
                    break;
1538
1539 807
                case ($property instanceof AssociationMetadata):
1540 803
                    if ($property->isOwningSide() && $property instanceof ToOneAssociationMetadata) {
1541 761
                        $targetClass = $this->em->getClassMetadata($property->getTargetEntity());
1542
1543 761
                        foreach ($property->getJoinColumns() as $joinColumn) {
1544
                            /** @var JoinColumnMetadata $joinColumn */
1545 761
                            $columnName           = $joinColumn->getColumnName();
1546 761
                            $referencedColumnName = $joinColumn->getReferencedColumnName();
1547
1548 761
                            if (! $joinColumn->getType()) {
1549 116
                                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
0 ignored issues
show
Bug introduced by
$targetClass of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Utility\Per...lper::getTypeOfColumn(). ( Ignorable by Annotation )

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

1549
                                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, /** @scrutinizer ignore-type */ $targetClass, $this->em));
Loading history...
1550
                            }
1551
1552 761
                            $columns[] = $columnName;
1553
1554 761
                            $this->columns[$columnName] = $joinColumn;
1555
                        }
1556
                    }
1557
1558 913
                    break;
1559
            }
1560
        }
1561
1562 913
        return $columns;
1563
    }
1564
1565
    /**
1566
     * Gets the SQL snippet of a qualified column name for the given field name.
1567
     *
1568
     * @param string        $field The field name.
1569
     * @param ClassMetadata $class The class that declares this field. The table this class is
1570
     *                             mapped to must own the column for the given field.
1571
     * @param string        $alias
1572
     *
1573
     * @return string
1574
     */
1575 552
    protected function getSelectColumnSQL($field, ClassMetadata $class, $alias = 'r')
1576
    {
1577 552
        $property    = $class->getProperty($field);
1578 552
        $columnAlias = $this->getSQLColumnAlias();
1579 552
        $sql         = sprintf(
1580 552
            '%s.%s',
1581 552
            $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

1581
            $this->getSQLTableAlias($property->/** @scrutinizer ignore-call */ getTableName(), ($alias === 'r' ? '' : $alias)),
Loading history...
1582 552
            $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

1582
            $this->platform->quoteIdentifier($property->/** @scrutinizer ignore-call */ getColumnName())
Loading history...
1583
        );
1584
1585 552
        $this->currentPersisterContext->rsm->addFieldResult($alias, $columnAlias, $field, $class->getClassName());
1586
1587 552
        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

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

1780
                /** @scrutinizer ignore-call */ 
1781
                $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

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

1782
                $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...
1783 2
                    ? $joinTable->getJoinColumns()
1784 3
                    : $joinTable->getInverseJoinColumns()
1785
                ;
1786
1787 3
                foreach ($joinColumns as $joinColumn) {
1788 3
                    $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName());
1789
1790 3
                    $columns[] = $joinTableName . '.' . $quotedColumnName;
1791
                }
1792
            } else {
1793 140
                if (! $owningAssociation->isOwningSide()) {
1794 1
                    throw ORMException::invalidFindByInverseAssociation($this->class->getClassName(), $field);
1795
                }
1796
1797 139
                $class      = $this->class->isInheritedProperty($field)
1798 11
                    ? $owningAssociation->getDeclaringClass()
1799 139
                    : $this->class
1800
                ;
1801 139
                $tableAlias = $this->getSQLTableAlias($class->getTableName());
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

1801
                $tableAlias = $this->getSQLTableAlias($class->/** @scrutinizer ignore-call */ getTableName());
Loading history...
1802
1803 139
                foreach ($owningAssociation->getJoinColumns() as $joinColumn) {
1804 139
                    $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName());
1805
1806 139
                    $columns[] = $tableAlias . '.' . $quotedColumnName;
1807
                }
1808
            }
1809
1810 141
            return $columns;
1811
        }
1812
1813 154
        if ($association !== null && strpos($field, ' ') === false && strpos($field, '(') === false) {
1814
            // very careless developers could potentially open up this normally hidden api for userland attacks,
1815
            // therefore checking for spaces and function calls which are not allowed.
1816
1817
            // found a join column condition, not really a "field"
1818 151
            return [$field];
1819
        }
1820
1821 3
        throw ORMException::unrecognizedField($field);
1822
    }
1823
1824
    /**
1825
     * Gets the conditional SQL fragment used in the WHERE clause when selecting
1826
     * entities in this persister.
1827
     *
1828
     * Subclasses are supposed to override this method if they intend to change
1829
     * or alter the criteria by which entities are selected.
1830
     *
1831
     * @param mixed[] $criteria
1832
     *
1833
     * @return string
1834
     */
1835 559
    protected function getSelectConditionSQL(array $criteria, ?AssociationMetadata $association = null)
1836
    {
1837 559
        $conditions = [];
1838
1839 559
        foreach ($criteria as $field => $value) {
1840 535
            $conditions[] = $this->getSelectConditionStatementSQL($field, $value, $association);
1841
        }
1842
1843 556
        return implode(' AND ', $conditions);
1844
    }
1845
1846
    /**
1847
     * {@inheritdoc}
1848
     */
1849 5
    public function getOneToManyCollection(
1850
        OneToManyAssociationMetadata $association,
1851
        $sourceEntity,
1852
        $offset = null,
1853
        $limit = null
1854
    ) {
1855 5
        $this->switchPersisterContext($offset, $limit);
1856
1857 5
        $stmt = $this->getOneToManyStatement($association, $sourceEntity, $offset, $limit);
1858
1859 5
        return $this->loadArrayFromStatement($association, $stmt);
1860
    }
1861
1862
    /**
1863
     * {@inheritdoc}
1864
     */
1865 72
    public function loadOneToManyCollection(
1866
        OneToManyAssociationMetadata $association,
1867
        $sourceEntity,
1868
        PersistentCollection $collection
1869
    ) {
1870 72
        $stmt = $this->getOneToManyStatement($association, $sourceEntity);
1871
1872 72
        return $this->loadCollectionFromStatement($association, $stmt, $collection);
1873
    }
1874
1875
    /**
1876
     * Builds criteria and execute SQL statement to fetch the one to many entities from.
1877
     *
1878
     * @param object   $sourceEntity
1879
     * @param int|null $offset
1880
     * @param int|null $limit
1881
     *
1882
     * @return \Doctrine\DBAL\Statement
1883
     */
1884 77
    private function getOneToManyStatement(
1885
        OneToManyAssociationMetadata $association,
1886
        $sourceEntity,
1887
        $offset = null,
1888
        $limit = null
1889
    ) {
1890 77
        $this->switchPersisterContext($offset, $limit);
1891
1892 77
        $criteria    = [];
1893 77
        $parameters  = [];
1894 77
        $owningAssoc = $this->class->getProperty($association->getMappedBy());
1895 77
        $sourceClass = $this->em->getClassMetadata($association->getSourceEntity());
1896 77
        $class       = $owningAssoc->getDeclaringClass();
1897 77
        $tableAlias  = $this->getSQLTableAlias($class->getTableName());
1898
1899 77
        foreach ($owningAssoc->getJoinColumns() as $joinColumn) {
1900 77
            $quotedColumnName = $this->platform->quoteIdentifier($joinColumn->getColumnName());
1901 77
            $fieldName        = $sourceClass->fieldNames[$joinColumn->getReferencedColumnName()];
0 ignored issues
show
Bug introduced by
Accessing fieldNames on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1902 77
            $property         = $sourceClass->getProperty($fieldName);
1903
1904 77
            if ($property instanceof FieldMetadata) {
1905 77
                $value = $property->getValue($sourceEntity);
1906 3
            } elseif ($property instanceof AssociationMetadata) {
1907 3
                $targetClass = $this->em->getClassMetadata($property->getTargetEntity());
1908 3
                $value       = $property->getValue($sourceEntity);
1909
1910 3
                $value = $this->em->getUnitOfWork()->getEntityIdentifier($value);
1911 3
                $value = $value[$targetClass->identifier[0]];
0 ignored issues
show
Bug introduced by
Accessing identifier on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1912
            }
1913
1914 77
            $criteria[$tableAlias . '.' . $quotedColumnName] = $value;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $value does not seem to be defined for all execution paths leading up to this point.
Loading history...
1915 77
            $parameters[]                                    = [
1916 77
                'value' => $value,
1917 77
                'field' => $fieldName,
1918 77
                'class' => $sourceClass,
1919
            ];
1920
        }
1921
1922 77
        $sql                  = $this->getSelectSQL($criteria, $association, null, $limit, $offset);
1923 77
        list($params, $types) = $this->expandToManyParameters($parameters);
1924
1925 77
        return $this->conn->executeQuery($sql, $params, $types);
1926
    }
1927
1928
    /**
1929
     * {@inheritdoc}
1930
     */
1931 536
    public function expandParameters($criteria)
1932
    {
1933 536
        $params = [];
1934 536
        $types  = [];
1935
1936 536
        foreach ($criteria as $field => $value) {
1937 512
            if ($value === null) {
1938 3
                continue; // skip null values.
1939
            }
1940
1941 510
            $types  = array_merge($types, $this->getTypes($field, $value, $this->class));
1942 510
            $params = array_merge($params, $this->getValues($value));
1943
        }
1944
1945 536
        return [$params, $types];
1946
    }
1947
1948
    /**
1949
     * Expands the parameters from the given criteria and use the correct binding types if found,
1950
     * specialized for OneToMany or ManyToMany associations.
1951
     *
1952
     * @param mixed[][] $criteria an array of arrays containing following:
1953
     *                             - field to which each criterion will be bound
1954
     *                             - value to be bound
1955
     *                             - class to which the field belongs to
1956
     *
1957
     *
1958
     * @return mixed[][]
1959
     */
1960 147
    private function expandToManyParameters($criteria)
1961
    {
1962 147
        $params = [];
1963 147
        $types  = [];
1964
1965 147
        foreach ($criteria as $criterion) {
1966 147
            if ($criterion['value'] === null) {
1967 6
                continue; // skip null values.
1968
            }
1969
1970 141
            $types  = array_merge($types, $this->getTypes($criterion['field'], $criterion['value'], $criterion['class']));
1971 141
            $params = array_merge($params, $this->getValues($criterion['value']));
1972
        }
1973
1974 147
        return [$params, $types];
1975
    }
1976
1977
    /**
1978
     * Infers field types to be used by parameter type casting.
1979
     *
1980
     * @param string $field
1981
     * @param mixed  $value
1982
     *
1983
     * @return mixed[]
1984
     *
1985
     * @throws \Doctrine\ORM\Query\QueryException
1986
     */
1987 667
    private function getTypes($field, $value, ClassMetadata $class)
1988
    {
1989 667
        $property = $class->getProperty($field);
1990 667
        $types    = [];
1991
1992
        switch (true) {
1993 667
            case ($property instanceof FieldMetadata):
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

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

Loading history...
1994 610
                $types = array_merge($types, [$property->getType()]);
1995 610
                break;
1996
1997 141
            case ($property instanceof AssociationMetadata):
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

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

Loading history...
1998 140
                $class = $this->em->getClassMetadata($property->getTargetEntity());
1999
2000 140
                if (! $property->isOwningSide()) {
2001 2
                    $property = $class->getProperty($property->getMappedBy());
2002 2
                    $class    = $this->em->getClassMetadata($property->getTargetEntity());
2003
                }
2004
2005 140
                $joinColumns = $property instanceof ManyToManyAssociationMetadata
2006 3
                    ? $property->getJoinTable()->getInverseJoinColumns()
2007 140
                    : $property->getJoinColumns()
2008
                ;
2009
2010 140
                foreach ($joinColumns as $joinColumn) {
2011
                    /** @var JoinColumnMetadata $joinColumn */
2012 140
                    $referencedColumnName = $joinColumn->getReferencedColumnName();
2013
2014 140
                    if (! $joinColumn->getType()) {
2015 1
                        $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $class, $this->em));
0 ignored issues
show
Bug introduced by
$class of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Utility\Per...lper::getTypeOfColumn(). ( Ignorable by Annotation )

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

2015
                        $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, /** @scrutinizer ignore-type */ $class, $this->em));
Loading history...
2016
                    }
2017
2018 140
                    $types[] = $joinColumn->getType();
2019
                }
2020
2021 140
                break;
2022
2023
            default:
0 ignored issues
show
Coding Style introduced by
DEFAULT statements must be defined using a colon

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

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

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

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

Loading history...
2024 1
                $types[] = null;
2025 1
                break;
2026
        }
2027
2028 667
        if (is_array($value)) {
2029 16
            return array_map(function ($type) {
2030 16
                return $type->getBindingType() + Connection::ARRAY_PARAM_OFFSET;
2031 16
            }, $types);
2032
        }
2033
2034 657
        return $types;
2035
    }
2036
2037
    /**
2038
     * Retrieves the parameters that identifies a value.
2039
     *
2040
     * @param mixed $value
2041
     *
2042
     * @return mixed[]
2043
     */
2044 542
    private function getValues($value)
2045
    {
2046 542
        if (is_array($value)) {
2047 16
            $newValue = [];
2048
2049 16
            foreach ($value as $itemValue) {
2050 16
                $newValue = array_merge($newValue, $this->getValues($itemValue));
2051
            }
2052
2053 16
            return [$newValue];
2054
        }
2055
2056 542
        $metadataFactory = $this->em->getMetadataFactory();
2057 542
        $unitOfWork      = $this->em->getUnitOfWork();
2058
2059 542
        if (is_object($value) && $metadataFactory->hasMetadataFor(StaticClassNameConverter::getClass($value))) {
2060 45
            $class     = $metadataFactory->getMetadataFor(get_class($value));
2061 45
            $persister = $unitOfWork->getEntityPersister($class->getClassName());
2062
2063 45
            if ($class->isIdentifierComposite()) {
0 ignored issues
show
Bug introduced by
The method isIdentifierComposite() does not exist on Doctrine\Common\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

2063
            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...
2064 3
                $newValue = [];
2065
2066 3
                foreach ($persister->getIdentifier($value) as $innerValue) {
2067 3
                    $newValue = array_merge($newValue, $this->getValues($innerValue));
2068
                }
2069
2070 3
                return $newValue;
2071
            }
2072
        }
2073
2074 542
        return [$this->getIndividualValue($value)];
2075
    }
2076
2077
    /**
2078
     * Retrieves an individual parameter value.
2079
     *
2080
     * @param mixed $value
2081
     *
2082
     * @return mixed
2083
     */
2084 542
    private function getIndividualValue($value)
2085
    {
2086 542
        if (! is_object($value) || ! $this->em->getMetadataFactory()->hasMetadataFor(StaticClassNameConverter::getClass($value))) {
2087 540
            return $value;
2088
        }
2089
2090 45
        return $this->em->getUnitOfWork()->getSingleIdentifierValue($value);
2091
    }
2092
2093
    /**
2094
     * {@inheritdoc}
2095
     */
2096 14
    public function exists($entity, ?Criteria $extraConditions = null)
2097
    {
2098 14
        $criteria = $this->getIdentifier($entity);
2099
2100 14
        if (! $criteria) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $criteria of type array 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...
2101 2
            return false;
2102
        }
2103
2104 13
        $alias = $this->getSQLTableAlias($this->class->getTableName());
2105
2106
        $sql = 'SELECT 1 '
2107 13
             . $this->getLockTablesSql(null)
2108 13
             . ' WHERE ' . $this->getSelectConditionSQL($criteria);
2109
2110 13
        list($params, $types) = $this->expandParameters($criteria);
2111
2112 13
        if ($extraConditions !== null) {
2113 9
            $sql                                 .= ' AND ' . $this->getSelectConditionCriteriaSQL($extraConditions);
2114 9
            list($criteriaParams, $criteriaTypes) = $this->expandCriteriaParameters($extraConditions);
2115
2116 9
            $params = array_merge($params, $criteriaParams);
2117 9
            $types  = array_merge($types, $criteriaTypes);
2118
        }
2119
2120 13
        $filterSql = $this->generateFilterConditionSQL($this->class, $alias);
2121
2122 13
        if ($filterSql) {
2123 3
            $sql .= ' AND ' . $filterSql;
2124
        }
2125
2126 13
        return (bool) $this->conn->fetchColumn($sql, $params, 0, $types);
2127
    }
2128
2129
    /**
2130
     * Generates the appropriate join SQL for the given association.
2131
     *
2132
     * @return string LEFT JOIN if one of the columns is nullable, INNER JOIN otherwise.
2133
     */
2134 170
    protected function getJoinSQLForAssociation(AssociationMetadata $association)
2135
    {
2136 170
        if (! $association->isOwningSide()) {
2137 163
            return 'LEFT JOIN';
2138
        }
2139
2140
        // if one of the join columns is nullable, return left join
2141 13
        foreach ($association->getJoinColumns() as $joinColumn) {
2142 13
            if (! $joinColumn->isNullable()) {
2143 5
                continue;
2144
            }
2145
2146 11
            return 'LEFT JOIN';
2147
        }
2148
2149 5
        return 'INNER JOIN';
2150
    }
2151
2152
    /**
2153
     * Gets an SQL column alias for a column name.
2154
     *
2155
     * @return string
2156
     */
2157 553
    public function getSQLColumnAlias()
2158
    {
2159 553
        return $this->platform->getSQLResultCasing('c' . $this->currentPersisterContext->sqlAliasCounter++);
2160
    }
2161
2162
    /**
2163
     * Generates the filter SQL for a given entity and table alias.
2164
     *
2165
     * @param ClassMetadata $targetEntity     Metadata of the target entity.
2166
     * @param string        $targetTableAlias The table alias of the joined/selected table.
2167
     *
2168
     * @return string The SQL query part to add to a query.
2169
     */
2170 577
    protected function generateFilterConditionSQL(ClassMetadata $targetEntity, $targetTableAlias)
2171
    {
2172 577
        $filterClauses = [];
2173
2174 577
        foreach ($this->em->getFilters()->getEnabledFilters() as $filter) {
2175 22
            $filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias);
2176
2177 22
            if ($filterExpr !== '') {
2178 22
                $filterClauses[] = '(' . $filterExpr . ')';
2179
            }
2180
        }
2181
2182 577
        $sql = implode(' AND ', $filterClauses);
2183
2184 577
        return $sql ? '(' . $sql . ')' : ''; // Wrap again to avoid "X or Y and FilterConditionSQL"
2185
    }
2186
2187
    /**
2188
     * Switches persister context according to current query offset/limits
2189
     *
2190
     * This is due to the fact that to-many associations cannot be fetch-joined when a limit is involved
2191
     *
2192
     * @param int|null $offset
2193
     * @param int|null $limit
2194
     */
2195 559
    protected function switchPersisterContext($offset, $limit)
2196
    {
2197 559
        if ($offset === null && $limit === null) {
2198 546
            $this->currentPersisterContext = $this->noLimitsContext;
2199
2200 546
            return;
2201
        }
2202
2203 41
        $this->currentPersisterContext = $this->limitsHandlingContext;
2204 41
    }
2205
}
2206