Failed Conditions
Push — master ( 2ade86...13f838 )
by Jonathan
18s
created

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

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ORM\Persisters\Entity;
21
22
use Doctrine\Common\Collections\Criteria;
23
use Doctrine\Common\Collections\Expr\Comparison;
24
use Doctrine\Common\Util\ClassUtils;
25
use Doctrine\DBAL\Connection;
26
use Doctrine\DBAL\LockMode;
27
use Doctrine\DBAL\Types\Type;
28
use Doctrine\ORM\EntityManagerInterface;
29
use Doctrine\ORM\Mapping\ClassMetadata;
30
use Doctrine\ORM\Mapping\MappingException;
31
use Doctrine\ORM\OptimisticLockException;
32
use Doctrine\ORM\ORMException;
33
use Doctrine\ORM\PersistentCollection;
34
use Doctrine\ORM\Persisters\SqlExpressionVisitor;
35
use Doctrine\ORM\Persisters\SqlValueVisitor;
36
use Doctrine\ORM\Query;
37
use Doctrine\ORM\UnitOfWork;
38
use Doctrine\ORM\Utility\IdentifierFlattener;
39
use Doctrine\ORM\Utility\PersisterHelper;
40
41
/**
42
 * A BasicEntityPersister maps an entity to a single table in a relational database.
43
 *
44
 * A persister is always responsible for a single entity type.
45
 *
46
 * EntityPersisters are used during a UnitOfWork to apply any changes to the persistent
47
 * state of entities onto a relational database when the UnitOfWork is committed,
48
 * as well as for basic querying of entities and their associations (not DQL).
49
 *
50
 * The persisting operations that are invoked during a commit of a UnitOfWork to
51
 * persist the persistent entity state are:
52
 *
53
 *   - {@link addInsert} : To schedule an entity for insertion.
54
 *   - {@link executeInserts} : To execute all scheduled insertions.
55
 *   - {@link update} : To update the persistent state of an entity.
56
 *   - {@link delete} : To delete the persistent state of an entity.
57
 *
58
 * As can be seen from the above list, insertions are batched and executed all at once
59
 * for increased efficiency.
60
 *
61
 * The querying operations invoked during a UnitOfWork, either through direct find
62
 * requests or lazy-loading, are the following:
63
 *
64
 *   - {@link load} : Loads (the state of) a single, managed entity.
65
 *   - {@link loadAll} : Loads multiple, managed entities.
66
 *   - {@link loadOneToOneEntity} : Loads a one/many-to-one entity association (lazy-loading).
67
 *   - {@link loadOneToManyCollection} : Loads a one-to-many entity association (lazy-loading).
68
 *   - {@link loadManyToManyCollection} : Loads a many-to-many entity association (lazy-loading).
69
 *
70
 * The BasicEntityPersister implementation provides the default behavior for
71
 * persisting and querying entities that are mapped to a single database table.
72
 *
73
 * Subclasses can be created to provide custom persisting and querying strategies,
74
 * i.e. spanning multiple tables.
75
 *
76
 * @author Roman Borschel <[email protected]>
77
 * @author Giorgio Sironi <[email protected]>
78
 * @author Benjamin Eberlei <[email protected]>
79
 * @author Alexander <[email protected]>
80
 * @author Fabio B. Silva <[email protected]>
81
 * @author Rob Caiger <[email protected]>
82
 * @since 2.0
83
 */
