Failed Conditions
Push — 2.7 ( c036c0...266f0d )
by Jonathan
57:23 queued 50:07
created

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

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
use function array_merge;
41
use function reset;
42
43
/**
44
 * A BasicEntityPersister maps an entity to a single table in a relational database.
45
 *
46
 * A persister is always responsible for a single entity type.
47
 *
48
 * EntityPersisters are used during a UnitOfWork to apply any changes to the persistent
49
 * state of entities onto a relational database when the UnitOfWork is committed,
50
 * as well as for basic querying of entities and their associations (not DQL).
51
 *
52
 * The persisting operations that are invoked during a commit of a UnitOfWork to
53
 * persist the persistent entity state are:
54
 *
55
 *   - {@link addInsert} : To schedule an entity for insertion.
56
 *   - {@link executeInserts} : To execute all scheduled insertions.
57
 *   - {@link update} : To update the persistent state of an entity.
58
 *   - {@link delete} : To delete the persistent state of an entity.
59
 *
60
 * As can be seen from the above list, insertions are batched and executed all at once
61
 * for increased efficiency.
62
 *
63
 * The querying operations invoked during a UnitOfWork, either through direct find
64
 * requests or lazy-loading, are the following:
65
 *
66
 *   - {@link load} : Loads (the state of) a single, managed entity.
67
 *   - {@link loadAll} : Loads multiple, managed entities.
68
 *   - {@link loadOneToOneEntity} : Loads a one/many-to-one entity association (lazy-loading).
69
 *   - {@link loadOneToManyCollection} : Loads a one-to-many entity association (lazy-loading).
70
 *   - {@link loadManyToManyCollection} : Loads a many-to-many entity association (lazy-loading).
71
 *
72
 * The BasicEntityPersister implementation provides the default behavior for
73
 * persisting and querying entities that are mapped to a single database table.
74
 *
75
 * Subclasses can be created to provide custom persisting and querying strategies,
76
 * i.e. spanning multiple tables.
77
 *
78
 * @author Roman Borschel <[email protected]>
79
 * @author Giorgio Sironi <[email protected]>
80
 * @author Benjamin Eberlei <[email protected]>
81
 * @author Alexander <[email protected]>
82
 * @author Fabio B. Silva <[email protected]>
83
 * @author Rob Caiger <[email protected]>
84
 * @since 2.0
85
 */