84
class BasicEntityPersister implements EntityPersister
85
{
86
    /**
87
     * @var array
88
     */
89
    static private $comparisonMap = [
90
        Comparison::EQ          => '= %s',
91
        Comparison::IS          => '= %s',
92
        Comparison::NEQ         => '!= %s',
93
        Comparison::GT          => '> %s',
94
        Comparison::GTE         => '>= %s',
95
        Comparison::LT          => '< %s',
96
        Comparison::LTE         => '<= %s',
97
        Comparison::IN          => 'IN (%s)',
98
        Comparison::NIN         => 'NOT IN (%s)',
99
        Comparison::CONTAINS    => 'LIKE %s',
100
        Comparison::STARTS_WITH => 'LIKE %s',
101
        Comparison::ENDS_WITH   => 'LIKE %s',
102
    ];
103
104
    /**
105
     * Metadata object that describes the mapping of the mapped entity class.
106
     *
107
     * @var \Doctrine\ORM\Mapping\ClassMetadata
108
     */
109
    protected $class;
110
111
    /**
112
     * The underlying DBAL Connection of the used EntityManager.
113
     *
114
     * @var \Doctrine\DBAL\Connection $conn
115
     */
116
    protected $conn;
117
118
    /**
119
     * The database platform.
120
     *
121
     * @var \Doctrine\DBAL\Platforms\AbstractPlatform
122
     */
123
    protected $platform;
124
125
    /**
126
     * The EntityManager instance.
127
     *
128
     * @var EntityManagerInterface
129
     */
130
    protected $em;
131
132
    /**
133
     * Queued inserts.
134
     *
135
     * @var array
136
     */
137
    protected $queuedInserts = [];
138
139
    /**
140
     * The map of column names to DBAL mapping types of all prepared columns used
141
     * when INSERTing or UPDATEing an entity.
142
     *
143
     * @var array
144
     *
145
     * @see prepareInsertData($entity)
146
     * @see prepareUpdateData($entity)
147
     */
148
    protected $columnTypes = [];
149
150
    /**
151
     * The map of quoted column names.
152
     *
153
     * @var array
154
     *
155
     * @see prepareInsertData($entity)
156
     * @see prepareUpdateData($entity)
157
     */
158
    protected $quotedColumns = [];
159
160
    /**
161
     * The INSERT SQL statement used for entities handled by this persister.
162
     * This SQL is only generated once per request, if at all.
163
     *
164
     * @var string
165
     */
166
    private $insertSql;
167
168
    /**
169
     * The quote strategy.
170
     *
171
     * @var \Doctrine\ORM\Mapping\QuoteStrategy
172
     */
173
    protected $quoteStrategy;
174
175
    /**
176
     * The IdentifierFlattener used for manipulating identifiers
177
     *
178
     * @var \Doctrine\ORM\Utility\IdentifierFlattener
179
     */
180
    private $identifierFlattener;
181
182
    /**
183
     * @var CachedPersisterContext
184
     */
185
    protected $currentPersisterContext;
186
187
    /**
188
     * @var CachedPersisterContext
189
     */
190
    private $limitsHandlingContext;
191
192
    /**
193
     * @var CachedPersisterContext
194
     */
195
    private $noLimitsContext;
196
197
    /**
198
     * Initializes a new <tt>BasicEntityPersister</tt> that uses the given EntityManager
199
     * and persists instances of the class described by the given ClassMetadata descriptor.
200
     *
201
     * @param EntityManagerInterface $em
202
     * @param ClassMetadata          $class
203
     */
204 1150
    public function __construct(EntityManagerInterface $em, ClassMetadata $class)
205
    {
206 1150
        $this->em                    = $em;
207 1150
        $this->class                 = $class;
208 1150
        $this->conn                  = $em->getConnection();
209 1150
        $this->platform              = $this->conn->getDatabasePlatform();
210 1150
        $this->quoteStrategy         = $em->getConfiguration()->getQuoteStrategy();
211 1150
        $this->identifierFlattener   = new IdentifierFlattener($em->getUnitOfWork(), $em->getMetadataFactory());
212 1150
        $this->noLimitsContext       = $this->currentPersisterContext = new CachedPersisterContext(
213 1150
            $class,
214 1150
            new Query\ResultSetMapping(),
215 1150
            false
216
        );
217 1150
        $this->limitsHandlingContext = new CachedPersisterContext(
218 1150
            $class,
219 1150
            new Query\ResultSetMapping(),
220 1150
            true
221
        );
222 1150
    }
223
224
    /**
225
     * {@inheritdoc}
226
     */
227 18
    public function getClassMetadata()
228
    {
229 18
        return $this->class;
230
    }
231
232
    /**
233
     * {@inheritdoc}
234
     */
235 11
    public function getResultSetMapping()
236
    {
237 11
        return $this->currentPersisterContext->rsm;
238
    }
239
240
    /**
241
     * {@inheritdoc}
242
     */
243 1033
    public function addInsert($entity)
244
    {
245 1033
        $this->queuedInserts[spl_object_hash($entity)] = $entity;
246 1033
    }
247
248
    /**
249
     * {@inheritdoc}
250
     */
251 94
    public function getInserts()
252
    {
253 94
        return $this->queuedInserts;
254
    }
255
256
    /**
257
     * {@inheritdoc}
258
     */
259 1007
    public function executeInserts()
260
    {
261 1007
        if ( ! $this->queuedInserts) {
262 640
            return [];
263
        }
264
265 958
        $postInsertIds  = [];
266 958
        $idGenerator    = $this->class->idGenerator;
267 958
        $isPostInsertId = $idGenerator->isPostInsertGenerator();
268
269 958
        $stmt       = $this->conn->prepare($this->getInsertSQL());
270 958
        $tableName  = $this->class->getTableName();
271
272 958
        foreach ($this->queuedInserts as $entity) {
273 958
            $insertData = $this->prepareInsertData($entity);
274
275 958
            if (isset($insertData[$tableName])) {
276 931
                $paramIndex = 1;
277
278 931
                foreach ($insertData[$tableName] as $column => $value) {
279 931
                    $stmt->bindValue($paramIndex++, $value, $this->columnTypes[$column]);
280
                }
281
            }
282
283 958
            $stmt->execute();
284
285 957 View Code Duplication
            if ($isPostInsertId) {
286 868
                $generatedId = $idGenerator->generate($this->em, $entity);
287
                $id = [
288 868
                    $this->class->identifier[0] => $generatedId
289
                ];
290 868
                $postInsertIds[] = [
291 868
                    'generatedId' => $generatedId,
292 868
                    'entity' => $entity,
293
                ];
294
            } else {
295 262
                $id = $this->class->getIdentifierValues($entity);
296
            }
297
298 957
            if ($this->class->isVersioned) {
299 957
                $this->assignDefaultVersionValue($entity, $id);
300
            }
301
        }
302
303 957
        $stmt->closeCursor();
304 957
        $this->queuedInserts = [];
305
306 957
        return $postInsertIds;
307
    }
308
309
    /**
310
     * Retrieves the default version value which was created
311
     * by the preceding INSERT statement and assigns it back in to the
312
     * entities version field.
313
     *
314
     * @param object $entity
315
     * @param array  $id
316
     *
317
     * @return void
318
     */
319 200
    protected function assignDefaultVersionValue($entity, array $id)
320
    {
321 200
        $value = $this->fetchVersionValue($this->class, $id);
322
323 200
        $this->class->setFieldValue($entity, $this->class->versionField, $value);
324 200
    }
325
326
    /**
327
     * Fetches the current version value of a versioned entity.
328
     *
329
     * @param \Doctrine\ORM\Mapping\ClassMetadata $versionedClass
330
     * @param array                               $id
331
     *
332
     * @return mixed
333
     */
334 209
    protected function fetchVersionValue($versionedClass, array $id)
335
    {
336 209
        $versionField = $versionedClass->versionField;
337 209
        $fieldMapping = $versionedClass->fieldMappings[$versionField];
338 209
        $tableName    = $this->quoteStrategy->getTableName($versionedClass, $this->platform);
339 209
        $identifier   = $this->quoteStrategy->getIdentifierColumnNames($versionedClass, $this->platform);
340 209
        $columnName   = $this->quoteStrategy->getColumnName($versionField, $versionedClass, $this->platform);
341
342
        // FIXME: Order with composite keys might not be correct
343 209
        $sql = 'SELECT ' . $columnName
344 209
             . ' FROM '  . $tableName
345 209
             . ' WHERE ' . implode(' = ? AND ', $identifier) . ' = ?';
346
347
348 209
        $flatId = $this->identifierFlattener->flattenIdentifier($versionedClass, $id);
349
350 209
        $value = $this->conn->fetchColumn(
351 209
            $sql,
352 209
            array_values($flatId),
353 209
            0,
354 209
            $this->extractIdentifierTypes($id, $versionedClass)
355
        );
356
357 209
        return Type::getType($fieldMapping['type'])->convertToPHPValue($value, $this->platform);
358
    }
359
360 209
    private function extractIdentifierTypes(array $id, ClassMetadata $versionedClass) : array
361
    {
362 209
        $types = [];
363
364 209
        foreach ($id as $field => $value) {
365 209
            $types = array_merge($types, $this->getTypes($field, $value, $versionedClass));
366
        }
367
368 209
        return $types;
369
    }
370
371
    /**
372
     * {@inheritdoc}
373
     */
374 97
    public function update($entity)
375
    {
376 97
        $tableName  = $this->class->getTableName();
377 97
        $updateData = $this->prepareUpdateData($entity);
378
379 97
        if ( ! isset($updateData[$tableName]) || ! ($data = $updateData[$tableName])) {
380 8
            return;
381
        }
382
383 89
        $isVersioned     = $this->class->isVersioned;
384 89
        $quotedTableName = $this->quoteStrategy->getTableName($this->class, $this->platform);
385
386 89
        $this->updateTable($entity, $quotedTableName, $data, $isVersioned);
387
388 87
        if ($isVersioned) {
389 12
            $id = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
390
391 12
            $this->assignDefaultVersionValue($entity, $id);
392
        }
393 87
    }
394
395
    /**
396
     * Performs an UPDATE statement for an entity on a specific table.
397
     * The UPDATE can optionally be versioned, which requires the entity to have a version field.
398
     *
399
     * @param object  $entity          The entity object being updated.
400
     * @param string  $quotedTableName The quoted name of the table to apply the UPDATE on.
401
     * @param array   $updateData      The map of columns to update (column => value).
402
     * @param boolean $versioned       Whether the UPDATE should be versioned.
403
     *
404
     * @return void
405
     *
406
     * @throws \Doctrine\ORM\ORMException
407
     * @throws \Doctrine\ORM\OptimisticLockException
408
     */
409 120
    protected final function updateTable($entity, $quotedTableName, array $updateData, $versioned = false)
410
    {
411 120
        $set    = [];
412 120
        $types  = [];
413 120
        $params = [];
414
415 120
        foreach ($updateData as $columnName => $value) {
416 120
            $placeholder = '?';
417 120
            $column      = $columnName;
418
419
            switch (true) {
420 120
                case isset($this->class->fieldNames[$columnName]):
421 60
                    $fieldName  = $this->class->fieldNames[$columnName];
422 60
                    $column     = $this->quoteStrategy->getColumnName($fieldName, $this->class, $this->platform);
423
424 60 View Code Duplication
                    if (isset($this->class->fieldMappings[$fieldName]['requireSQLConversion'])) {
425 3
                        $type        = Type::getType($this->columnTypes[$columnName]);
426 3
                        $placeholder = $type->convertToDatabaseValueSQL('?', $this->platform);
427
                    }
428
429 60
                    break;
430
431 62
                case isset($this->quotedColumns[$columnName]):
432 62
                    $column = $this->quotedColumns[$columnName];
433
434 62
                    break;
435
            }
436
437 120
            $params[]   = $value;
438 120
            $set[]      = $column . ' = ' . $placeholder;
439 120
            $types[]    = $this->columnTypes[$columnName];
440
        }
441
442 120
        $where      = [];
443 120
        $identifier = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
444
445 120
        foreach ($this->class->identifier as $idField) {
446 120
            if ( ! isset($this->class->associationMappings[$idField])) {
447 117
                $params[]   = $identifier[$idField];
448 117
                $types[]    = $this->class->fieldMappings[$idField]['type'];
449 117
                $where[]    = $this->quoteStrategy->getColumnName($idField, $this->class, $this->platform);
450
451 117
                continue;
452
            }
453
454 4
            $params[]       = $identifier[$idField];
455 4
            $where[]        = $this->class->associationMappings[$idField]['joinColumns'][0]['name'];
456 4
            $targetMapping  = $this->em->getClassMetadata($this->class->associationMappings[$idField]['targetEntity']);
457
458
            switch (true) {
459 4 View Code Duplication
                case (isset($targetMapping->fieldMappings[$targetMapping->identifier[0]])):
460 3
                    $types[] = $targetMapping->fieldMappings[$targetMapping->identifier[0]]['type'];
461 3
                    break;
462
463 1 View Code Duplication
                case (isset($targetMapping->associationMappings[$targetMapping->identifier[0]])):
464 1
                    $types[] = $targetMapping->associationMappings[$targetMapping->identifier[0]]['type'];
465 1
                    break;
466
467
                default:
468 4
                    throw ORMException::unrecognizedField($targetMapping->identifier[0]);
469
            }
470
471
        }
472
473 120
        if ($versioned) {
474 18
            $versionField       = $this->class->versionField;
475 18
            $versionFieldType   = $this->class->fieldMappings[$versionField]['type'];
476 18
            $versionColumn      = $this->quoteStrategy->getColumnName($versionField, $this->class, $this->platform);
477
478 18
            $where[]    = $versionColumn;
479 18
            $types[]    = $this->class->fieldMappings[$versionField]['type'];
480 18
            $params[]   = $this->class->reflFields[$versionField]->getValue($entity);
481
482
            switch ($versionFieldType) {
483 18
                case Type::SMALLINT:
484 18
                case Type::INTEGER:
485 1
                case Type::BIGINT:
486 17
                    $set[] = $versionColumn . ' = ' . $versionColumn . ' + 1';
487 17
                    break;
488
489 1
                case Type::DATETIME:
490 1
                    $set[] = $versionColumn . ' = CURRENT_TIMESTAMP';
491 1
                    break;
492
            }
493
        }
494
495 120
        $sql = 'UPDATE ' . $quotedTableName
496 120
             . ' SET ' . implode(', ', $set)
497 120
             . ' WHERE ' . implode(' = ? AND ', $where) . ' = ?';
498
499 120
        $result = $this->conn->executeUpdate($sql, $params, $types);
500
501 120
        if ($versioned && ! $result) {
502 4
            throw OptimisticLockException::lockFailed($entity);
503
        }
504 117
    }
505
506
    /**
507
     * @todo Add check for platform if it supports foreign keys/cascading.
508
     *
509
     * @param array $identifier
510
     *
511
     * @return void
512
     */
513 65
    protected function deleteJoinTableRecords($identifier)
514
    {
515 65
        foreach ($this->class->associationMappings as $mapping) {
516 49
            if ($mapping['type'] !== ClassMetadata::MANY_TO_MANY) {
517 48
                continue;
518
            }
519
520
            // @Todo this only covers scenarios with no inheritance or of the same level. Is there something
521
            // like self-referential relationship between different levels of an inheritance hierarchy? I hope not!
522 24
            $selfReferential = ($mapping['targetEntity'] == $mapping['sourceEntity']);
523 24
            $class           = $this->class;
524 24
            $association     = $mapping;
525 24
            $otherColumns    = [];
526 24
            $otherKeys       = [];
527 24
            $keys            = [];
528
529 24 View Code Duplication
            if ( ! $mapping['isOwningSide']) {
530 6
                $class       = $this->em->getClassMetadata($mapping['targetEntity']);
531 6
                $association = $class->associationMappings[$mapping['mappedBy']];
532
            }
533
534 24
            $joinColumns = $mapping['isOwningSide']
535 20
                ? $association['joinTable']['joinColumns']
536 24
                : $association['joinTable']['inverseJoinColumns'];
537
538
539 24
            if ($selfReferential) {
540 1
                $otherColumns = (! $mapping['isOwningSide'])
541
                    ? $association['joinTable']['joinColumns']
542 1
                    : $association['joinTable']['inverseJoinColumns'];
543
            }
544
545 24
            foreach ($joinColumns as $joinColumn) {
546 24
                $keys[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
547
            }
548
549 24
            foreach ($otherColumns as $joinColumn) {
550 1
                $otherKeys[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
551
            }
552
553 24
            if (isset($mapping['isOnDeleteCascade'])) {
554 5
                continue;
555
            }
556
557 20
            $joinTableName = $this->quoteStrategy->getJoinTableName($association, $this->class, $this->platform);
558
559 20
            $this->conn->delete($joinTableName, array_combine($keys, $identifier));
560
561 20
            if ($selfReferential) {
562 20
                $this->conn->delete($joinTableName, array_combine($otherKeys, $identifier));
563
            }
564
        }
565 65
    }
566
567
    /**
568
     * {@inheritdoc}
569
     */
570 61
    public function delete($entity)
571
    {
572 61
        $self       = $this;
573 61
        $class      = $this->class;
574 61
        $identifier = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
575 61
        $tableName  = $this->quoteStrategy->getTableName($class, $this->platform);
576 61
        $idColumns  = $this->quoteStrategy->getIdentifierColumnNames($class, $this->platform);
577 61
        $id         = array_combine($idColumns, $identifier);
578 61
        $types      = array_map(function ($identifier) use ($class, $self) {
579 61
            if (isset($class->fieldMappings[$identifier])) {
580 59
                return $class->fieldMappings[$identifier]['type'];
581
            }
582
583 5
            $targetMapping = $self->em->getClassMetadata($class->associationMappings[$identifier]['targetEntity']);
584
585 5
            if (isset($targetMapping->fieldMappings[$targetMapping->identifier[0]])) {
586 4
                return $targetMapping->fieldMappings[$targetMapping->identifier[0]]['type'];
587
            }
588
589 1
            if (isset($targetMapping->associationMappings[$targetMapping->identifier[0]])) {
590 1
                return $targetMapping->associationMappings[$targetMapping->identifier[0]]['type'];
591
            }
592
593
            throw ORMException::unrecognizedField($targetMapping->identifier[0]);
594 61
        }, $class->identifier);
595
596 61
        $this->deleteJoinTableRecords($identifier);
597
598 61
        return (bool) $this->conn->delete($tableName, $id, $types);
599
    }
600
601
    /**
602
     * Prepares the changeset of an entity for database insertion (UPDATE).
603
     *
604
     * The changeset is obtained from the currently running UnitOfWork.
605
     *
606
     * During this preparation the array that is passed as the second parameter is filled with
607
     * <columnName> => <value> pairs, grouped by table name.
608
     *
609
     * Example:
610
     * <code>
611
     * array(
612
     *    'foo_table' => array('column1' => 'value1', 'column2' => 'value2', ...),
613
     *    'bar_table' => array('columnX' => 'valueX', 'columnY' => 'valueY', ...),
614
     *    ...
615
     * )
616
     * </code>
617
     *
618
     * @param object $entity The entity for which to prepare the data.
619
     *
620
     * @return array The prepared data.
621
     */
622 1038
    protected function prepareUpdateData($entity)
623
    {
624 1038
        $versionField = null;
625 1038
        $result       = [];
626 1038
        $uow          = $this->em->getUnitOfWork();
627
628 1038
        if (($versioned = $this->class->isVersioned) != false) {
629 213
            $versionField = $this->class->versionField;
630
        }
631
632 1038
        foreach ($uow->getEntityChangeSet($entity) as $field => $change) {
633 1002
            if (isset($versionField) && $versionField == $field) {
634
                continue;
635
            }
636
637 1002
            if (isset($this->class->embeddedClasses[$field])) {
638 9
                continue;
639
            }
640
641 1002
            $newVal = $change[1];
642
643 1002
            if ( ! isset($this->class->associationMappings[$field])) {
644 964
                $fieldMapping = $this->class->fieldMappings[$field];
645 964
                $columnName   = $fieldMapping['columnName'];
646
647 964
                $this->columnTypes[$columnName] = $fieldMapping['type'];
648
649 964
                $result[$this->getOwningTable($field)][$columnName] = $newVal;
650
651 964
                continue;
652
            }
653
654 853
            $assoc = $this->class->associationMappings[$field];
655
656
            // Only owning side of x-1 associations can have a FK column.
657 853
            if ( ! $assoc['isOwningSide'] || ! ($assoc['type'] & ClassMetadata::TO_ONE)) {
658 8
                continue;
659
            }
660
661 853
            if ($newVal !== null) {
662 631
                $oid = spl_object_hash($newVal);
663
664 631
                if (isset($this->queuedInserts[$oid]) || $uow->isScheduledForInsert($newVal)) {
665
                    // The associated entity $newVal is not yet persisted, so we must
666
                    // set $newVal = null, in order to insert a null value and schedule an
667
                    // extra update on the UnitOfWork.
668 43
                    $uow->scheduleExtraUpdate($entity, [$field => [null, $newVal]]);
669
670 43
                    $newVal = null;
671
                }
672
            }
673
674 853
            $newValId = null;
675
676 853
            if ($newVal !== null) {
677 631
                $newValId = $uow->getEntityIdentifier($newVal);
678
            }
679
680 853
            $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
681 853
            $owningTable = $this->getOwningTable($field);
682
683 853
            foreach ($assoc['joinColumns'] as $joinColumn) {
684 853
                $sourceColumn = $joinColumn['name'];
685 853
                $targetColumn = $joinColumn['referencedColumnName'];
686 853
                $quotedColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
687
688 853
                $this->quotedColumns[$sourceColumn]  = $quotedColumn;
689 853
                $this->columnTypes[$sourceColumn]    = PersisterHelper::getTypeOfColumn($targetColumn, $targetClass, $this->em);
690 853
                $result[$owningTable][$sourceColumn] = $newValId
691 631
                    ? $newValId[$targetClass->getFieldForColumn($targetColumn)]
692 853
                    : null;
693
            }
694
        }
695
696 1038
        return $result;
697
    }
698
699
    /**
700
     * Prepares the data changeset of a managed entity for database insertion (initial INSERT).
701
     * The changeset of the entity is obtained from the currently running UnitOfWork.
702
     *
703
     * The default insert data preparation is the same as for updates.
704
     *
705
     * @param object $entity The entity for which to prepare the data.
706
     *
707
     * @return array The prepared data for the tables to update.
708
     *
709
     * @see prepareUpdateData
710
     */
711 1033
    protected function prepareInsertData($entity)
712
    {
713 1033
        return $this->prepareUpdateData($entity);
714
    }
715
716
    /**
717
     * {@inheritdoc}
718
     */
719 930
    public function getOwningTable($fieldName)
720
    {
721 930
        return $this->class->getTableName();
722
    }
723
724
    /**
725
     * {@inheritdoc}
726
     */
727 509
    public function load(array $criteria, $entity = null, $assoc = null, array $hints = [], $lockMode = null, $limit = null, array $orderBy = null)
728
    {
729 509
        $this->switchPersisterContext(null, $limit);
730
731 509
        $sql = $this->getSelectSQL($criteria, $assoc, $lockMode, $limit, null, $orderBy);
732 508
        list($params, $types) = $this->expandParameters($criteria);
733 508
        $stmt = $this->conn->executeQuery($sql, $params, $types);
734
735 508 View Code Duplication
        if ($entity !== null) {
736 72
            $hints[Query::HINT_REFRESH]         = true;
737 72
            $hints[Query::HINT_REFRESH_ENTITY]  = $entity;
738
        }
739
740 508
        $hydrator = $this->em->newHydrator($this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
741 508
        $entities = $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, $hints);
742
743 508
        return $entities ? $entities[0] : null;
744
    }
745
746
    /**
747
     * {@inheritdoc}
748
     */
749 433
    public function loadById(array $identifier, $entity = null)
750
    {
751 433
        return $this->load($identifier, $entity);
752
    }
753
754
    /**
755
     * {@inheritdoc}
756
     */
757 96
    public function loadOneToOneEntity(array $assoc, $sourceEntity, array $identifier = [])
758
    {
759 96
        if (($foundEntity = $this->em->getUnitOfWork()->tryGetById($identifier, $assoc['targetEntity'])) != false) {
760
            return $foundEntity;
761
        }
762
763 96
        $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
764
765 96
        if ($assoc['isOwningSide']) {
766 32
            $isInverseSingleValued = $assoc['inversedBy'] && ! $targetClass->isCollectionValuedAssociation($assoc['inversedBy']);
767
768
            // Mark inverse side as fetched in the hints, otherwise the UoW would
769
            // try to load it in a separate query (remember: to-one inverse sides can not be lazy).
770 32
            $hints = [];
771
772 32
            if ($isInverseSingleValued) {
773
                $hints['fetched']["r"][$assoc['inversedBy']] = true;
774
            }
775
776
            /* 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...
777
            if ($this->em->getUnitOfWork()->isReadOnly($sourceEntity)) {
778
                $hints[Query::HINT_READ_ONLY] = true;
779
            }
780
            */
781
782 32
            $targetEntity = $this->load($identifier, null, $assoc, $hints);
783
784
            // Complete bidirectional association, if necessary
785 32
            if ($targetEntity !== null && $isInverseSingleValued) {
786
                $targetClass->reflFields[$assoc['inversedBy']]->setValue($targetEntity, $sourceEntity);
0 ignored issues
show
Accessing reflFields on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
787
            }
788
789 32
            return $targetEntity;
790
        }
791
792 64
        $sourceClass = $this->em->getClassMetadata($assoc['sourceEntity']);
793 64
        $owningAssoc = $targetClass->getAssociationMapping($assoc['mappedBy']);
794
795
        // TRICKY: since the association is specular source and target are flipped
796 64
        foreach ($owningAssoc['targetToSourceKeyColumns'] as $sourceKeyColumn => $targetKeyColumn) {
797 64
            if ( ! isset($sourceClass->fieldNames[$sourceKeyColumn])) {
798
                throw MappingException::joinColumnMustPointToMappedField(
799
                    $sourceClass->name, $sourceKeyColumn
800
                );
801
            }
802
803
            // unset the old value and set the new sql aliased value here. By definition
804
            // unset($identifier[$targetKeyColumn] works here with how UnitOfWork::createEntity() calls this method.
805 64
            $identifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
806 64
                $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
807
808 64
            unset($identifier[$targetKeyColumn]);
809
        }
810
811 64
        $targetEntity = $this->load($identifier, null, $assoc);
812
813 64
        if ($targetEntity !== null) {
814 16
            $targetClass->setFieldValue($targetEntity, $assoc['mappedBy'], $sourceEntity);
815
        }
816
817 64
        return $targetEntity;
818
    }
819
820
    /**
821
     * {@inheritdoc}
822
     */
823 17
    public function refresh(array $id, $entity, $lockMode = null)
824
    {
825 17
        $sql = $this->getSelectSQL($id, null, $lockMode);
826 17
        list($params, $types) = $this->expandParameters($id);
827 17
        $stmt = $this->conn->executeQuery($sql, $params, $types);
828
829 17
        $hydrator = $this->em->newHydrator(Query::HYDRATE_OBJECT);
830 17
        $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [Query::HINT_REFRESH => true]);
831 17
    }
832
833
    /**
834
     * {@inheritDoc}
835
     */
836 46
    public function count($criteria = [])
837
    {
838 46
        $sql = $this->getCountSQL($criteria);
839
840 46
        list($params, $types) = ($criteria instanceof Criteria)
841 43
            ? $this->expandCriteriaParameters($criteria)
842 46
            : $this->expandParameters($criteria);
843
844 46
        return (int) $this->conn->executeQuery($sql, $params, $types)->fetchColumn();
845
    }
846
847
    /**
848
     * {@inheritdoc}
849
     */
850 9
    public function loadCriteria(Criteria $criteria)
851
    {
852 9
        $orderBy = $criteria->getOrderings();
853 9
        $limit   = $criteria->getMaxResults();
854 9
        $offset  = $criteria->getFirstResult();
855 9
        $query   = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy);
856
857 7
        list($params, $types) = $this->expandCriteriaParameters($criteria);
858
859 7
        $stmt       = $this->conn->executeQuery($query, $params, $types);
860 7
        $hydrator   = $this->em->newHydrator(($this->currentPersisterContext->selectJoinSql) ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
861
862 7
        return $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [UnitOfWork::HINT_DEFEREAGERLOAD => true]
863
        );
864
    }
865
866
    /**
867
     * {@inheritdoc}
868
     */
869 56
    public function expandCriteriaParameters(Criteria $criteria)
870
    {
871 56
        $expression = $criteria->getWhereExpression();
872 56
        $sqlParams  = [];
873 56
        $sqlTypes   = [];
874
875 56
        if ($expression === null) {
876 2
            return [$sqlParams, $sqlTypes];
877
        }
878
879 55
        $valueVisitor = new SqlValueVisitor();
880
881 55
        $valueVisitor->dispatch($expression);
882
883 55
        list($params, $types) = $valueVisitor->getParamsAndTypes();
884
885 55
        foreach ($params as $param) {
886 51
            $sqlParams = array_merge($sqlParams, $this->getValues($param));
887
        }
888
889 55
        foreach ($types as $type) {
890 51
            list ($field, $value) = $type;
891 51
            $sqlTypes = array_merge($sqlTypes, $this->getTypes($field, $value, $this->class));
892
        }
893
894 55
        return [$sqlParams, $sqlTypes];
895
    }
896
897
    /**
898
     * {@inheritdoc}
899
     */
900 71
    public function loadAll(array $criteria = [], array $orderBy = null, $limit = null, $offset = null)
901
    {
902 71
        $this->switchPersisterContext($offset, $limit);
903
904 71
        $sql = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy);
905 67
        list($params, $types) = $this->expandParameters($criteria);
906 67
        $stmt = $this->conn->executeQuery($sql, $params, $types);
907
908 67
        $hydrator = $this->em->newHydrator(($this->currentPersisterContext->selectJoinSql) ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
909
910 67
        return $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [UnitOfWork::HINT_DEFEREAGERLOAD => true]
911
        );
912
    }
913
914
    /**
915
     * {@inheritdoc}
916
     */
917 8 View Code Duplication
    public function getManyToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null)
918
    {
919 8
        $this->switchPersisterContext($offset, $limit);
920
921 8
        $stmt = $this->getManyToManyStatement($assoc, $sourceEntity, $offset, $limit);
922
923 8
        return $this->loadArrayFromStatement($assoc, $stmt);
924
    }
925
926
    /**
927
     * Loads an array of entities from a given DBAL statement.
928
     *
929
     * @param array                    $assoc
930
     * @param \Doctrine\DBAL\Statement $stmt
931
     *
932
     * @return array
933
     */
934 13 View Code Duplication
    private function loadArrayFromStatement($assoc, $stmt)
935
    {
936 13
        $rsm    = $this->currentPersisterContext->rsm;
937 13
        $hints  = [UnitOfWork::HINT_DEFEREAGERLOAD => true];
938
939 13
        if (isset($assoc['indexBy'])) {
940 7
            $rsm = clone ($this->currentPersisterContext->rsm); // this is necessary because the "default rsm" should be changed.
941 7
            $rsm->addIndexBy('r', $assoc['indexBy']);
942
        }
943
944 13
        return $this->em->newHydrator(Query::HYDRATE_OBJECT)->hydrateAll($stmt, $rsm, $hints);
945
    }