86
class BasicEntityPersister implements EntityPersister
87
{
88
    /**
89
     * @var array
90
     */
91
    static private $comparisonMap = [
92
        Comparison::EQ          => '= %s',
93
        Comparison::IS          => '= %s',
94
        Comparison::NEQ         => '!= %s',
95
        Comparison::GT          => '> %s',
96
        Comparison::GTE         => '>= %s',
97
        Comparison::LT          => '< %s',
98
        Comparison::LTE         => '<= %s',
99
        Comparison::IN          => 'IN (%s)',
100
        Comparison::NIN         => 'NOT IN (%s)',
101
        Comparison::CONTAINS    => 'LIKE %s',
102
        Comparison::STARTS_WITH => 'LIKE %s',
103
        Comparison::ENDS_WITH   => 'LIKE %s',
104
    ];
105
106
    /**
107
     * Metadata object that describes the mapping of the mapped entity class.
108
     *
109
     * @var \Doctrine\ORM\Mapping\ClassMetadata
110
     */
111
    protected $class;
112
113
    /**
114
     * The underlying DBAL Connection of the used EntityManager.
115
     *
116
     * @var \Doctrine\DBAL\Connection $conn
117
     */
118
    protected $conn;
119
120
    /**
121
     * The database platform.
122
     *
123
     * @var \Doctrine\DBAL\Platforms\AbstractPlatform
124
     */
125
    protected $platform;
126
127
    /**
128
     * The EntityManager instance.
129
     *
130
     * @var EntityManagerInterface
131
     */
132
    protected $em;
133
134
    /**
135
     * Queued inserts.
136
     *
137
     * @var array
138
     */
139
    protected $queuedInserts = [];
140
141
    /**
142
     * The map of column names to DBAL mapping types of all prepared columns used
143
     * when INSERTing or UPDATEing an entity.
144
     *
145
     * @var array
146
     *
147
     * @see prepareInsertData($entity)
148
     * @see prepareUpdateData($entity)
149
     */
150
    protected $columnTypes = [];
151
152
    /**
153
     * The map of quoted column names.
154
     *
155
     * @var array
156
     *
157
     * @see prepareInsertData($entity)
158
     * @see prepareUpdateData($entity)
159
     */
160
    protected $quotedColumns = [];
161
162
    /**
163
     * The INSERT SQL statement used for entities handled by this persister.
164
     * This SQL is only generated once per request, if at all.
165
     *
166
     * @var string
167
     */
168
    private $insertSql;
169
170
    /**
171
     * The quote strategy.
172
     *
173
     * @var \Doctrine\ORM\Mapping\QuoteStrategy
174
     */
175
    protected $quoteStrategy;
176
177
    /**
178
     * The IdentifierFlattener used for manipulating identifiers
179
     *
180
     * @var \Doctrine\ORM\Utility\IdentifierFlattener
181
     */
182
    private $identifierFlattener;
183
184
    /**
185
     * @var CachedPersisterContext
186
     */
187
    protected $currentPersisterContext;
188
189
    /**
190
     * @var CachedPersisterContext
191
     */
192
    private $limitsHandlingContext;
193
194
    /**
195
     * @var CachedPersisterContext
196
     */
197
    private $noLimitsContext;
198
199
    /**
200
     * Initializes a new <tt>BasicEntityPersister</tt> that uses the given EntityManager
201
     * and persists instances of the class described by the given ClassMetadata descriptor.
202
     *
203
     * @param EntityManagerInterface $em
204
     * @param ClassMetadata          $class
205
     */
206 1181
    public function __construct(EntityManagerInterface $em, ClassMetadata $class)
207
    {
208 1181
        $this->em                    = $em;
209 1181
        $this->class                 = $class;
210 1181
        $this->conn                  = $em->getConnection();
211 1181
        $this->platform              = $this->conn->getDatabasePlatform();
212 1181
        $this->quoteStrategy         = $em->getConfiguration()->getQuoteStrategy();
213 1181
        $this->identifierFlattener   = new IdentifierFlattener($em->getUnitOfWork(), $em->getMetadataFactory());
214 1181
        $this->noLimitsContext       = $this->currentPersisterContext = new CachedPersisterContext(
215 1181
            $class,
216 1181
            new Query\ResultSetMapping(),
217 1181
            false
218
        );
219 1181
        $this->limitsHandlingContext = new CachedPersisterContext(
220 1181
            $class,
221 1181
            new Query\ResultSetMapping(),
222 1181
            true
223
        );
224 1181
    }
225
226
    /**
227
     * {@inheritdoc}
228
     */
229 18
    public function getClassMetadata()
230
    {
231 18
        return $this->class;
232
    }
233
234
    /**
235
     * {@inheritdoc}
236
     */
237 11
    public function getResultSetMapping()
238
    {
239 11
        return $this->currentPersisterContext->rsm;
240
    }
241
242
    /**
243
     * {@inheritdoc}
244
     */
245 1066
    public function addInsert($entity)
246
    {
247 1066
        $this->queuedInserts[spl_object_hash($entity)] = $entity;
248 1066
    }
249
250
    /**
251
     * {@inheritdoc}
252
     */
253 95
    public function getInserts()
254
    {
255 95
        return $this->queuedInserts;
256
    }
257
258
    /**
259
     * {@inheritdoc}
260
     */
261 1037
    public function executeInserts()
262
    {
263 1037
        if ( ! $this->queuedInserts) {
264 665
            return [];
265
        }
266
267 977
        $postInsertIds  = [];
268 977
        $idGenerator    = $this->class->idGenerator;
269 977
        $isPostInsertId = $idGenerator->isPostInsertGenerator();
270
271 977
        $stmt       = $this->conn->prepare($this->getInsertSQL());
272 977
        $tableName  = $this->class->getTableName();
273
274 977
        foreach ($this->queuedInserts as $entity) {
275 977
            $insertData = $this->prepareInsertData($entity);
276
277 977
            if (isset($insertData[$tableName])) {
278 949
                $paramIndex = 1;
279
280 949
                foreach ($insertData[$tableName] as $column => $value) {
281 949
                    $stmt->bindValue($paramIndex++, $value, $this->columnTypes[$column]);
282
                }
283
            }
284
285 977
            $stmt->execute();
286
287 976
            if ($isPostInsertId) {
288 884
                $generatedId = $idGenerator->generate($this->em, $entity);
289
                $id = [
290 884
                    $this->class->identifier[0] => $generatedId
291
                ];
292 884
                $postInsertIds[] = [
293 884
                    'generatedId' => $generatedId,
294 884
                    'entity' => $entity,
295
                ];
296
            } else {
297 269
                $id = $this->class->getIdentifierValues($entity);
298
            }
299
300 976
            if ($this->class->isVersioned) {
301 976
                $this->assignDefaultVersionValue($entity, $id);
302
            }
303
        }
304
305 976
        $stmt->closeCursor();
306 976
        $this->queuedInserts = [];
307
308 976
        return $postInsertIds;
309
    }
310
311
    /**
312
     * Retrieves the default version value which was created
313
     * by the preceding INSERT statement and assigns it back in to the
314
     * entities version field.
315
     *
316
     * @param object $entity
317
     * @param array  $id
318
     *
319
     * @return void
320
     */
321 202
    protected function assignDefaultVersionValue($entity, array $id)
322
    {
323 202
        $value = $this->fetchVersionValue($this->class, $id);
324
325 202
        $this->class->setFieldValue($entity, $this->class->versionField, $value);
326 202
    }
327
328
    /**
329
     * Fetches the current version value of a versioned entity.
330
     *
331
     * @param \Doctrine\ORM\Mapping\ClassMetadata $versionedClass
332
     * @param array                               $id
333
     *
334
     * @return mixed
335
     */
336 211
    protected function fetchVersionValue($versionedClass, array $id)
337
    {
338 211
        $versionField = $versionedClass->versionField;
339 211
        $fieldMapping = $versionedClass->fieldMappings[$versionField];
340 211
        $tableName    = $this->quoteStrategy->getTableName($versionedClass, $this->platform);
341 211
        $identifier   = $this->quoteStrategy->getIdentifierColumnNames($versionedClass, $this->platform);
342 211
        $columnName   = $this->quoteStrategy->getColumnName($versionField, $versionedClass, $this->platform);
343
344
        // FIXME: Order with composite keys might not be correct
345 211
        $sql = 'SELECT ' . $columnName
346 211
             . ' FROM '  . $tableName
347 211
             . ' WHERE ' . implode(' = ? AND ', $identifier) . ' = ?';
348
349
350 211
        $flatId = $this->identifierFlattener->flattenIdentifier($versionedClass, $id);
351
352 211
        $value = $this->conn->fetchColumn(
353 211
            $sql,
354 211
            array_values($flatId),
355 211
            0,
356 211
            $this->extractIdentifierTypes($id, $versionedClass)
357
        );
358
359 211
        return Type::getType($fieldMapping['type'])->convertToPHPValue($value, $this->platform);
360
    }
361
362 211
    private function extractIdentifierTypes(array $id, ClassMetadata $versionedClass) : array
363
    {
364 211
        $types = [];
365
366 211
        foreach ($id as $field => $value) {
367 211
            $types = array_merge($types, $this->getTypes($field, $value, $versionedClass));
368
        }
369
370 211
        return $types;
371
    }
372
373
    /**
374
     * {@inheritdoc}
375
     */
376 100
    public function update($entity)
377
    {
378 100
        $tableName  = $this->class->getTableName();
379 100
        $updateData = $this->prepareUpdateData($entity);
380
381 100
        if ( ! isset($updateData[$tableName]) || ! ($data = $updateData[$tableName])) {
382 8
            return;
383
        }
384
385 92
        $isVersioned     = $this->class->isVersioned;
386 92
        $quotedTableName = $this->quoteStrategy->getTableName($this->class, $this->platform);
387
388 92
        $this->updateTable($entity, $quotedTableName, $data, $isVersioned);
389
390 90
        if ($isVersioned) {
391 13
            $id = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
392
393 13
            $this->assignDefaultVersionValue($entity, $id);
394
        }
395 90
    }
396
397
    /**
398
     * Performs an UPDATE statement for an entity on a specific table.
399
     * The UPDATE can optionally be versioned, which requires the entity to have a version field.
400
     *
401
     * @param object  $entity          The entity object being updated.
402
     * @param string  $quotedTableName The quoted name of the table to apply the UPDATE on.
403
     * @param array   $updateData      The map of columns to update (column => value).
404
     * @param boolean $versioned       Whether the UPDATE should be versioned.
405
     *
406
     * @return void
407
     *
408
     * @throws \Doctrine\ORM\ORMException
409
     * @throws \Doctrine\ORM\OptimisticLockException
410
     */
411 123
    protected final function updateTable($entity, $quotedTableName, array $updateData, $versioned = false)
412
    {
413 123
        $set    = [];
414 123
        $types  = [];
415 123
        $params = [];
416
417 123
        foreach ($updateData as $columnName => $value) {
418 123
            $placeholder = '?';
419 123
            $column      = $columnName;
420
421
            switch (true) {
422 123
                case isset($this->class->fieldNames[$columnName]):
423 63
                    $fieldName  = $this->class->fieldNames[$columnName];
424 63
                    $column     = $this->quoteStrategy->getColumnName($fieldName, $this->class, $this->platform);
425
426 63
                    if (isset($this->class->fieldMappings[$fieldName]['requireSQLConversion'])) {
427 3
                        $type        = Type::getType($this->columnTypes[$columnName]);
428 3
                        $placeholder = $type->convertToDatabaseValueSQL('?', $this->platform);
429
                    }
430
431 63
                    break;
432
433 62
                case isset($this->quotedColumns[$columnName]):
434 62
                    $column = $this->quotedColumns[$columnName];
435
436 62
                    break;
437
            }
438
439 123
            $params[]   = $value;
440 123
            $set[]      = $column . ' = ' . $placeholder;
441 123
            $types[]    = $this->columnTypes[$columnName];
442
        }
443
444 123
        $where      = [];
445 123
        $identifier = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
446
447 123
        foreach ($this->class->identifier as $idField) {
448 123
            if ( ! isset($this->class->associationMappings[$idField])) {
449 118
                $params[]   = $identifier[$idField];
450 118
                $types[]    = $this->class->fieldMappings[$idField]['type'];
451 118
                $where[]    = $this->quoteStrategy->getColumnName($idField, $this->class, $this->platform);
452
453 118
                continue;
454
            }
455
456 6
            $params[] = $identifier[$idField];
457 6
            $where[]  = $this->quoteStrategy->getJoinColumnName(
458 6
                $this->class->associationMappings[$idField]['joinColumns'][0],
459 6
                $this->class,
460 6
                $this->platform
461
            );
462
463 6
            $targetMapping = $this->em->getClassMetadata($this->class->associationMappings[$idField]['targetEntity']);
464 6
            $targetType    = PersisterHelper::getTypeOfField($targetMapping->identifier[0], $targetMapping, $this->em);
465
466 6
            if ($targetType === []) {
467
                throw ORMException::unrecognizedField($targetMapping->identifier[0]);
468
            }
469
470 6
            $types[] = reset($targetType);
471
        }
472
473 123
        if ($versioned) {
474 19
            $versionField       = $this->class->versionField;
475 19
            $versionFieldType   = $this->class->fieldMappings[$versionField]['type'];
476 19
            $versionColumn      = $this->quoteStrategy->getColumnName($versionField, $this->class, $this->platform);
477
478 19
            $where[]    = $versionColumn;
479 19
            $types[]    = $this->class->fieldMappings[$versionField]['type'];
480 19
            $params[]   = $this->class->reflFields[$versionField]->getValue($entity);
481
482
            switch ($versionFieldType) {
483 19
                case Type::SMALLINT:
484 19
                case Type::INTEGER:
485 2
                case Type::BIGINT:
486 17
                    $set[] = $versionColumn . ' = ' . $versionColumn . ' + 1';
487 17
                    break;
488
489 2
                case Type::DATETIME:
490 2
                    $set[] = $versionColumn . ' = CURRENT_TIMESTAMP';
491 2
                    break;
492
            }
493
        }
494
495 123
        $sql = 'UPDATE ' . $quotedTableName
496 123
             . ' SET ' . implode(', ', $set)
497 123
             . ' WHERE ' . implode(' = ? AND ', $where) . ' = ?';
498
499 123
        $result = $this->conn->executeUpdate($sql, $params, $types);
500
501 123
        if ($versioned && ! $result) {
502 4
            throw OptimisticLockException::lockFailed($entity);
503
        }
504 120
    }
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 66
    protected function deleteJoinTableRecords($identifier)
514
    {
515 66
        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
            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 66
    }
566
567
    /**
568
     * {@inheritdoc}
569
     */
570 62
    public function delete($entity)
571
    {
572 62
        $self       = $this;
573 62
        $class      = $this->class;
574 62
        $identifier = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
575 62
        $tableName  = $this->quoteStrategy->getTableName($class, $this->platform);
576 62
        $idColumns  = $this->quoteStrategy->getIdentifierColumnNames($class, $this->platform);
577 62
        $id         = array_combine($idColumns, $identifier);
578 62
        $types      = array_map(function ($identifier) use ($class, $self) {
579 62
            if (isset($class->fieldMappings[$identifier])) {
580 60
                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 62
        }, $class->identifier);
595
596 62
        $this->deleteJoinTableRecords($identifier);
597
598 62
        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 1071
    protected function prepareUpdateData($entity)
623
    {
624 1071
        $versionField = null;
625 1071
        $result       = [];
626 1071
        $uow          = $this->em->getUnitOfWork();
627
628 1071
        if (($versioned = $this->class->isVersioned) != false) {
629 215
            $versionField = $this->class->versionField;
630
        }
631
632 1071
        foreach ($uow->getEntityChangeSet($entity) as $field => $change) {
633 1034
            if (isset($versionField) && $versionField == $field) {
634
                continue;
635
            }
636
637 1034
            if (isset($this->class->embeddedClasses[$field])) {
638 9
                continue;
639
            }
640
641 1034
            $newVal = $change[1];
642
643 1034
            if ( ! isset($this->class->associationMappings[$field])) {
644 993
                $fieldMapping = $this->class->fieldMappings[$field];
645 993
                $columnName   = $fieldMapping['columnName'];
646
647 993
                $this->columnTypes[$columnName] = $fieldMapping['type'];
648
649 993
                $result[$this->getOwningTable($field)][$columnName] = $newVal;
650
651 993
                continue;
652
            }
653
654 878
            $assoc = $this->class->associationMappings[$field];
655
656
            // Only owning side of x-1 associations can have a FK column.
657 878
            if ( ! $assoc['isOwningSide'] || ! ($assoc['type'] & ClassMetadata::TO_ONE)) {
658 8
                continue;
659
            }
660
661 878
            if ($newVal !== null) {
662 640
                $oid = spl_object_hash($newVal);
663
664 640
                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 878
            $newValId = null;
675
676 878
            if ($newVal !== null) {
677 640
                $newValId = $uow->getEntityIdentifier($newVal);
678
            }
679
680 878
            $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
681 878
            $owningTable = $this->getOwningTable($field);
682
683 878
            foreach ($assoc['joinColumns'] as $joinColumn) {
684 878
                $sourceColumn = $joinColumn['name'];
685 878
                $targetColumn = $joinColumn['referencedColumnName'];
686 878
                $quotedColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
687
688 878
                $this->quotedColumns[$sourceColumn]  = $quotedColumn;
689 878
                $this->columnTypes[$sourceColumn]    = PersisterHelper::getTypeOfColumn($targetColumn, $targetClass, $this->em);
690 878
                $result[$owningTable][$sourceColumn] = $newValId
691 640
                    ? $newValId[$targetClass->getFieldForColumn($targetColumn)]
692 878
                    : null;
693
            }
694
        }
695
696 1071
        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 1066
    protected function prepareInsertData($entity)
712
    {
713 1066
        return $this->prepareUpdateData($entity);
714
    }
715
716
    /**
717
     * {@inheritdoc}
718
     */
719 948
    public function getOwningTable($fieldName)
720
    {
721 948
        return $this->class->getTableName();
722
    }
723
724
    /**
725
     * {@inheritdoc}
726
     */
727 519
    public function load(array $criteria, $entity = null, $assoc = null, array $hints = [], $lockMode = null, $limit = null, array $orderBy = null)
728
    {
729 519
        $this->switchPersisterContext(null, $limit);
730
731 519
        $sql = $this->getSelectSQL($criteria, $assoc, $lockMode, $limit, null, $orderBy);
732 518
        list($params, $types) = $this->expandParameters($criteria);
733 518
        $stmt = $this->conn->executeQuery($sql, $params, $types);
734
735 518
        if ($entity !== null) {
736 72
            $hints[Query::HINT_REFRESH]         = true;
737 72
            $hints[Query::HINT_REFRESH_ENTITY]  = $entity;
738
        }
739
740 518
        $hydrator = $this->em->newHydrator($this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
741 518
        $entities = $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, $hints);
742
743 518
        return $entities ? $entities[0] : null;
744
    }
745
746
    /**
747
     * {@inheritdoc}
748
     */
749 442
    public function loadById(array $identifier, $entity = null)
750
    {
751 442
        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
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);
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 64
        $computedIdentifier = [];
796
797
        // TRICKY: since the association is specular source and target are flipped
798 64
        foreach ($owningAssoc['targetToSourceKeyColumns'] as $sourceKeyColumn => $targetKeyColumn) {
799 64
            if ( ! isset($sourceClass->fieldNames[$sourceKeyColumn])) {
800
                throw MappingException::joinColumnMustPointToMappedField(
801
                    $sourceClass->name, $sourceKeyColumn
802
                );
803
            }
804
805 64
            $computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
806 64
                $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
807
        }
808
809 64
        $targetEntity = $this->load($computedIdentifier, null, $assoc);
810
811 64
        if ($targetEntity !== null) {
812 17
            $targetClass->setFieldValue($targetEntity, $assoc['mappedBy'], $sourceEntity);
813
        }
814
815 64
        return $targetEntity;
816
    }
817
818
    /**
819
     * {@inheritdoc}
820
     */
821 17
    public function refresh(array $id, $entity, $lockMode = null)
822
    {
823 17
        $sql = $this->getSelectSQL($id, null, $lockMode);
824 17
        list($params, $types) = $this->expandParameters($id);
825 17
        $stmt = $this->conn->executeQuery($sql, $params, $types);
826
827 17
        $hydrator = $this->em->newHydrator(Query::HYDRATE_OBJECT);
828 17
        $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [Query::HINT_REFRESH => true]);
829 17
    }
830
831
    /**
832
     * {@inheritDoc}
833
     */
834 46
    public function count($criteria = [])
835
    {
836 46
        $sql = $this->getCountSQL($criteria);
837
838 46
        list($params, $types) = ($criteria instanceof Criteria)
839 43
            ? $this->expandCriteriaParameters($criteria)
840 46
            : $this->expandParameters($criteria);
841
842 46
        return (int) $this->conn->executeQuery($sql, $params, $types)->fetchColumn();
843
    }
844
845
    /**
846
     * {@inheritdoc}
847
     */
848 9
    public function loadCriteria(Criteria $criteria)
849
    {
850 9
        $orderBy = $criteria->getOrderings();
851 9
        $limit   = $criteria->getMaxResults();
852 9
        $offset  = $criteria->getFirstResult();
853 9
        $query   = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy);
854
855 7
        list($params, $types) = $this->expandCriteriaParameters($criteria);
856
857 7
        $stmt       = $this->conn->executeQuery($query, $params, $types);
858 7
        $hydrator   = $this->em->newHydrator(($this->currentPersisterContext->selectJoinSql) ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
859
860 7
        return $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [UnitOfWork::HINT_DEFEREAGERLOAD => true]
861
        );
862
    }
863
864
    /**
865
     * {@inheritdoc}
866
     */
867 56
    public function expandCriteriaParameters(Criteria $criteria)
868
    {
869 56
        $expression = $criteria->getWhereExpression();
870 56
        $sqlParams  = [];
871 56
        $sqlTypes   = [];
872
873 56
        if ($expression === null) {
874 2
            return [$sqlParams, $sqlTypes];
875
        }
876
877 55
        $valueVisitor = new SqlValueVisitor();
878
879 55
        $valueVisitor->dispatch($expression);
880
881 55
        list($params, $types) = $valueVisitor->getParamsAndTypes();
882
883 55
        foreach ($params as $param) {
884 51
            $sqlParams = array_merge($sqlParams, $this->getValues($param));
885
        }
886
887 55
        foreach ($types as $type) {
888 51
            list ($field, $value) = $type;
889 51
            $sqlTypes = array_merge($sqlTypes, $this->getTypes($field, $value, $this->class));
890
        }
891
892 55
        return [$sqlParams, $sqlTypes];
893
    }
894
895
    /**
896
     * {@inheritdoc}
897
     */
898 71
    public function loadAll(array $criteria = [], array $orderBy = null, $limit = null, $offset = null)
899
    {
900 71
        $this->switchPersisterContext($offset, $limit);
901
902 71
        $sql = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy);
903 67
        list($params, $types) = $this->expandParameters($criteria);
904 67
        $stmt = $this->conn->executeQuery($sql, $params, $types);
905
906 67
        $hydrator = $this->em->newHydrator(($this->currentPersisterContext->selectJoinSql) ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
907
908 67
        return $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [UnitOfWork::HINT_DEFEREAGERLOAD => true]
909
        );
910
    }
911
912
    /**
913
     * {@inheritdoc}
914
     */
915 8
    public function getManyToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null)