946
947
    /**
948
     * Hydrates a collection from a given DBAL statement.
949
     *
950
     * @param array                    $assoc
951
     * @param \Doctrine\DBAL\Statement $stmt
952
     * @param PersistentCollection     $coll
953
     *
954
     * @return array
955
     */
956 144 View Code Duplication
    private function loadCollectionFromStatement($assoc, $stmt, $coll)
957
    {
958 144
        $rsm   = $this->currentPersisterContext->rsm;
959
        $hints = [
960 144
            UnitOfWork::HINT_DEFEREAGERLOAD => true,
961 144
            'collection' => $coll
962
        ];
963
964 144
        if (isset($assoc['indexBy'])) {
965 10
            $rsm = clone ($this->currentPersisterContext->rsm); // this is necessary because the "default rsm" should be changed.
966 10
            $rsm->addIndexBy('r', $assoc['indexBy']);
967
        }
968
969 144
        return $this->em->newHydrator(Query::HYDRATE_OBJECT)->hydrateAll($stmt, $rsm, $hints);
970
    }
971
972
    /**
973
     * {@inheritdoc}
974
     */
975 82
    public function loadManyToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll)
976
    {
977 82
        $stmt = $this->getManyToManyStatement($assoc, $sourceEntity);
978
979 82
        return $this->loadCollectionFromStatement($assoc, $stmt, $coll);
980
    }
981
982
    /**
983
     * @param array    $assoc
984
     * @param object   $sourceEntity
985
     * @param int|null $offset
986
     * @param int|null $limit
987
     *
988
     * @return \Doctrine\DBAL\Driver\Statement
989
     *
990
     * @throws \Doctrine\ORM\Mapping\MappingException
991
     */
992 89
    private function getManyToManyStatement(array $assoc, $sourceEntity, $offset = null, $limit = null)
993
    {
994 89
        $this->switchPersisterContext($offset, $limit);
995
996 89
        $sourceClass    = $this->em->getClassMetadata($assoc['sourceEntity']);
997 89
        $class          = $sourceClass;
998 89
        $association    = $assoc;
999 89
        $criteria       = [];
1000 89
        $parameters     = [];
1001
1002 89 View Code Duplication
        if ( ! $assoc['isOwningSide']) {
1003 12
            $class       = $this->em->getClassMetadata($assoc['targetEntity']);
1004 12
            $association = $class->associationMappings[$assoc['mappedBy']];
1005
        }
1006
1007 89
        $joinColumns = $assoc['isOwningSide']
1008 82
            ? $association['joinTable']['joinColumns']
1009 89
            : $association['joinTable']['inverseJoinColumns'];
1010
1011 89
        $quotedJoinTable = $this->quoteStrategy->getJoinTableName($association, $class, $this->platform);
1012
1013 89
        foreach ($joinColumns as $joinColumn) {
1014 89
            $sourceKeyColumn    = $joinColumn['referencedColumnName'];
1015 89
            $quotedKeyColumn    = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
1016
1017
            switch (true) {
1018 89
                case $sourceClass->containsForeignIdentifier:
1019 4
                    $field = $sourceClass->getFieldForColumn($sourceKeyColumn);
1020 4
                    $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
1021
1022 4 View Code Duplication
                    if (isset($sourceClass->associationMappings[$field])) {
1023 4
                        $value = $this->em->getUnitOfWork()->getEntityIdentifier($value);
1024 4
                        $value = $value[$this->em->getClassMetadata($sourceClass->associationMappings[$field]['targetEntity'])->identifier[0]];
1025
                    }
1026
1027 4
                    break;
1028
1029 87
                case isset($sourceClass->fieldNames[$sourceKeyColumn]):
1030 87
                    $field = $sourceClass->fieldNames[$sourceKeyColumn];
1031 87
                    $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
1032
1033 87
                    break;
1034
1035
                default:
1036
                    throw MappingException::joinColumnMustPointToMappedField(
1037
                        $sourceClass->name, $sourceKeyColumn
0 ignored issues
show
Accessing name on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
1038
                    );
1039
            }
1040
1041 89
            $criteria[$quotedJoinTable . '.' . $quotedKeyColumn] = $value;
1042 89
            $parameters[] = [
1043 89
                'value' => $value,
1044 89
                'field' => $field,
1045 89
                'class' => $sourceClass,
1046
            ];
1047
        }
1048
1049 89
        $sql = $this->getSelectSQL($criteria, $assoc, null, $limit, $offset);
1050 89
        list($params, $types) = $this->expandToManyParameters($parameters);
1051
1052 89
        return $this->conn->executeQuery($sql, $params, $types);
1053
    }
1054
1055
    /**
1056
     * {@inheritdoc}
1057
     */
1058 562
    public function getSelectSQL($criteria, $assoc = null, $lockMode = null, $limit = null, $offset = null, array $orderBy = null)
1059
    {
1060 562
        $this->switchPersisterContext($offset, $limit);
1061
1062 562
        $lockSql    = '';
1063 562
        $joinSql    = '';
1064 562
        $orderBySql = '';
1065
1066 562 View Code Duplication
        if ($assoc != null && $assoc['type'] == ClassMetadata::MANY_TO_MANY) {
1067 90
            $joinSql = $this->getSelectManyToManyJoinSQL($assoc);
1068
        }
1069
1070 562
        if (isset($assoc['orderBy'])) {
1071 5
            $orderBy = $assoc['orderBy'];
1072
        }
1073
1074 562
        if ($orderBy) {
1075 11
            $orderBySql = $this->getOrderBySQL($orderBy, $this->getSQLTableAlias($this->class->name));
1076
        }
1077
1078 560
        $conditionSql = ($criteria instanceof Criteria)
1079 9
            ? $this->getSelectConditionCriteriaSQL($criteria)
1080 558
            : $this->getSelectConditionSQL($criteria, $assoc);
1081
1082 View Code Duplication
        switch ($lockMode) {
1083 555
            case LockMode::PESSIMISTIC_READ:
1084
                $lockSql = ' ' . $this->platform->getReadLockSQL();
1085
                break;
1086
1087 555
            case LockMode::PESSIMISTIC_WRITE:
1088
                $lockSql = ' ' . $this->platform->getWriteLockSQL();
1089
                break;
1090
        }
1091
1092 555
        $columnList = $this->getSelectColumnsSQL();
1093 555
        $tableAlias = $this->getSQLTableAlias($this->class->name);
1094 555
        $filterSql  = $this->generateFilterConditionSQL($this->class, $tableAlias);
1095 555
        $tableName  = $this->quoteStrategy->getTableName($this->class, $this->platform);
1096
1097 555
        if ('' !== $filterSql) {
1098 12
            $conditionSql = $conditionSql
1099 11
                ? $conditionSql . ' AND ' . $filterSql
1100 12
                : $filterSql;
1101
        }
1102
1103 555
        $select = 'SELECT ' . $columnList;
1104 555
        $from   = ' FROM ' . $tableName . ' '. $tableAlias;
1105 555
        $join   = $this->currentPersisterContext->selectJoinSql . $joinSql;
1106 555
        $where  = ($conditionSql ? ' WHERE ' . $conditionSql : '');
1107 555
        $lock   = $this->platform->appendLockHint($from, $lockMode);
1108
        $query  = $select
1109 555
            . $lock
1110 555
            . $join
1111 555
            . $where
1112 555
            . $orderBySql;
1113
1114 555
        return $this->platform->modifyLimitQuery($query, $limit, $offset) . $lockSql;
1115
    }
1116
1117
    /**
1118
     * {@inheritDoc}
1119
     */
1120 41
    public function getCountSQL($criteria = [])
1121
    {
1122 41
        $tableName  = $this->quoteStrategy->getTableName($this->class, $this->platform);
1123 41
        $tableAlias = $this->getSQLTableAlias($this->class->name);
1124
1125 41
        $conditionSql = ($criteria instanceof Criteria)
1126 38
            ? $this->getSelectConditionCriteriaSQL($criteria)
1127 41
            : $this->getSelectConditionSQL($criteria);
1128
1129 41
        $filterSql = $this->generateFilterConditionSQL($this->class, $tableAlias);
1130
1131 41
        if ('' !== $filterSql) {
1132 2
            $conditionSql = $conditionSql
1133 2
                ? $conditionSql . ' AND ' . $filterSql
1134 2
                : $filterSql;
1135
        }
1136
1137
        $sql = 'SELECT COUNT(*) '
1138 41
            . 'FROM ' . $tableName . ' ' . $tableAlias
1139 41
            . (empty($conditionSql) ? '' : ' WHERE ' . $conditionSql);
1140
1141 41
        return $sql;
1142
    }
1143
1144
    /**
1145
     * Gets the ORDER BY SQL snippet for ordered collections.
1146
     *
1147
     * @param array  $orderBy
1148
     * @param string $baseTableAlias
1149
     *
1150
     * @return string
1151
     *
1152
     * @throws \Doctrine\ORM\ORMException
1153
     */
1154 12
    protected final function getOrderBySQL(array $orderBy, $baseTableAlias)
1155
    {
1156 12
        $orderByList = [];
1157
1158 12
        foreach ($orderBy as $fieldName => $orientation) {
1159
1160 12
            $orientation = strtoupper(trim($orientation));
1161
1162 12
            if ($orientation != 'ASC' && $orientation != 'DESC') {
1163 1
                throw ORMException::invalidOrientation($this->class->name, $fieldName);
1164
            }
1165
1166 11
            if (isset($this->class->fieldMappings[$fieldName])) {
1167 9
                $tableAlias = isset($this->class->fieldMappings[$fieldName]['inherited'])
1168
                    ? $this->getSQLTableAlias($this->class->fieldMappings[$fieldName]['inherited'])
1169 9
                    : $baseTableAlias;
1170
1171 9
                $columnName    = $this->quoteStrategy->getColumnName($fieldName, $this->class, $this->platform);
1172 9
                $orderByList[] = $tableAlias . '.' . $columnName . ' ' . $orientation;
1173
1174 9
                continue;
1175
            }
1176
1177 2
            if (isset($this->class->associationMappings[$fieldName])) {
1178
1179 2
                if ( ! $this->class->associationMappings[$fieldName]['isOwningSide']) {
1180 1
                    throw ORMException::invalidFindByInverseAssociation($this->class->name, $fieldName);
1181
                }
1182
1183 1
                $tableAlias = isset($this->class->associationMappings[$fieldName]['inherited'])
1184
                    ? $this->getSQLTableAlias($this->class->associationMappings[$fieldName]['inherited'])
1185 1
                    : $baseTableAlias;
1186
1187 1
                foreach ($this->class->associationMappings[$fieldName]['joinColumns'] as $joinColumn) {
1188 1
                    $columnName    = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
1189 1
                    $orderByList[] = $tableAlias . '.' . $columnName . ' ' . $orientation;
1190
                }
1191
1192 1
                continue;
1193
            }
1194
1195
            throw ORMException::unrecognizedField($fieldName);
1196
        }
1197
1198 10
        return ' ORDER BY ' . implode(', ', $orderByList);
1199
    }
1200
1201
    /**
1202
     * Gets the SQL fragment with the list of columns to select when querying for
1203
     * an entity in this persister.
1204
     *
1205
     * Subclasses should override this method to alter or change the select column
1206
     * list SQL fragment. Note that in the implementation of BasicEntityPersister
1207
     * the resulting SQL fragment is generated only once and cached in {@link selectColumnListSql}.
1208
     * Subclasses may or may not do the same.
1209
     *
1210
     * @return string The SQL fragment.
1211
     */
1212 556
    protected function getSelectColumnsSQL()
1213
    {
1214 556
        if ($this->currentPersisterContext->selectColumnListSql !== null) {
1215 146
            return $this->currentPersisterContext->selectColumnListSql;
1216
        }
1217
1218 556
        $columnList = [];
1219 556
        $this->currentPersisterContext->rsm->addEntityResult($this->class->name, 'r'); // r for root
1220
1221
        // Add regular columns to select list
1222 556
        foreach ($this->class->fieldNames as $field) {
1223 554
            $columnList[] = $this->getSelectColumnSQL($field, $this->class);
1224
        }
1225
1226 556
        $this->currentPersisterContext->selectJoinSql    = '';
1227 556
        $eagerAliasCounter      = 0;
1228
1229 556
        foreach ($this->class->associationMappings as $assocField => $assoc) {
1230 493
            $assocColumnSQL = $this->getSelectColumnAssociationSQL($assocField, $assoc, $this->class);
1231
1232 493
            if ($assocColumnSQL) {
1233 415
                $columnList[] = $assocColumnSQL;
1234
            }
1235
1236 493
            $isAssocToOneInverseSide = $assoc['type'] & ClassMetadata::TO_ONE && ! $assoc['isOwningSide'];
1237 493
            $isAssocFromOneEager     = $assoc['type'] !== ClassMetadata::MANY_TO_MANY && $assoc['fetch'] === ClassMetadata::FETCH_EAGER;
1238
1239 493
            if ( ! ($isAssocFromOneEager || $isAssocToOneInverseSide)) {
1240 471
                continue;
1241
            }
1242
1243 191
            if ((($assoc['type'] & ClassMetadata::TO_MANY) > 0) && $this->currentPersisterContext->handlesLimits) {
1244 3
                continue;
1245
            }
1246
1247 188
            $eagerEntity = $this->em->getClassMetadata($assoc['targetEntity']);
1248
1249 188
            if ($eagerEntity->inheritanceType != ClassMetadata::INHERITANCE_TYPE_NONE) {
1250 5
                continue; // now this is why you shouldn't use inheritance
1251
            }
1252
1253 183
            $assocAlias = 'e' . ($eagerAliasCounter++);
1254 183
            $this->currentPersisterContext->rsm->addJoinedEntityResult($assoc['targetEntity'], $assocAlias, 'r', $assocField);
1255
1256 183
            foreach ($eagerEntity->fieldNames as $field) {
1257 181
                $columnList[] = $this->getSelectColumnSQL($field, $eagerEntity, $assocAlias);
1258
            }
1259
1260 183
            foreach ($eagerEntity->associationMappings as $eagerAssocField => $eagerAssoc) {
1261 180
                $eagerAssocColumnSQL = $this->getSelectColumnAssociationSQL(
1262 180
                    $eagerAssocField, $eagerAssoc, $eagerEntity, $assocAlias
1263
                );
1264
1265 180
                if ($eagerAssocColumnSQL) {
1266 180
                    $columnList[] = $eagerAssocColumnSQL;
1267
                }
1268
            }
1269
1270 183
            $association    = $assoc;
1271 183
            $joinCondition  = [];
1272
1273 183
            if (isset($assoc['indexBy'])) {
1274 1
                $this->currentPersisterContext->rsm->addIndexBy($assocAlias, $assoc['indexBy']);
1275
            }
1276
1277 183 View Code Duplication
            if ( ! $assoc['isOwningSide']) {
1278 176
                $eagerEntity = $this->em->getClassMetadata($assoc['targetEntity']);
1279 176
                $association = $eagerEntity->getAssociationMapping($assoc['mappedBy']);
1280
            }
1281
1282 183
            $joinTableAlias = $this->getSQLTableAlias($eagerEntity->name, $assocAlias);
1283 183
            $joinTableName  = $this->quoteStrategy->getTableName($eagerEntity, $this->platform);
1284
1285 183
            if ($assoc['isOwningSide']) {
1286 13
                $tableAlias           = $this->getSQLTableAlias($association['targetEntity'], $assocAlias);
1287 13
                $this->currentPersisterContext->selectJoinSql .= ' ' . $this->getJoinSQLForJoinColumns($association['joinColumns']);
1288
1289 13 View Code Duplication
                foreach ($association['joinColumns'] as $joinColumn) {
1290 13
                    $sourceCol       = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
1291 13
                    $targetCol       = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform);
1292 13
                    $joinCondition[] = $this->getSQLTableAlias($association['sourceEntity'])
1293 13
                                        . '.' . $sourceCol . ' = ' . $tableAlias . '.' . $targetCol;
1294
                }
1295
1296
                // Add filter SQL
1297 13
                if ($filterSql = $this->generateFilterConditionSQL($eagerEntity, $tableAlias)) {
1298 13
                    $joinCondition[] = $filterSql;
1299
                }
1300
1301
            } else {
1302
1303 176
                $this->currentPersisterContext->selectJoinSql .= ' LEFT JOIN';
1304
1305 176 View Code Duplication
                foreach ($association['joinColumns'] as $joinColumn) {
1306 176
                    $sourceCol       = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
1307 176
                    $targetCol       = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform);
1308
1309 176
                    $joinCondition[] = $this->getSQLTableAlias($association['sourceEntity'], $assocAlias) . '.' . $sourceCol . ' = '
1310 176
                        . $this->getSQLTableAlias($association['targetEntity']) . '.' . $targetCol;
1311
                }
1312
            }