916
    {
917 8
        $this->switchPersisterContext($offset, $limit);
918
919 8
        $stmt = $this->getManyToManyStatement($assoc, $sourceEntity, $offset, $limit);
920
921 8
        return $this->loadArrayFromStatement($assoc, $stmt);
922
    }
923
924
    /**
925
     * Loads an array of entities from a given DBAL statement.
926
     *
927
     * @param array                    $assoc
928
     * @param \Doctrine\DBAL\Statement $stmt
929
     *
930
     * @return array
931
     */
932 13
    private function loadArrayFromStatement($assoc, $stmt)
933
    {
934 13
        $rsm    = $this->currentPersisterContext->rsm;
935 13
        $hints  = [UnitOfWork::HINT_DEFEREAGERLOAD => true];
936
937 13
        if (isset($assoc['indexBy'])) {
938 7
            $rsm = clone ($this->currentPersisterContext->rsm); // this is necessary because the "default rsm" should be changed.
939 7
            $rsm->addIndexBy('r', $assoc['indexBy']);
940
        }
941
942 13
        return $this->em->newHydrator(Query::HYDRATE_OBJECT)->hydrateAll($stmt, $rsm, $hints);
943
    }
944
945
    /**
946
     * Hydrates a collection from a given DBAL statement.
947
     *
948
     * @param array                    $assoc
949
     * @param \Doctrine\DBAL\Statement $stmt
950
     * @param PersistentCollection     $coll
951
     *
952
     * @return array
953
     */
954 145
    private function loadCollectionFromStatement($assoc, $stmt, $coll)
955
    {
956 145
        $rsm   = $this->currentPersisterContext->rsm;
957
        $hints = [
958 145
            UnitOfWork::HINT_DEFEREAGERLOAD => true,
959 145
            'collection' => $coll
960
        ];
961
962 145
        if (isset($assoc['indexBy'])) {
963 10
            $rsm = clone ($this->currentPersisterContext->rsm); // this is necessary because the "default rsm" should be changed.
964 10
            $rsm->addIndexBy('r', $assoc['indexBy']);
965
        }
966
967 145
        return $this->em->newHydrator(Query::HYDRATE_OBJECT)->hydrateAll($stmt, $rsm, $hints);
968
    }