1313
1314 183
            $this->currentPersisterContext->selectJoinSql .= ' ' . $joinTableName . ' ' . $joinTableAlias . ' ON ';
1315 183
            $this->currentPersisterContext->selectJoinSql .= implode(' AND ', $joinCondition);
1316
        }
1317
1318 556
        $this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList);
1319
1320 556
        return $this->currentPersisterContext->selectColumnListSql;
1321
    }
1322
1323
    /**
1324
     * Gets the SQL join fragment used when selecting entities from an association.
1325
     *
1326
     * @param string        $field
1327
     * @param array         $assoc
1328
     * @param ClassMetadata $class
1329
     * @param string        $alias
1330
     *
1331
     * @return string
1332
     */
1333 493
    protected function getSelectColumnAssociationSQL($field, $assoc, ClassMetadata $class, $alias = 'r')
1334
    {
1335 493
        if ( ! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) ) {
1336 394
            return '';
1337
        }
1338
1339 432
        $columnList    = [];
1340 432
        $targetClass   = $this->em->getClassMetadata($assoc['targetEntity']);
1341 432
        $isIdentifier  = isset($assoc['id']) && $assoc['id'] === true;
1342 432
        $sqlTableAlias = $this->getSQLTableAlias($class->name, ($alias == 'r' ? '' : $alias));
1343
1344 432
        foreach ($assoc['joinColumns'] as $joinColumn) {
1345 432
            $quotedColumn     = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
1346 432
            $resultColumnName = $this->getSQLColumnAlias($joinColumn['name']);
1347 432
            $type             = PersisterHelper::getTypeOfColumn($joinColumn['referencedColumnName'], $targetClass, $this->em);
1348
1349 432
            $this->currentPersisterContext->rsm->addMetaResult($alias, $resultColumnName, $joinColumn['name'], $isIdentifier, $type);
1350
1351 432
            $columnList[] = sprintf('%s.%s AS %s', $sqlTableAlias, $quotedColumn, $resultColumnName);
1352
        }
1353
1354 432
        return implode(', ', $columnList);
1355
    }
1356
1357
    /**
1358
     * Gets the SQL join fragment used when selecting entities from a
1359
     * many-to-many association.
1360
     *
1361
     * @param array $manyToMany
1362
     *
1363
     * @return string
1364
     */
1365 92
    protected function getSelectManyToManyJoinSQL(array $manyToMany)
1366
    {
1367 92
        $conditions         = [];
1368 92
        $association        = $manyToMany;
1369 92
        $sourceTableAlias   = $this->getSQLTableAlias($this->class->name);
1370
1371 92 View Code Duplication
        if ( ! $manyToMany['isOwningSide']) {
1372 13
            $targetEntity   = $this->em->getClassMetadata($manyToMany['targetEntity']);
1373 13
            $association    = $targetEntity->associationMappings[$manyToMany['mappedBy']];
1374
        }
1375
1376 92
        $joinTableName  = $this->quoteStrategy->getJoinTableName($association, $this->class, $this->platform);
1377 92
        $joinColumns    = ($manyToMany['isOwningSide'])
1378 84
            ? $association['joinTable']['inverseJoinColumns']
1379 92
            : $association['joinTable']['joinColumns'];
1380
1381 92
        foreach ($joinColumns as $joinColumn) {
1382 92
            $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
1383 92
            $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform);
1384 92
            $conditions[]       = $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableName . '.' . $quotedSourceColumn;
1385
        }
1386
1387 92
        return ' INNER JOIN ' . $joinTableName . ' ON ' . implode(' AND ', $conditions);
1388
    }
1389
1390
    /**
1391
     * {@inheritdoc}
1392
     */
1393 1034
    public function getInsertSQL()
1394
    {
1395 1034
        if ($this->insertSql !== null) {
1396 89
            return $this->insertSql;
1397
        }
1398
1399 1034
        $columns   = $this->getInsertColumnList();
1400 1034
        $tableName = $this->quoteStrategy->getTableName($this->class, $this->platform);
1401
1402 1034
        if (empty($columns)) {
1403 112
            $identityColumn  = $this->quoteStrategy->getColumnName($this->class->identifier[0], $this->class, $this->platform);
1404 112
            $this->insertSql = $this->platform->getEmptyIdentityInsertSQL($tableName, $identityColumn);
1405
1406 112
            return $this->insertSql;
1407
        }
1408
1409 1009
        $values  = [];
1410 1009
        $columns = array_unique($columns);
1411
1412 1009
        foreach ($columns as $column) {
1413 1009
            $placeholder = '?';
1414
1415 1009
            if (isset($this->class->fieldNames[$column])
1416 1009
                && isset($this->columnTypes[$this->class->fieldNames[$column]])
1417 1009
                && isset($this->class->fieldMappings[$this->class->fieldNames[$column]]['requireSQLConversion'])) {
1418 6
                $type        = Type::getType($this->columnTypes[$this->class->fieldNames[$column]]);
1419 6
                $placeholder = $type->convertToDatabaseValueSQL('?', $this->platform);
1420
            }
1421
1422 1009
            $values[] = $placeholder;
1423
        }
1424
1425 1009
        $columns = implode(', ', $columns);
1426 1009
        $values  = implode(', ', $values);
1427
1428 1009
        $this->insertSql = sprintf('INSERT INTO %s (%s) VALUES (%s)', $tableName, $columns, $values);
1429
1430 1009
        return $this->insertSql;
1431
    }
1432
1433
    /**
1434
     * Gets the list of columns to put in the INSERT SQL statement.
1435
     *
1436
     * Subclasses should override this method to alter or change the list of
1437
     * columns placed in the INSERT statements used by the persister.
1438
     *
1439
     * @return array The list of columns.
1440
     */
1441 959
    protected function getInsertColumnList()
1442
    {
1443 959
        $columns = [];
1444
1445 959
        foreach ($this->class->reflFields as $name => $field) {
1446 959
            if ($this->class->isVersioned && $this->class->versionField == $name) {
1447 200
                continue;
1448
            }
1449
1450 959
            if (isset($this->class->embeddedClasses[$name])) {
1451 8
                continue;
1452
            }
1453
1454 959
            if (isset($this->class->associationMappings[$name])) {
1455 840
                $assoc = $this->class->associationMappings[$name];
1456
1457 840
                if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
1458 790 View Code Duplication
                    foreach ($assoc['joinColumns'] as $joinColumn) {
1459 790
                        $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
1460
                    }
1461
                }
1462
1463 840
                continue;
1464
            }
1465
1466 959
            if (! $this->class->isIdGeneratorIdentity() || $this->class->identifier[0] != $name) {
1467 889
                $columns[]                = $this->quoteStrategy->getColumnName($name, $this->class, $this->platform);
1468 959
                $this->columnTypes[$name] = $this->class->fieldMappings[$name]['type'];
1469
            }
1470
        }
1471
1472 959
        return $columns;
1473
    }
1474
1475
    /**
1476
     * Gets the SQL snippet of a qualified column name for the given field name.
1477
     *
1478
     * @param string        $field The field name.
1479
     * @param ClassMetadata $class The class that declares this field. The table this class is
1480
     *                             mapped to must own the column for the given field.
1481
     * @param string        $alias
1482
     *
1483
     * @return string
1484
     */
1485 519 View Code Duplication
    protected function getSelectColumnSQL($field, ClassMetadata $class, $alias = 'r')
1486
    {
1487 519
        $root         = $alias == 'r' ? '' : $alias ;
1488 519
        $tableAlias   = $this->getSQLTableAlias($class->name, $root);
1489 519
        $fieldMapping = $class->fieldMappings[$field];
1490 519
        $sql          = sprintf('%s.%s', $tableAlias, $this->quoteStrategy->getColumnName($field, $class, $this->platform));
1491 519
        $columnAlias  = $this->getSQLColumnAlias($fieldMapping['columnName']);
1492
1493 519
        $this->currentPersisterContext->rsm->addFieldResult($alias, $columnAlias, $field);
1494
1495 519
        if (isset($fieldMapping['requireSQLConversion'])) {
1496 3
            $type = Type::getType($fieldMapping['type']);
1497 3
            $sql  = $type->convertToPHPValueSQL($sql, $this->platform);
1498
        }
1499
1500 519
        return $sql . ' AS ' . $columnAlias;
1501
    }
1502
1503
    /**
1504
     * Gets the SQL table alias for the given class name.
1505
     *
1506
     * @param string $className
1507
     * @param string $assocName
1508
     *
1509
     * @return string The SQL table alias.
1510
     *
1511
     * @todo Reconsider. Binding table aliases to class names is not such a good idea.
1512
     */
1513 625
    protected function getSQLTableAlias($className, $assocName = '')
1514
    {
1515 625
        if ($assocName) {
1516 183
            $className .= '#' . $assocName;
1517
        }
1518
1519 625
        if (isset($this->currentPersisterContext->sqlTableAliases[$className])) {
1520 621
            return $this->currentPersisterContext->sqlTableAliases[$className];
1521
        }
1522
1523 625
        $tableAlias = 't' . $this->currentPersisterContext->sqlAliasCounter++;
1524
1525 625
        $this->currentPersisterContext->sqlTableAliases[$className] = $tableAlias;
1526
1527 625
        return $tableAlias;
1528
    }
1529
1530
    /**
1531
     * {@inheritdoc}
1532
     */
1533
    public function lock(array $criteria, $lockMode)
1534
    {
1535
        $lockSql      = '';
1536
        $conditionSql = $this->getSelectConditionSQL($criteria);
1537
1538 View Code Duplication
        switch ($lockMode) {
1539
            case LockMode::PESSIMISTIC_READ:
1540
                $lockSql = $this->platform->getReadLockSQL();
1541
1542
                break;
1543
            case LockMode::PESSIMISTIC_WRITE:
1544
1545
                $lockSql = $this->platform->getWriteLockSQL();
1546
                break;
1547
        }
1548
1549
        $lock  = $this->getLockTablesSql($lockMode);
1550
        $where = ($conditionSql ? ' WHERE ' . $conditionSql : '') . ' ';
1551
        $sql = 'SELECT 1 '
1552
             . $lock
1553
             . $where
1554
             . $lockSql;
1555
1556
        list($params, $types) = $this->expandParameters($criteria);
1557
1558
        $this->conn->executeQuery($sql, $params, $types);
1559
    }
1560
1561
    /**
1562
     * Gets the FROM and optionally JOIN conditions to lock the entity managed by this persister.
1563
     *
1564
     * @param integer $lockMode One of the Doctrine\DBAL\LockMode::* constants.
1565
     *
1566
     * @return string
1567
     */
1568 13
    protected function getLockTablesSql($lockMode)
1569
    {
1570 13
        return $this->platform->appendLockHint(
1571
            'FROM '
1572 13
            . $this->quoteStrategy->getTableName($this->class, $this->platform) . ' '
1573 13
            . $this->getSQLTableAlias($this->class->name),
1574 13
            $lockMode
1575
        );
1576
    }
1577
1578
    /**
1579
     * Gets the Select Where Condition from a Criteria object.
1580
     *
1581
     * @param \Doctrine\Common\Collections\Criteria $criteria
1582
     *
1583
     * @return string
1584
     */
1585 58
    protected function getSelectConditionCriteriaSQL(Criteria $criteria)
1586
    {
1587 58
        $expression = $criteria->getWhereExpression();
1588
1589 58
        if ($expression === null) {
1590 2
            return '';
1591
        }
1592
1593 57
        $visitor = new SqlExpressionVisitor($this, $this->class);
1594
1595 57
        return $visitor->dispatch($expression);
1596
    }
1597
1598
    /**
1599
     * {@inheritdoc}
1600
     */
1601 604
    public function getSelectConditionStatementSQL($field, $value, $assoc = null, $comparison = null)
1602
    {
1603 604
        $selectedColumns = [];
1604 604
        $columns         = $this->getSelectConditionStatementColumnSQL($field, $assoc);
1605
1606 600
        if (count($columns) > 1 && $comparison === Comparison::IN) {
1607
            /*
1608
             *  @todo try to support multi-column IN expressions.
1609
             *  Example: (col1, col2) IN (('val1A', 'val2A'), ('val1B', 'val2B'))
1610
             */
1611 1
            throw ORMException::cantUseInOperatorOnCompositeKeys();
1612
        }
1613
1614 599
        foreach ($columns as $column) {
1615 599
            $placeholder = '?';
1616
1617 599 View Code Duplication
            if (isset($this->class->fieldMappings[$field]['requireSQLConversion'])) {
1618 1
                $type        = Type::getType($this->class->fieldMappings[$field]['type']);
1619 1
                $placeholder = $type->convertToDatabaseValueSQL($placeholder, $this->platform);
1620
            }
1621
1622 599
            if (null !== $comparison) {
1623
                // special case null value handling
1624 61 View Code Duplication
                if (($comparison === Comparison::EQ || $comparison === Comparison::IS) && null ===$value) {
1625 6
                    $selectedColumns[] = $column . ' IS NULL';
1626
1627 6
                    continue;
1628
                }
1629
1630 55 View Code Duplication
                if ($comparison === Comparison::NEQ && null === $value) {
1631 3
                    $selectedColumns[] = $column . ' IS NOT NULL';
1632
1633 3
                    continue;
1634
                }
1635
1636 52
                $selectedColumns[] = $column . ' ' . sprintf(self::$comparisonMap[$comparison], $placeholder);
1637
1638 52
                continue;
1639
            }
1640
1641 571
            if (is_array($value)) {
1642 14
                $in = sprintf('%s IN (%s)', $column, $placeholder);
1643
1644 14
                if (false !== array_search(null, $value, true)) {
1645 4
                    $selectedColumns[] = sprintf('(%s OR %s IS NULL)', $in, $column);
1646
1647 4
                    continue;
1648
                }
1649
1650 10
                $selectedColumns[] = $in;
1651
1652 10
                continue;
1653
            }
1654
1655 560
            if (null === $value) {
1656 9
                $selectedColumns[] = sprintf('%s IS NULL', $column);
1657
1658 9
                continue;
1659
            }
1660
1661 552
            $selectedColumns[] = sprintf('%s = %s', $column, $placeholder);
1662
        }
1663
1664 599
        return implode(' AND ', $selectedColumns);
1665
    }
1666
1667
    /**
1668
     * Builds the left-hand-side of a where condition statement.
1669
     *
1670
     * @param string     $field
1671
     * @param array|null $assoc
1672
     *
1673
     * @return string[]
1674
     *
1675
     * @throws \Doctrine\ORM\ORMException
1676
     */
1677 604
    private function getSelectConditionStatementColumnSQL($field, $assoc = null)
1678
    {
1679 604
        if (isset($this->class->fieldMappings[$field])) {
1680 508
            $className = (isset($this->class->fieldMappings[$field]['inherited']))
1681 54
                ? $this->class->fieldMappings[$field]['inherited']
1682 508
                : $this->class->name;
1683
1684 508
            return [$this->getSQLTableAlias($className) . '.' . $this->quoteStrategy->getColumnName($field, $this->class, $this->platform)];
1685
        }
1686
1687 295
        if (isset($this->class->associationMappings[$field])) {
1688 148
            $association = $this->class->associationMappings[$field];
1689
            // Many-To-Many requires join table check for joinColumn
1690 148
            $columns = [];
1691 148
            $class   = $this->class;
1692
1693 148
            if ($association['type'] === ClassMetadata::MANY_TO_MANY) {
1694 3
                if ( ! $association['isOwningSide']) {
1695 2
                    $association = $assoc;
1696
                }
1697
1698 3
                $joinTableName = $this->quoteStrategy->getJoinTableName($association, $class, $this->platform);
1699 3
                $joinColumns   = $assoc['isOwningSide']
1700 2
                    ? $association['joinTable']['joinColumns']
1701 3
                    : $association['joinTable']['inverseJoinColumns'];
1702
1703
1704 3
                foreach ($joinColumns as $joinColumn) {
1705 3
                    $columns[] = $joinTableName . '.' . $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
1706
                }
1707
1708
            } else {
1709 146
                if ( ! $association['isOwningSide']) {
1710 1
                    throw ORMException::invalidFindByInverseAssociation($this->class->name, $field);
1711
                }
1712
1713 145
                $className  = (isset($association['inherited']))
1714 11
                    ? $association['inherited']
1715 145
                    : $this->class->name;
1716
1717 145 View Code Duplication
                foreach ($association['joinColumns'] as $joinColumn) {
1718 145
                    $columns[] = $this->getSQLTableAlias($className) . '.' . $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
1719
                }
1720
            }
1721 147
            return $columns;
1722
        }
1723
1724 163
        if ($assoc !== null && strpos($field, " ") === false && strpos($field, "(") === false) {
1725
            // very careless developers could potentially open up this normally hidden api for userland attacks,
1726
            // therefore checking for spaces and function calls which are not allowed.
1727
1728
            // found a join column condition, not really a "field"
1729 160
            return [$field];
1730
        }
1731
1732 3
        throw ORMException::unrecognizedField($field);
1733
    }
1734
1735
    /**
1736
     * Gets the conditional SQL fragment used in the WHERE clause when selecting
1737
     * entities in this persister.
1738
     *
1739
     * Subclasses are supposed to override this method if they intend to change
1740
     * or alter the criteria by which entities are selected.
1741
     *
1742
     * @param array      $criteria
1743
     * @param array|null $assoc
1744
     *
1745
     * @return string
1746
     */
1747 598
    protected function getSelectConditionSQL(array $criteria, $assoc = null)
1748
    {
1749 598
        $conditions = [];
1750
1751 598
        foreach ($criteria as $field => $value) {
1752 573
            $conditions[] = $this->getSelectConditionStatementSQL($field, $value, $assoc);
1753
        }
1754
1755 595
        return implode(' AND ', $conditions);
1756
    }
1757
1758
    /**
1759
     * {@inheritdoc}
1760
     */
1761 5 View Code Duplication
    public function getOneToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null)
1762
    {
1763 5
        $this->switchPersisterContext($offset, $limit);
1764
1765 5
        $stmt = $this->getOneToManyStatement($assoc, $sourceEntity, $offset, $limit);
1766
1767 5
        return $this->loadArrayFromStatement($assoc, $stmt);
1768
    }
1769
1770
    /**
1771
     * {@inheritdoc}
1772
     */
1773 76
    public function loadOneToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll)
1774
    {
1775 76
        $stmt = $this->getOneToManyStatement($assoc, $sourceEntity);
1776
1777 76
        return $this->loadCollectionFromStatement($assoc, $stmt, $coll);
1778
    }
1779
1780
    /**
1781
     * Builds criteria and execute SQL statement to fetch the one to many entities from.
1782
     *
1783
     * @param array    $assoc
1784
     * @param object   $sourceEntity
1785
     * @param int|null $offset
1786
     * @param int|null $limit
1787
     *
1788
     * @return \Doctrine\DBAL\Statement
1789
     */
1790 81
    private function getOneToManyStatement(array $assoc, $sourceEntity, $offset = null, $limit = null)
1791
    {
1792 81
        $this->switchPersisterContext($offset, $limit);
1793
1794 81
        $criteria    = [];
1795 81
        $parameters  = [];
1796 81
        $owningAssoc = $this->class->associationMappings[$assoc['mappedBy']];
1797 81
        $sourceClass = $this->em->getClassMetadata($assoc['sourceEntity']);
1798 81
        $tableAlias  = $this->getSQLTableAlias($owningAssoc['inherited'] ?? $this->class->name);
1799
1800 81
        foreach ($owningAssoc['targetToSourceKeyColumns'] as $sourceKeyColumn => $targetKeyColumn) {
1801 81
            if ($sourceClass->containsForeignIdentifier) {
1802 3
                $field = $sourceClass->getFieldForColumn($sourceKeyColumn);
1803 3
                $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
1804
1805 3 View Code Duplication
                if (isset($sourceClass->associationMappings[$field])) {
1806 3
                    $value = $this->em->getUnitOfWork()->getEntityIdentifier($value);
1807 3
                    $value = $value[$this->em->getClassMetadata($sourceClass->associationMappings[$field]['targetEntity'])->identifier[0]];
1808
                }
1809
1810 3
                $criteria[$tableAlias . "." . $targetKeyColumn] = $value;
1811 3
                $parameters[]                                   = [
1812 3
                    'value' => $value,
1813 3
                    'field' => $field,
1814 3
                    'class' => $sourceClass,
1815
                ];
1816
1817 3
                continue;
1818
            }
1819
1820 79
            $field = $sourceClass->fieldNames[$sourceKeyColumn];
1821 79
            $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
1822
1823 79
            $criteria[$tableAlias . "." . $targetKeyColumn] = $value;
1824 79
            $parameters[] = [
1825 79
                'value' => $value,
1826 79
                'field' => $field,
1827 79
                'class' => $sourceClass,
1828
            ];
1829
1830
        }
1831
1832 81
        $sql                  = $this->getSelectSQL($criteria, $assoc, null, $limit, $offset);
1833 81
        list($params, $types) = $this->expandToManyParameters($parameters);
1834
1835 81
        return $this->conn->executeQuery($sql, $params, $types);
1836
    }