969
970
    /**
971
     * {@inheritdoc}
972
     */
973 82
    public function loadManyToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll)
974
    {
975 82
        $stmt = $this->getManyToManyStatement($assoc, $sourceEntity);
976
977 82
        return $this->loadCollectionFromStatement($assoc, $stmt, $coll);
978
    }
979
980
    /**
981
     * @param array    $assoc
982
     * @param object   $sourceEntity
983
     * @param int|null $offset
984
     * @param int|null $limit
985
     *
986
     * @return \Doctrine\DBAL\Driver\Statement
987
     *
988
     * @throws \Doctrine\ORM\Mapping\MappingException
989
     */
990 89
    private function getManyToManyStatement(array $assoc, $sourceEntity, $offset = null, $limit = null)
991
    {
992 89
        $this->switchPersisterContext($offset, $limit);
993
994 89
        $sourceClass    = $this->em->getClassMetadata($assoc['sourceEntity']);
995 89
        $class          = $sourceClass;
996 89
        $association    = $assoc;
997 89
        $criteria       = [];
998 89
        $parameters     = [];
999
1000 89
        if ( ! $assoc['isOwningSide']) {
1001 12
            $class       = $this->em->getClassMetadata($assoc['targetEntity']);
1002 12
            $association = $class->associationMappings[$assoc['mappedBy']];
1003
        }
1004
1005 89
        $joinColumns = $assoc['isOwningSide']
1006 82
            ? $association['joinTable']['joinColumns']
1007 89
            : $association['joinTable']['inverseJoinColumns'];
1008
1009 89
        $quotedJoinTable = $this->quoteStrategy->getJoinTableName($association, $class, $this->platform);
1010
1011 89
        foreach ($joinColumns as $joinColumn) {
1012 89
            $sourceKeyColumn    = $joinColumn['referencedColumnName'];
1013 89
            $quotedKeyColumn    = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
1014
1015
            switch (true) {
1016 89
                case $sourceClass->containsForeignIdentifier:
1017 4
                    $field = $sourceClass->getFieldForColumn($sourceKeyColumn);
1018 4
                    $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
1019
1020 4
                    if (isset($sourceClass->associationMappings[$field])) {
1021 4
                        $value = $this->em->getUnitOfWork()->getEntityIdentifier($value);
1022 4
                        $value = $value[$this->em->getClassMetadata($sourceClass->associationMappings[$field]['targetEntity'])->identifier[0]];
1023
                    }
1024
1025 4
                    break;
1026
1027 87
                case isset($sourceClass->fieldNames[$sourceKeyColumn]):
1028 87
                    $field = $sourceClass->fieldNames[$sourceKeyColumn];
1029 87
                    $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
1030
1031 87
                    break;
1032
1033
                default:
1034
                    throw MappingException::joinColumnMustPointToMappedField(
1035
                        $sourceClass->name, $sourceKeyColumn
1036
                    );
1037
            }
1038
1039 89
            $criteria[$quotedJoinTable . '.' . $quotedKeyColumn] = $value;
1040 89
            $parameters[] = [
1041 89
                'value' => $value,
1042 89
                'field' => $field,
1043 89
                'class' => $sourceClass,
1044
            ];
1045
        }
1046
1047 89
        $sql = $this->getSelectSQL($criteria, $assoc, null, $limit, $offset);
1048 89
        list($params, $types) = $this->expandToManyParameters($parameters);
1049
1050 89
        return $this->conn->executeQuery($sql, $params, $types);
1051
    }
1052
1053
    /**
1054
     * {@inheritdoc}
1055
     */
1056 571
    public function getSelectSQL($criteria, $assoc = null, $lockMode = null, $limit = null, $offset = null, array $orderBy = null)
1057
    {
1058 571
        $this->switchPersisterContext($offset, $limit);
1059
1060 571
        $lockSql    = '';
1061 571
        $joinSql    = '';
1062 571
        $orderBySql = '';
1063
1064 571
        if ($assoc != null && $assoc['type'] == ClassMetadata::MANY_TO_MANY) {
1065 90
            $joinSql = $this->getSelectManyToManyJoinSQL($assoc);
1066
        }
1067
1068 571
        if (isset($assoc['orderBy'])) {
1069 5
            $orderBy = $assoc['orderBy'];
1070
        }
1071
1072 571
        if ($orderBy) {
1073 11
            $orderBySql = $this->getOrderBySQL($orderBy, $this->getSQLTableAlias($this->class->name));
1074
        }
1075
1076 569
        $conditionSql = ($criteria instanceof Criteria)
1077 9
            ? $this->getSelectConditionCriteriaSQL($criteria)
1078 567
            : $this->getSelectConditionSQL($criteria, $assoc);
1079
1080
        switch ($lockMode) {
1081 564
            case LockMode::PESSIMISTIC_READ:
1082
                $lockSql = ' ' . $this->platform->getReadLockSQL();
1083
                break;
1084
1085 564
            case LockMode::PESSIMISTIC_WRITE:
1086
                $lockSql = ' ' . $this->platform->getWriteLockSQL();
1087
                break;
1088
        }
1089
1090 564
        $columnList = $this->getSelectColumnsSQL();
1091 564
        $tableAlias = $this->getSQLTableAlias($this->class->name);
1092 564
        $filterSql  = $this->generateFilterConditionSQL($this->class, $tableAlias);
1093 564
        $tableName  = $this->quoteStrategy->getTableName($this->class, $this->platform);
1094
1095 564
        if ('' !== $filterSql) {
1096 12
            $conditionSql = $conditionSql
1097 11
                ? $conditionSql . ' AND ' . $filterSql
1098 12
                : $filterSql;
1099
        }
1100
1101 564
        $select = 'SELECT ' . $columnList;
1102 564
        $from   = ' FROM ' . $tableName . ' '. $tableAlias;
1103 564
        $join   = $this->currentPersisterContext->selectJoinSql . $joinSql;
1104 564
        $where  = ($conditionSql ? ' WHERE ' . $conditionSql : '');
1105 564
        $lock   = $this->platform->appendLockHint($from, $lockMode);
1106
        $query  = $select
1107 564
            . $lock
1108 564
            . $join
1109 564
            . $where
1110 564
            . $orderBySql;
1111
1112 564
        return $this->platform->modifyLimitQuery($query, $limit, $offset) . $lockSql;
1113
    }
1114
1115
    /**
1116
     * {@inheritDoc}
1117
     */
1118 41
    public function getCountSQL($criteria = [])
1119
    {
1120 41
        $tableName  = $this->quoteStrategy->getTableName($this->class, $this->platform);
1121 41
        $tableAlias = $this->getSQLTableAlias($this->class->name);
1122
1123 41
        $conditionSql = ($criteria instanceof Criteria)
1124 38
            ? $this->getSelectConditionCriteriaSQL($criteria)
1125 41
            : $this->getSelectConditionSQL($criteria);
1126
1127 41
        $filterSql = $this->generateFilterConditionSQL($this->class, $tableAlias);
1128
1129 41
        if ('' !== $filterSql) {
1130 2
            $conditionSql = $conditionSql
1131 2
                ? $conditionSql . ' AND ' . $filterSql
1132 2
                : $filterSql;
1133
        }
1134
1135
        $sql = 'SELECT COUNT(*) '
1136 41
            . 'FROM ' . $tableName . ' ' . $tableAlias
1137 41
            . (empty($conditionSql) ? '' : ' WHERE ' . $conditionSql);
1138
1139 41
        return $sql;
1140
    }
1141
1142
    /**
1143
     * Gets the ORDER BY SQL snippet for ordered collections.
1144
     *
1145
     * @param array  $orderBy
1146
     * @param string $baseTableAlias
1147
     *
1148
     * @return string
1149
     *
1150
     * @throws \Doctrine\ORM\ORMException
1151
     */
1152 12
    protected final function getOrderBySQL(array $orderBy, $baseTableAlias)
1153
    {
1154 12
        $orderByList = [];
1155
1156 12
        foreach ($orderBy as $fieldName => $orientation) {
1157
1158 12
            $orientation = strtoupper(trim($orientation));
1159
1160 12
            if ($orientation != 'ASC' && $orientation != 'DESC') {
1161 1
                throw ORMException::invalidOrientation($this->class->name, $fieldName);
1162
            }
1163
1164 11
            if (isset($this->class->fieldMappings[$fieldName])) {
1165 9
                $tableAlias = isset($this->class->fieldMappings[$fieldName]['inherited'])
1166
                    ? $this->getSQLTableAlias($this->class->fieldMappings[$fieldName]['inherited'])
1167 9
                    : $baseTableAlias;
1168
1169 9
                $columnName    = $this->quoteStrategy->getColumnName($fieldName, $this->class, $this->platform);
1170 9
                $orderByList[] = $tableAlias . '.' . $columnName . ' ' . $orientation;
1171
1172 9
                continue;
1173
            }
1174
1175 2
            if (isset($this->class->associationMappings[$fieldName])) {
1176
1177 2
                if ( ! $this->class->associationMappings[$fieldName]['isOwningSide']) {
1178 1
                    throw ORMException::invalidFindByInverseAssociation($this->class->name, $fieldName);
1179
                }
1180
1181 1
                $tableAlias = isset($this->class->associationMappings[$fieldName]['inherited'])
1182
                    ? $this->getSQLTableAlias($this->class->associationMappings[$fieldName]['inherited'])
1183 1
                    : $baseTableAlias;
1184
1185 1
                foreach ($this->class->associationMappings[$fieldName]['joinColumns'] as $joinColumn) {
1186 1
                    $columnName    = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
1187 1
                    $orderByList[] = $tableAlias . '.' . $columnName . ' ' . $orientation;
1188
                }
1189
1190 1
                continue;
1191
            }
1192
1193
            throw ORMException::unrecognizedField($fieldName);
1194
        }
1195
1196 10
        return ' ORDER BY ' . implode(', ', $orderByList);
1197
    }