1837
1838
    /**
1839
     * {@inheritdoc}
1840
     */
1841 574
    public function expandParameters($criteria)
1842
    {
1843 574
        $params = [];
1844 574
        $types  = [];
1845
1846 574
        foreach ($criteria as $field => $value) {
1847 549
            if ($value === null) {
1848 3
                continue; // skip null values.
1849
            }
1850
1851 547
            $types  = array_merge($types, $this->getTypes($field, $value, $this->class));
1852 547
            $params = array_merge($params, $this->getValues($value));
1853
        }
1854
1855 574
        return [$params, $types];
1856
    }
1857
1858
    /**
1859
     * Expands the parameters from the given criteria and use the correct binding types if found,
1860
     * specialized for OneToMany or ManyToMany associations.
1861
     *
1862
     * @param mixed[][] $criteria an array of arrays containing following:
1863
     *                             - field to which each criterion will be bound
1864
     *                             - value to be bound
1865
     *                             - class to which the field belongs to
1866
     *
1867
     *
1868
     * @return array
1869
     */
1870 156
    private function expandToManyParameters($criteria)
1871
    {
1872 156
        $params = [];
1873 156
        $types  = [];
1874
1875 156
        foreach ($criteria as $criterion) {
1876 156
            if ($criterion['value'] === null) {
1877 6
                continue; // skip null values.
1878
            }
1879
1880 150
            $types  = array_merge($types, $this->getTypes($criterion['field'], $criterion['value'], $criterion['class']));
1881 150
            $params = array_merge($params, $this->getValues($criterion['value']));
1882
        }
1883
1884 156
        return [$params, $types];
1885
    }
1886
1887
    /**
1888
     * Infers field types to be used by parameter type casting.
1889
     *
1890
     * @param string        $field
1891
     * @param mixed         $value
1892
     * @param ClassMetadata $class
1893
     *
1894
     * @return array
1895
     *
1896
     * @throws \Doctrine\ORM\Query\QueryException
1897
     */
1898 706
    private function getTypes($field, $value, ClassMetadata $class)
1899
    {
1900 706
        $types = [];
1901
1902
        switch (true) {
1903 706
            case (isset($class->fieldMappings[$field])):
1904 646
                $types = array_merge($types, [$class->fieldMappings[$field]['type']]);
1905 646
                break;
1906
1907 147
            case (isset($class->associationMappings[$field])):
1908 146
                $assoc = $class->associationMappings[$field];
1909 146
                $class = $this->em->getClassMetadata($assoc['targetEntity']);
1910
1911 146 View Code Duplication
                if (! $assoc['isOwningSide']) {
1912 2
                    $assoc = $class->associationMappings[$assoc['mappedBy']];
1913 2
                    $class = $this->em->getClassMetadata($assoc['targetEntity']);
1914
                }
1915
1916 146
                $columns = $assoc['type'] === ClassMetadata::MANY_TO_MANY
1917 3
                    ? $assoc['relationToTargetKeyColumns']
1918 146
                    : $assoc['sourceToTargetKeyColumns'];
1919
1920 146
                foreach ($columns as $column){
1921 146
                    $types[] = PersisterHelper::getTypeOfColumn($column, $class, $this->em);
1922
                }
1923 146
                break;
1924
1925
            default:
1926 1
                $types[] = null;
1927 1
                break;
1928
        }
1929
1930 706
        if (is_array($value)) {
1931 16
            return array_map(function ($type) {
1932 16
                $type = Type::getType($type);
1933
1934 16
                return $type->getBindingType() + Connection::ARRAY_PARAM_OFFSET;
1935 16
            }, $types);
1936
        }
1937
1938 696
        return $types;
1939
    }
1940
1941
    /**
1942
     * Retrieves the parameters that identifies a value.
1943
     *
1944
     * @param mixed $value
1945
     *
1946
     * @return array
1947
     */
1948 581
    private function getValues($value)
1949
    {
1950 581
        if (is_array($value)) {
1951 16
            $newValue = [];
1952
1953 16
            foreach ($value as $itemValue) {
1954 16
                $newValue = array_merge($newValue, $this->getValues($itemValue));
1955
            }
1956
1957 16
            return [$newValue];
1958
        }
1959
1960 581
        if (is_object($value) && $this->em->getMetadataFactory()->hasMetadataFor(ClassUtils::getClass($value))) {
1961 44
            $class = $this->em->getClassMetadata(get_class($value));
1962 44
            if ($class->isIdentifierComposite) {
1963 3
                $newValue = [];
1964
1965 3
                foreach ($class->getIdentifierValues($value) as $innerValue) {
1966 3
                    $newValue = array_merge($newValue, $this->getValues($innerValue));
1967
                }
1968
1969 3
                return $newValue;
1970
            }
1971
        }
1972
1973 581
        return [$this->getIndividualValue($value)];
1974
    }
1975
1976
    /**
1977
     * Retrieves an individual parameter value.
1978
     *
1979
     * @param mixed $value
1980
     *
1981
     * @return mixed
1982
     */
1983 581
    private function getIndividualValue($value)
1984
    {
1985 581
        if ( ! is_object($value) || ! $this->em->getMetadataFactory()->hasMetadataFor(ClassUtils::getClass($value))) {
1986 578
            return $value;
1987
        }
1988
1989 44
        return $this->em->getUnitOfWork()->getSingleIdentifierValue($value);
1990
    }
1991
1992
    /**
1993
     * {@inheritdoc}
1994
     */
1995 14
    public function exists($entity, Criteria $extraConditions = null)
1996
    {
1997 14
        $criteria = $this->class->getIdentifierValues($entity);
1998
1999 14
        if ( ! $criteria) {
2000 2
            return false;
2001
        }
2002
2003 13
        $alias = $this->getSQLTableAlias($this->class->name);
2004
2005
        $sql = 'SELECT 1 '
2006 13
             . $this->getLockTablesSql(null)
2007 13
             . ' WHERE ' . $this->getSelectConditionSQL($criteria);
2008
2009 13
        list($params, $types) = $this->expandParameters($criteria);
2010
2011 13
        if (null !== $extraConditions) {
2012 9
            $sql                                 .= ' AND ' . $this->getSelectConditionCriteriaSQL($extraConditions);
2013 9
            list($criteriaParams, $criteriaTypes) = $this->expandCriteriaParameters($extraConditions);
2014
2015 9
            $params = array_merge($params, $criteriaParams);
2016 9
            $types  = array_merge($types, $criteriaTypes);
2017
        }
2018
2019 13
        if ($filterSql = $this->generateFilterConditionSQL($this->class, $alias)) {
2020 3
            $sql .= ' AND ' . $filterSql;
2021
        }
2022
2023 13
        return (bool) $this->conn->fetchColumn($sql, $params, 0, $types);
2024
    }
2025
2026
    /**
2027
     * Generates the appropriate join SQL for the given join column.
2028
     *
2029
     * @param array $joinColumns The join columns definition of an association.
2030
     *
2031
     * @return string LEFT JOIN if one of the columns is nullable, INNER JOIN otherwise.
2032
     */
2033 13
    protected function getJoinSQLForJoinColumns($joinColumns)
2034
    {
2035
        // if one of the join columns is nullable, return left join
2036 13
        foreach ($joinColumns as $joinColumn) {
2037 13
             if ( ! isset($joinColumn['nullable']) || $joinColumn['nullable']) {
2038 13
                 return 'LEFT JOIN';
2039
             }
2040
        }
2041
2042 5
        return 'INNER JOIN';
2043
    }
2044
2045
    /**
2046
     * {@inheritdoc}
2047
     */
2048 592
    public function getSQLColumnAlias($columnName)
2049
    {
2050 592
        return $this->quoteStrategy->getColumnAlias($columnName, $this->currentPersisterContext->sqlAliasCounter++, $this->platform);
2051
    }
2052
2053
    /**
2054
     * Generates the filter SQL for a given entity and table alias.
2055
     *
2056
     * @param ClassMetadata $targetEntity     Metadata of the target entity.
2057
     * @param string        $targetTableAlias The table alias of the joined/selected table.
2058
     *
2059
     * @return string The SQL query part to add to a query.
2060
     */
2061 616 View Code Duplication
    protected function generateFilterConditionSQL(ClassMetadata $targetEntity, $targetTableAlias)
2062
    {
2063 616
        $filterClauses = [];
2064
2065 616
        foreach ($this->em->getFilters()->getEnabledFilters() as $filter) {
2066 22
            if ('' !== $filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias)) {
2067 22
                $filterClauses[] = '(' . $filterExpr . ')';
2068
            }
2069
        }
2070
2071 616
        $sql = implode(' AND ', $filterClauses);
2072
2073 616
        return $sql ? "(" . $sql . ")" : ""; // Wrap again to avoid "X or Y and FilterConditionSQL"
2074
    }
2075
2076
    /**
2077
     * Switches persister context according to current query offset/limits
2078
     *
2079
     * This is due to the fact that to-many associations cannot be fetch-joined when a limit is involved
2080
     *
2081
     * @param null|int $offset
2082
     * @param null|int $limit
2083
     */
2084 598
    protected function switchPersisterContext($offset, $limit)
2085
    {
2086 598
        if (null === $offset && null === $limit) {
2087 585
            $this->currentPersisterContext = $this->noLimitsContext;
2088
2089 585
            return;
2090
        }
2091
2092 42
        $this->currentPersisterContext = $this->limitsHandlingContext;
2093 42
    }
2094
}
2095