1198
1199
    /**
1200
     * Gets the SQL fragment with the list of columns to select when querying for
1201
     * an entity in this persister.
1202
     *
1203
     * Subclasses should override this method to alter or change the select column
1204
     * list SQL fragment. Note that in the implementation of BasicEntityPersister
1205
     * the resulting SQL fragment is generated only once and cached in {@link selectColumnListSql}.
1206
     * Subclasses may or may not do the same.
1207
     *
1208
     * @return string The SQL fragment.
1209
     */
1210 565
    protected function getSelectColumnsSQL()
1211
    {
1212 565
        if ($this->currentPersisterContext->selectColumnListSql !== null) {
1213 147
            return $this->currentPersisterContext->selectColumnListSql;
1214
        }
1215
1216 565
        $columnList = [];
1217 565
        $this->currentPersisterContext->rsm->addEntityResult($this->class->name, 'r'); // r for root
1218
1219
        // Add regular columns to select list
1220 565
        foreach ($this->class->fieldNames as $field) {
1221 563
            $columnList[] = $this->getSelectColumnSQL($field, $this->class);
1222
        }
1223
1224 565
        $this->currentPersisterContext->selectJoinSql    = '';
1225 565
        $eagerAliasCounter      = 0;
1226
1227 565
        foreach ($this->class->associationMappings as $assocField => $assoc) {
1228 500
            $assocColumnSQL = $this->getSelectColumnAssociationSQL($assocField, $assoc, $this->class);
1229
1230 500
            if ($assocColumnSQL) {
1231 422
                $columnList[] = $assocColumnSQL;
1232
            }
1233
1234 500
            $isAssocToOneInverseSide = $assoc['type'] & ClassMetadata::TO_ONE && ! $assoc['isOwningSide'];
1235 500
            $isAssocFromOneEager     = $assoc['type'] !== ClassMetadata::MANY_TO_MANY && $assoc['fetch'] === ClassMetadata::FETCH_EAGER;
1236
1237 500
            if ( ! ($isAssocFromOneEager || $isAssocToOneInverseSide)) {
1238 478
                continue;
1239
            }
1240
1241 192
            if ((($assoc['type'] & ClassMetadata::TO_MANY) > 0) && $this->currentPersisterContext->handlesLimits) {
1242 3
                continue;
1243
            }
1244
1245 189
            $eagerEntity = $this->em->getClassMetadata($assoc['targetEntity']);
1246
1247 189
            if ($eagerEntity->inheritanceType != ClassMetadata::INHERITANCE_TYPE_NONE) {
1248 5
                continue; // now this is why you shouldn't use inheritance
1249
            }
1250
1251 184
            $assocAlias = 'e' . ($eagerAliasCounter++);
1252 184
            $this->currentPersisterContext->rsm->addJoinedEntityResult($assoc['targetEntity'], $assocAlias, 'r', $assocField);
1253
1254 184
            foreach ($eagerEntity->fieldNames as $field) {
1255 181
                $columnList[] = $this->getSelectColumnSQL($field, $eagerEntity, $assocAlias);
1256
            }
1257
1258 184
            foreach ($eagerEntity->associationMappings as $eagerAssocField => $eagerAssoc) {
1259 181
                $eagerAssocColumnSQL = $this->getSelectColumnAssociationSQL(
1260 181
                    $eagerAssocField, $eagerAssoc, $eagerEntity, $assocAlias
1261
                );
1262
1263 181
                if ($eagerAssocColumnSQL) {
1264 181
                    $columnList[] = $eagerAssocColumnSQL;
1265
                }
1266
            }
1267
1268 184
            $association    = $assoc;
1269 184
            $joinCondition  = [];
1270
1271 184
            if (isset($assoc['indexBy'])) {
1272 1
                $this->currentPersisterContext->rsm->addIndexBy($assocAlias, $assoc['indexBy']);
1273
            }
1274
1275 184
            if ( ! $assoc['isOwningSide']) {
1276 177
                $eagerEntity = $this->em->getClassMetadata($assoc['targetEntity']);
1277 177
                $association = $eagerEntity->getAssociationMapping($assoc['mappedBy']);
1278
            }
1279
1280 184
            $joinTableAlias = $this->getSQLTableAlias($eagerEntity->name, $assocAlias);
1281 184
            $joinTableName  = $this->quoteStrategy->getTableName($eagerEntity, $this->platform);
1282
1283 184
            if ($assoc['isOwningSide']) {
1284 13
                $tableAlias           = $this->getSQLTableAlias($association['targetEntity'], $assocAlias);
1285 13
                $this->currentPersisterContext->selectJoinSql .= ' ' . $this->getJoinSQLForJoinColumns($association['joinColumns']);
1286
1287 13
                foreach ($association['joinColumns'] as $joinColumn) {
1288 13
                    $sourceCol       = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
1289 13
                    $targetCol       = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform);
1290 13
                    $joinCondition[] = $this->getSQLTableAlias($association['sourceEntity'])
1291 13
                                        . '.' . $sourceCol . ' = ' . $tableAlias . '.' . $targetCol;
1292
                }
1293
1294
                // Add filter SQL
1295 13
                if ($filterSql = $this->generateFilterConditionSQL($eagerEntity, $tableAlias)) {
1296 13
                    $joinCondition[] = $filterSql;
1297
                }
1298
1299
            } else {
1300
1301 177
                $this->currentPersisterContext->selectJoinSql .= ' LEFT JOIN';
1302
1303 177
                foreach ($association['joinColumns'] as $joinColumn) {
1304 177
                    $sourceCol       = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
1305 177
                    $targetCol       = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform);
1306
1307 177
                    $joinCondition[] = $this->getSQLTableAlias($association['sourceEntity'], $assocAlias) . '.' . $sourceCol . ' = '
1308 177
                        . $this->getSQLTableAlias($association['targetEntity']) . '.' . $targetCol;
1309
                }
1310
            }
1311
1312 184
            $this->currentPersisterContext->selectJoinSql .= ' ' . $joinTableName . ' ' . $joinTableAlias . ' ON ';
1313 184
            $this->currentPersisterContext->selectJoinSql .= implode(' AND ', $joinCondition);
1314
        }
1315
1316 565
        $this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList);
1317
1318 565
        return $this->currentPersisterContext->selectColumnListSql;
1319
    }
1320
1321
    /**
1322
     * Gets the SQL join fragment used when selecting entities from an association.
1323
     *
1324
     * @param string        $field
1325
     * @param array         $assoc
1326
     * @param ClassMetadata $class
1327
     * @param string        $alias
1328
     *
1329
     * @return string
1330
     */
1331 500
    protected function getSelectColumnAssociationSQL($field, $assoc, ClassMetadata $class, $alias = 'r')
1332
    {
1333 500
        if ( ! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) ) {
1334 400
            return '';
1335
        }
1336
1337 439
        $columnList    = [];
1338 439
        $targetClass   = $this->em->getClassMetadata($assoc['targetEntity']);
1339 439
        $isIdentifier  = isset($assoc['id']) && $assoc['id'] === true;
1340 439
        $sqlTableAlias = $this->getSQLTableAlias($class->name, ($alias == 'r' ? '' : $alias));
1341
1342 439
        foreach ($assoc['joinColumns'] as $joinColumn) {
1343 439
            $quotedColumn     = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
1344 439
            $resultColumnName = $this->getSQLColumnAlias($joinColumn['name']);
1345 439
            $type             = PersisterHelper::getTypeOfColumn($joinColumn['referencedColumnName'], $targetClass, $this->em);
1346
1347 439
            $this->currentPersisterContext->rsm->addMetaResult($alias, $resultColumnName, $joinColumn['name'], $isIdentifier, $type);
1348
1349 439
            $columnList[] = sprintf('%s.%s AS %s', $sqlTableAlias, $quotedColumn, $resultColumnName);
1350
        }
1351
1352 439
        return implode(', ', $columnList);
1353
    }
1354
1355
    /**
1356
     * Gets the SQL join fragment used when selecting entities from a
1357
     * many-to-many association.
1358
     *
1359
     * @param array $manyToMany
1360
     *
1361
     * @return string
1362
     */
1363 92
    protected function getSelectManyToManyJoinSQL(array $manyToMany)
1364
    {
1365 92
        $conditions         = [];
1366 92
        $association        = $manyToMany;
1367 92
        $sourceTableAlias   = $this->getSQLTableAlias($this->class->name);
1368
1369 92
        if ( ! $manyToMany['isOwningSide']) {
1370 13
            $targetEntity   = $this->em->getClassMetadata($manyToMany['targetEntity']);
1371 13
            $association    = $targetEntity->associationMappings[$manyToMany['mappedBy']];
1372
        }
1373
1374 92
        $joinTableName  = $this->quoteStrategy->getJoinTableName($association, $this->class, $this->platform);
1375 92
        $joinColumns    = ($manyToMany['isOwningSide'])
1376 84
            ? $association['joinTable']['inverseJoinColumns']
1377 92
            : $association['joinTable']['joinColumns'];
1378
1379 92
        foreach ($joinColumns as $joinColumn) {
1380 92
            $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
1381 92
            $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform);
1382 92
            $conditions[]       = $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableName . '.' . $quotedSourceColumn;
1383
        }
1384
1385 92
        return ' INNER JOIN ' . $joinTableName . ' ON ' . implode(' AND ', $conditions);
1386
    }
1387
1388
    /**
1389
     * {@inheritdoc}
1390
     */
1391 1067
    public function getInsertSQL()
1392
    {
1393 1067
        if ($this->insertSql !== null) {
1394 89
            return $this->insertSql;
1395
        }
1396
1397 1067
        $columns   = $this->getInsertColumnList();
1398 1067
        $tableName = $this->quoteStrategy->getTableName($this->class, $this->platform);
1399
1400 1067
        if (empty($columns)) {
1401 116
            $identityColumn  = $this->quoteStrategy->getColumnName($this->class->identifier[0], $this->class, $this->platform);
1402 116
            $this->insertSql = $this->platform->getEmptyIdentityInsertSQL($tableName, $identityColumn);
1403
1404 116
            return $this->insertSql;
1405
        }
1406
1407 1041
        $values  = [];
1408 1041
        $columns = array_unique($columns);
1409
1410 1041
        foreach ($columns as $column) {
1411 1041
            $placeholder = '?';
1412
1413 1041
            if (isset($this->class->fieldNames[$column])
1414 1041
                && isset($this->columnTypes[$this->class->fieldNames[$column]])
1415 1041
                && isset($this->class->fieldMappings[$this->class->fieldNames[$column]]['requireSQLConversion'])) {
1416 6
                $type        = Type::getType($this->columnTypes[$this->class->fieldNames[$column]]);
1417 6
                $placeholder = $type->convertToDatabaseValueSQL('?', $this->platform);
1418
            }
1419
1420 1041
            $values[] = $placeholder;
1421
        }
1422
1423 1041
        $columns = implode(', ', $columns);
1424 1041
        $values  = implode(', ', $values);
1425
1426 1041
        $this->insertSql = sprintf('INSERT INTO %s (%s) VALUES (%s)', $tableName, $columns, $values);
1427
1428 1041
        return $this->insertSql;
1429
    }
1430
1431
    /**
1432
     * Gets the list of columns to put in the INSERT SQL statement.
1433
     *
1434
     * Subclasses should override this method to alter or change the list of
1435
     * columns placed in the INSERT statements used by the persister.
1436
     *
1437
     * @return array The list of columns.
1438
     */
1439 978
    protected function getInsertColumnList()
1440
    {
1441 978
        $columns = [];
1442
1443 978
        foreach ($this->class->reflFields as $name => $field) {
1444 978
            if ($this->class->isVersioned && $this->class->versionField == $name) {
1445 202
                continue;
1446
            }
1447
1448 978
            if (isset($this->class->embeddedClasses[$name])) {
1449 8
                continue;
1450
            }
1451
1452 978
            if (isset($this->class->associationMappings[$name])) {
1453 854
                $assoc = $this->class->associationMappings[$name];
1454
1455 854
                if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
1456 804
                    foreach ($assoc['joinColumns'] as $joinColumn) {
1457 804
                        $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
1458
                    }
1459
                }
1460
1461 854
                continue;
1462
            }
1463
1464 978
            if (! $this->class->isIdGeneratorIdentity() || $this->class->identifier[0] != $name) {
1465 904
                $columns[]                = $this->quoteStrategy->getColumnName($name, $this->class, $this->platform);
1466 978
                $this->columnTypes[$name] = $this->class->fieldMappings[$name]['type'];
1467
            }
1468
        }
1469
1470 978
        return $columns;
1471
    }
1472
1473
    /**
1474
     * Gets the SQL snippet of a qualified column name for the given field name.
1475
     *
1476
     * @param string        $field The field name.
1477
     * @param ClassMetadata $class The class that declares this field. The table this class is
1478
     *                             mapped to must own the column for the given field.
1479
     * @param string        $alias
1480
     *
1481
     * @return string
1482
     */
1483 528
    protected function getSelectColumnSQL($field, ClassMetadata $class, $alias = 'r')
1484
    {
1485 528
        $root         = $alias == 'r' ? '' : $alias ;
1486 528
        $tableAlias   = $this->getSQLTableAlias($class->name, $root);
1487 528
        $fieldMapping = $class->fieldMappings[$field];
1488 528
        $sql          = sprintf('%s.%s', $tableAlias, $this->quoteStrategy->getColumnName($field, $class, $this->platform));
1489 528
        $columnAlias  = $this->getSQLColumnAlias($fieldMapping['columnName']);
1490
1491 528
        $this->currentPersisterContext->rsm->addFieldResult($alias, $columnAlias, $field);
1492
1493 528
        if (isset($fieldMapping['requireSQLConversion'])) {
1494 3
            $type = Type::getType($fieldMapping['type']);
1495 3
            $sql  = $type->convertToPHPValueSQL($sql, $this->platform);
1496
        }
1497
1498 528
        return $sql . ' AS ' . $columnAlias;
1499
    }
1500
1501
    /**
1502
     * Gets the SQL table alias for the given class name.
1503
     *
1504
     * @param string $className
1505
     * @param string $assocName
1506
     *
1507
     * @return string The SQL table alias.
1508
     *
1509
     * @todo Reconsider. Binding table aliases to class names is not such a good idea.
1510
     */
1511 635
    protected function getSQLTableAlias($className, $assocName = '')
1512
    {
1513 635
        if ($assocName) {
1514 184
            $className .= '#' . $assocName;
1515
        }
1516
1517 635
        if (isset($this->currentPersisterContext->sqlTableAliases[$className])) {
1518 631
            return $this->currentPersisterContext->sqlTableAliases[$className];
1519
        }
1520
1521 635
        $tableAlias = 't' . $this->currentPersisterContext->sqlAliasCounter++;
1522
1523 635
        $this->currentPersisterContext->sqlTableAliases[$className] = $tableAlias;
1524
1525 635
        return $tableAlias;
1526
    }
1527
1528
    /**
1529
     * {@inheritdoc}
1530
     */
1531
    public function lock(array $criteria, $lockMode)
1532
    {
1533
        $lockSql      = '';
1534
        $conditionSql = $this->getSelectConditionSQL($criteria);
1535
1536
        switch ($lockMode) {
1537
            case LockMode::PESSIMISTIC_READ:
1538
                $lockSql = $this->platform->getReadLockSQL();
1539
1540
                break;
1541
            case LockMode::PESSIMISTIC_WRITE:
1542
1543
                $lockSql = $this->platform->getWriteLockSQL();
1544
                break;
1545
        }
1546
1547
        $lock  = $this->getLockTablesSql($lockMode);
1548
        $where = ($conditionSql ? ' WHERE ' . $conditionSql : '') . ' ';
1549
        $sql = 'SELECT 1 '
1550
             . $lock
1551
             . $where
1552
             . $lockSql;
1553
1554
        list($params, $types) = $this->expandParameters($criteria);
1555
1556
        $this->conn->executeQuery($sql, $params, $types);
1557
    }
1558
1559
    /**
1560
     * Gets the FROM and optionally JOIN conditions to lock the entity managed by this persister.
1561
     *
1562
     * @param integer $lockMode One of the Doctrine\DBAL\LockMode::* constants.
1563
     *
1564
     * @return string
1565
     */
1566 13
    protected function getLockTablesSql($lockMode)
1567
    {
1568 13
        return $this->platform->appendLockHint(
1569
            'FROM '
1570 13
            . $this->quoteStrategy->getTableName($this->class, $this->platform) . ' '
1571 13
            . $this->getSQLTableAlias($this->class->name),
1572 13
            $lockMode
1573
        );
1574
    }
1575
1576
    /**
1577
     * Gets the Select Where Condition from a Criteria object.
1578
     *
1579
     * @param \Doctrine\Common\Collections\Criteria $criteria
1580
     *
1581
     * @return string
1582
     */
1583 58
    protected function getSelectConditionCriteriaSQL(Criteria $criteria)
1584
    {
1585 58
        $expression = $criteria->getWhereExpression();
1586
1587 58
        if ($expression === null) {
1588 2
            return '';
1589
        }
1590
1591 57
        $visitor = new SqlExpressionVisitor($this, $this->class);
1592
1593 57
        return $visitor->dispatch($expression);
1594
    }
1595
1596
    /**
1597
     * {@inheritdoc}
1598
     */
1599 614
    public function getSelectConditionStatementSQL($field, $value, $assoc = null, $comparison = null)
1600
    {
1601 614
        $selectedColumns = [];
1602 614
        $columns         = $this->getSelectConditionStatementColumnSQL($field, $assoc);
1603
1604 610
        if (count($columns) > 1 && $comparison === Comparison::IN) {
1605
            /*
1606
             *  @todo try to support multi-column IN expressions.
1607
             *  Example: (col1, col2) IN (('val1A', 'val2A'), ('val1B', 'val2B'))
1608
             */
1609 1
            throw ORMException::cantUseInOperatorOnCompositeKeys();
1610
        }
1611
1612 609
        foreach ($columns as $column) {
1613 609
            $placeholder = '?';
1614
1615 609
            if (isset($this->class->fieldMappings[$field]['requireSQLConversion'])) {
1616 1
                $type        = Type::getType($this->class->fieldMappings[$field]['type']);
1617 1
                $placeholder = $type->convertToDatabaseValueSQL($placeholder, $this->platform);
1618
            }
1619
1620 609
            if (null !== $comparison) {
1621
                // special case null value handling
1622 61
                if (($comparison === Comparison::EQ || $comparison === Comparison::IS) && null ===$value) {
1623 6
                    $selectedColumns[] = $column . ' IS NULL';
1624
1625 6
                    continue;
1626
                }
1627
1628 55
                if ($comparison === Comparison::NEQ && null === $value) {
1629 3
                    $selectedColumns[] = $column . ' IS NOT NULL';
1630
1631 3
                    continue;
1632
                }
1633
1634 52
                $selectedColumns[] = $column . ' ' . sprintf(self::$comparisonMap[$comparison], $placeholder);
1635
1636 52
                continue;
1637
            }
1638
1639 581
            if (is_array($value)) {
1640 14
                $in = sprintf('%s IN (%s)', $column, $placeholder);
1641
1642 14
                if (false !== array_search(null, $value, true)) {
1643 4
                    $selectedColumns[] = sprintf('(%s OR %s IS NULL)', $in, $column);
1644
1645 4
                    continue;
1646
                }
1647
1648 10
                $selectedColumns[] = $in;
1649
1650 10
                continue;
1651
            }
1652
1653 570
            if (null === $value) {
1654 9
                $selectedColumns[] = sprintf('%s IS NULL', $column);
1655
1656 9
                continue;
1657
            }
1658
1659 562
            $selectedColumns[] = sprintf('%s = %s', $column, $placeholder);
1660
        }
1661
1662 609
        return implode(' AND ', $selectedColumns);
1663
    }
1664
1665
    /**
1666
     * Builds the left-hand-side of a where condition statement.
1667
     *
1668
     * @param string     $field
1669
     * @param array|null $assoc
1670
     *
1671
     * @return string[]
1672
     *
1673
     * @throws \Doctrine\ORM\ORMException
1674
     */
1675 614
    private function getSelectConditionStatementColumnSQL($field, $assoc = null)
1676
    {
1677 614
        if (isset($this->class->fieldMappings[$field])) {
1678 518
            $className = (isset($this->class->fieldMappings[$field]['inherited']))
1679 54
                ? $this->class->fieldMappings[$field]['inherited']
1680 518
                : $this->class->name;
1681
1682 518
            return [$this->getSQLTableAlias($className) . '.' . $this->quoteStrategy->getColumnName($field, $this->class, $this->platform)];
1683
        }
1684
1685 296
        if (isset($this->class->associationMappings[$field])) {
1686 149
            $association = $this->class->associationMappings[$field];
1687
            // Many-To-Many requires join table check for joinColumn
1688 149
            $columns = [];
1689 149
            $class   = $this->class;
1690
1691 149
            if ($association['type'] === ClassMetadata::MANY_TO_MANY) {
1692 3
                if ( ! $association['isOwningSide']) {
1693 2
                    $association = $assoc;
1694
                }
1695
1696 3
                $joinTableName = $this->quoteStrategy->getJoinTableName($association, $class, $this->platform);
1697 3
                $joinColumns   = $assoc['isOwningSide']
1698 2
                    ? $association['joinTable']['joinColumns']
1699 3
                    : $association['joinTable']['inverseJoinColumns'];
1700
1701
1702 3
                foreach ($joinColumns as $joinColumn) {
1703 3
                    $columns[] = $joinTableName . '.' . $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
1704
                }
1705
1706
            } else {
1707 147
                if ( ! $association['isOwningSide']) {
1708 1
                    throw ORMException::invalidFindByInverseAssociation($this->class->name, $field);
1709
                }
1710
1711 146
                $className  = (isset($association['inherited']))
1712 11
                    ? $association['inherited']
1713 146
                    : $this->class->name;
1714
1715 146
                foreach ($association['joinColumns'] as $joinColumn) {
1716 146
                    $columns[] = $this->getSQLTableAlias($className) . '.' . $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
1717
                }
1718
            }
1719 148
            return $columns;
1720
        }
1721
1722 164
        if ($assoc !== null && strpos($field, " ") === false && strpos($field, "(") === false) {
1723
            // very careless developers could potentially open up this normally hidden api for userland attacks,
1724
            // therefore checking for spaces and function calls which are not allowed.
1725
1726
            // found a join column condition, not really a "field"
1727 161
            return [$field];
1728
        }
1729
1730 3
        throw ORMException::unrecognizedField($field);
1731
    }
1732
1733
    /**
1734
     * Gets the conditional SQL fragment used in the WHERE clause when selecting
1735
     * entities in this persister.
1736
     *
1737
     * Subclasses are supposed to override this method if they intend to change
1738
     * or alter the criteria by which entities are selected.
1739
     *
1740
     * @param array      $criteria
1741
     * @param array|null $assoc
1742
     *
1743
     * @return string
1744
     */
1745 608
    protected function getSelectConditionSQL(array $criteria, $assoc = null)
1746
    {
1747 608
        $conditions = [];
1748
1749 608
        foreach ($criteria as $field => $value) {
1750 583
            $conditions[] = $this->getSelectConditionStatementSQL($field, $value, $assoc);
1751
        }
1752
1753 605
        return implode(' AND ', $conditions);
1754
    }
1755
1756
    /**
1757
     * {@inheritdoc}
1758
     */
1759 5
    public function getOneToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null)
1760
    {
1761 5
        $this->switchPersisterContext($offset, $limit);
1762
1763 5
        $stmt = $this->getOneToManyStatement($assoc, $sourceEntity, $offset, $limit);
1764
1765 5
        return $this->loadArrayFromStatement($assoc, $stmt);
1766
    }
1767
1768
    /**
1769
     * {@inheritdoc}
1770
     */
1771 77
    public function loadOneToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll)
1772
    {
1773 77
        $stmt = $this->getOneToManyStatement($assoc, $sourceEntity);
1774
1775 77
        return $this->loadCollectionFromStatement($assoc, $stmt, $coll);
1776
    }
1777
1778
    /**
1779
     * Builds criteria and execute SQL statement to fetch the one to many entities from.
1780
     *
1781
     * @param array    $assoc
1782
     * @param object   $sourceEntity
1783
     * @param int|null $offset
1784
     * @param int|null $limit
1785
     *
1786
     * @return \Doctrine\DBAL\Statement
1787
     */
1788 82
    private function getOneToManyStatement(array $assoc, $sourceEntity, $offset = null, $limit = null)
1789
    {
1790 82
        $this->switchPersisterContext($offset, $limit);
1791
1792 82
        $criteria    = [];
1793 82
        $parameters  = [];
1794 82
        $owningAssoc = $this->class->associationMappings[$assoc['mappedBy']];
1795 82
        $sourceClass = $this->em->getClassMetadata($assoc['sourceEntity']);
1796 82
        $tableAlias  = $this->getSQLTableAlias($owningAssoc['inherited'] ?? $this->class->name);
1797
1798 82
        foreach ($owningAssoc['targetToSourceKeyColumns'] as $sourceKeyColumn => $targetKeyColumn) {
1799 82
            if ($sourceClass->containsForeignIdentifier) {
1800 4
                $field = $sourceClass->getFieldForColumn($sourceKeyColumn);
1801 4
                $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
1802
1803 4
                if (isset($sourceClass->associationMappings[$field])) {
1804 4
                    $value = $this->em->getUnitOfWork()->getEntityIdentifier($value);
1805 4
                    $value = $value[$this->em->getClassMetadata($sourceClass->associationMappings[$field]['targetEntity'])->identifier[0]];
1806
                }
1807
1808 4
                $criteria[$tableAlias . "." . $targetKeyColumn] = $value;
1809 4
                $parameters[]                                   = [
1810 4
                    'value' => $value,
1811 4
                    'field' => $field,
1812 4
                    'class' => $sourceClass,
1813
                ];
1814
1815 4
                continue;
1816
            }
1817
1818 79
            $field = $sourceClass->fieldNames[$sourceKeyColumn];
1819 79
            $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
1820
1821 79
            $criteria[$tableAlias . "." . $targetKeyColumn] = $value;
1822 79
            $parameters[] = [
1823 79
                'value' => $value,
1824 79
                'field' => $field,
1825 79
                'class' => $sourceClass,
1826
            ];
1827
1828
        }
1829
1830 82
        $sql                  = $this->getSelectSQL($criteria, $assoc, null, $limit, $offset);
1831 82
        list($params, $types) = $this->expandToManyParameters($parameters);
1832
1833 82
        return $this->conn->executeQuery($sql, $params, $types);
1834
    }
1835
1836
    /**
1837
     * {@inheritdoc}
1838
     */
1839 584
    public function expandParameters($criteria)
1840
    {
1841 584
        $params = [];
1842 584
        $types  = [];
1843
1844 584
        foreach ($criteria as $field => $value) {
1845 559
            if ($value === null) {
1846 3
                continue; // skip null values.
1847
            }
1848
1849 557
            $types  = array_merge($types, $this->getTypes($field, $value, $this->class));
1850 557
            $params = array_merge($params, $this->getValues($value));
1851
        }
1852
1853 584
        return [$params, $types];
1854
    }
1855
1856
    /**
1857
     * Expands the parameters from the given criteria and use the correct binding types if found,
1858
     * specialized for OneToMany or ManyToMany associations.
1859
     *
1860
     * @param mixed[][] $criteria an array of arrays containing following:
1861
     *                             - field to which each criterion will be bound
1862
     *                             - value to be bound
1863
     *                             - class to which the field belongs to
1864
     *
1865
     *
1866
     * @return array
1867
     */
1868 157
    private function expandToManyParameters($criteria)
1869
    {
1870 157
        $params = [];
1871 157
        $types  = [];
1872
1873 157
        foreach ($criteria as $criterion) {
1874 157
            if ($criterion['value'] === null) {
1875 6
                continue; // skip null values.
1876
            }
1877
1878 151
            $types  = array_merge($types, $this->getTypes($criterion['field'], $criterion['value'], $criterion['class']));
1879 151
            $params = array_merge($params, $this->getValues($criterion['value']));
1880
        }
1881
1882 157
        return [$params, $types];
1883
    }
1884
1885
    /**
1886
     * Infers field types to be used by parameter type casting.
1887
     *
1888
     * @param string        $field
1889
     * @param mixed         $value
1890
     * @param ClassMetadata $class
1891
     *
1892
     * @return array
1893
     *
1894
     * @throws \Doctrine\ORM\Query\QueryException
1895
     */
1896 717
    private function getTypes($field, $value, ClassMetadata $class)
1897
    {
1898 717
        $types = [];
1899
1900
        switch (true) {
1901 717
            case (isset($class->fieldMappings[$field])):
1902 657
                $types = array_merge($types, [$class->fieldMappings[$field]['type']]);
1903 657
                break;
1904
1905 148
            case (isset($class->associationMappings[$field])):
1906 147
                $assoc = $class->associationMappings[$field];
1907 147
                $class = $this->em->getClassMetadata($assoc['targetEntity']);
1908
1909 147
                if (! $assoc['isOwningSide']) {
1910 2
                    $assoc = $class->associationMappings[$assoc['mappedBy']];
0 ignored issues
show
Accessing associationMappings on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1911 2
                    $class = $this->em->getClassMetadata($assoc['targetEntity']);
1912
                }
1913
1914 147
                $columns = $assoc['type'] === ClassMetadata::MANY_TO_MANY
1915 3
                    ? $assoc['relationToTargetKeyColumns']
1916 147
                    : $assoc['sourceToTargetKeyColumns'];
1917
1918 147
                foreach ($columns as $column){
1919 147
                    $types[] = PersisterHelper::getTypeOfColumn($column, $class, $this->em);
1920
                }
1921 147
                break;
1922
1923
            default:
1924 1
                $types[] = null;
1925 1
                break;
1926
        }
1927
1928 717
        if (is_array($value)) {
1929 16
            return array_map(function ($type) {
1930 16
                $type = Type::getType($type);
1931
1932 16
                return $type->getBindingType() + Connection::ARRAY_PARAM_OFFSET;
1933 16
            }, $types);
1934
        }
1935
1936 707
        return $types;
1937
    }
1938
1939
    /**
1940
     * Retrieves the parameters that identifies a value.
1941
     *
1942
     * @param mixed $value
1943
     *
1944
     * @return array
1945
     */
1946 591
    private function getValues($value)
1947
    {
1948 591
        if (is_array($value)) {
1949 16
            $newValue = [];
1950
1951 16
            foreach ($value as $itemValue) {
1952 16
                $newValue = array_merge($newValue, $this->getValues($itemValue));
1953
            }
1954
1955 16
            return [$newValue];
1956
        }
1957
1958 591
        if (is_object($value) && $this->em->getMetadataFactory()->hasMetadataFor(ClassUtils::getClass($value))) {
1959 44
            $class = $this->em->getClassMetadata(get_class($value));
1960 44
            if ($class->isIdentifierComposite) {
0 ignored issues
show
Accessing isIdentifierComposite on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
1961 3
                $newValue = [];
1962
1963 3
                foreach ($class->getIdentifierValues($value) as $innerValue) {
1964 3
                    $newValue = array_merge($newValue, $this->getValues($innerValue));
1965
                }
1966
1967 3
                return $newValue;
1968
            }
1969
        }
1970
1971 591
        return [$this->getIndividualValue($value)];
1972
    }
1973
1974
    /**
1975
     * Retrieves an individual parameter value.
1976
     *
1977
     * @param mixed $value
1978
     *
1979
     * @return mixed
1980
     */
1981 591
    private function getIndividualValue($value)
1982
    {
1983 591
        if ( ! is_object($value) || ! $this->em->getMetadataFactory()->hasMetadataFor(ClassUtils::getClass($value))) {
1984 588
            return $value;
1985
        }
1986
1987 44
        return $this->em->getUnitOfWork()->getSingleIdentifierValue($value);
1988
    }
1989
1990
    /**
1991
     * {@inheritdoc}
1992
     */
1993 14
    public function exists($entity, Criteria $extraConditions = null)
1994
    {
1995 14
        $criteria = $this->class->getIdentifierValues($entity);
1996
1997 14
        if ( ! $criteria) {
1998 2
            return false;
1999
        }
2000
2001 13
        $alias = $this->getSQLTableAlias($this->class->name);
2002
2003
        $sql = 'SELECT 1 '
2004 13
             . $this->getLockTablesSql(null)
2005 13
             . ' WHERE ' . $this->getSelectConditionSQL($criteria);
2006
2007 13
        list($params, $types) = $this->expandParameters($criteria);
2008
2009 13
        if (null !== $extraConditions) {
2010 9
            $sql                                 .= ' AND ' . $this->getSelectConditionCriteriaSQL($extraConditions);
2011 9
            list($criteriaParams, $criteriaTypes) = $this->expandCriteriaParameters($extraConditions);
2012
2013 9
            $params = array_merge($params, $criteriaParams);
2014 9
            $types  = array_merge($types, $criteriaTypes);
2015
        }
2016
2017 13
        if ($filterSql = $this->generateFilterConditionSQL($this->class, $alias)) {
2018 3
            $sql .= ' AND ' . $filterSql;
2019
        }
2020
2021 13
        return (bool) $this->conn->fetchColumn($sql, $params, 0, $types);
2022
    }
2023
2024
    /**
2025
     * Generates the appropriate join SQL for the given join column.
2026
     *
2027
     * @param array $joinColumns The join columns definition of an association.
2028
     *
2029
     * @return string LEFT JOIN if one of the columns is nullable, INNER JOIN otherwise.
2030
     */
2031 13
    protected function getJoinSQLForJoinColumns($joinColumns)
2032
    {
2033
        // if one of the join columns is nullable, return left join
2034 13
        foreach ($joinColumns as $joinColumn) {
2035 13
             if ( ! isset($joinColumn['nullable']) || $joinColumn['nullable']) {
2036 13
                 return 'LEFT JOIN';
2037
             }
2038
        }
2039
2040 5
        return 'INNER JOIN';
2041
    }
2042
2043
    /**
2044
     * {@inheritdoc}
2045
     */
2046 602
    public function getSQLColumnAlias($columnName)
2047
    {
2048 602
        return $this->quoteStrategy->getColumnAlias($columnName, $this->currentPersisterContext->sqlAliasCounter++, $this->platform);
2049
    }
2050
2051
    /**
2052
     * Generates the filter SQL for a given entity and table alias.
2053
     *
2054
     * @param ClassMetadata $targetEntity     Metadata of the target entity.
2055
     * @param string        $targetTableAlias The table alias of the joined/selected table.
2056
     *
2057
     * @return string The SQL query part to add to a query.
2058
     */
2059 626
    protected function generateFilterConditionSQL(ClassMetadata $targetEntity, $targetTableAlias)
2060
    {
2061 626
        $filterClauses = [];
2062
2063 626
        foreach ($this->em->getFilters()->getEnabledFilters() as $filter) {
2064 22
            if ('' !== $filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias)) {
2065 22
                $filterClauses[] = '(' . $filterExpr . ')';
2066
            }
2067
        }
2068
2069 626
        $sql = implode(' AND ', $filterClauses);
2070
2071 626
        return $sql ? "(" . $sql . ")" : ""; // Wrap again to avoid "X or Y and FilterConditionSQL"
2072
    }
2073
2074
    /**
2075
     * Switches persister context according to current query offset/limits
2076
     *
2077
     * This is due to the fact that to-many associations cannot be fetch-joined when a limit is involved
2078
     *
2079
     * @param null|int $offset
2080
     * @param null|int $limit
2081
     */
2082 608
    protected function switchPersisterContext($offset, $limit)
2083
    {
2084 608
        if (null === $offset && null === $limit) {
2085 595
            $this->currentPersisterContext = $this->noLimitsContext;
2086
2087 595
            return;
2088
        }
2089
2090 42
        $this->currentPersisterContext = $this->limitsHandlingContext;
2091 42
    }
2092
}
2093