Completed
Push — master ( 027344...6c7bb5 )
by Guilherme
64:45 queued 10s
created

ORM/Persisters/Entity/JoinedSubclassPersister.php (1 issue)

Severity
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM\Persisters\Entity;
6
7
use Doctrine\Common\Collections\Criteria;
8
use Doctrine\DBAL\LockMode;
9
use Doctrine\DBAL\Statement;
10
use Doctrine\DBAL\Types\Type;
11
use Doctrine\ORM\Mapping\AssociationMetadata;
12
use Doctrine\ORM\Mapping\FieldMetadata;
13
use Doctrine\ORM\Mapping\GeneratorType;
14
use Doctrine\ORM\Mapping\JoinColumnMetadata;
15
use Doctrine\ORM\Mapping\ManyToManyAssociationMetadata;
16
use Doctrine\ORM\Mapping\ToManyAssociationMetadata;
17
use Doctrine\ORM\Mapping\ToOneAssociationMetadata;
18
use Doctrine\ORM\Mapping\TransientMetadata;
19
use Doctrine\ORM\Utility\PersisterHelper;
20
use function array_combine;
21
use function array_keys;
22
use function implode;
23
use function is_array;
24
25
/**
26
 * The joined subclass persister maps a single entity instance to several tables in the
27
 * database as it is defined by the <tt>Class Table Inheritance</tt> strategy.
28
 *
29
 * @see https://martinfowler.com/eaaCatalog/classTableInheritance.html
30
 */
31
class JoinedSubclassPersister extends AbstractEntityInheritancePersister
32
{
33
    /**
34
     * {@inheritdoc}
35 295
     */
36
    public function insert($entity)
37 295
    {
38 289
        $rootClass      = ! $this->class->isRootEntity()
39 295
            ? $this->em->getClassMetadata($this->class->getRootClassName())
40 295
            : $this->class;
41
        $generationPlan = $this->class->getValueGenerationPlan();
42
43 295
        // Prepare statement for the root table
44 295
        $rootPersister = $this->em->getUnitOfWork()->getEntityPersister($rootClass->getClassName());
45 295
        $rootTableName = $rootClass->getTableName();
46
        $rootTableStmt = $this->conn->prepare($rootPersister->getInsertSQL());
47
48 295
        // Prepare statements for sub tables.
49
        $subTableStmts = [];
50 295
51 289
        if ($rootClass !== $this->class) {
52
            $subTableStmts[$this->class->getTableName()] = $this->conn->prepare($this->getInsertSQL());
53
        }
54 295
55
        $parentClass = $this->class;
56 295
57 289
        while (($parentClass = $parentClass->getParent()) !== null) {
58
            $parentTableName = $parentClass->getTableName();
59 289
60 170
            if ($parentClass !== $rootClass) {
61
                $parentPersister = $this->em->getUnitOfWork()->getEntityPersister($parentClass->getClassName());
62 170
63
                $subTableStmts[$parentTableName] = $this->conn->prepare($parentPersister->getInsertSQL());
64
            }
65
        }
66
67
        // Execute all inserts. For each entity:
68
        // 1) Insert on root table
69 295
        // 2) Insert on sub tables
70
        $insertData = $this->prepareInsertData($entity);
71
72 295
        // Execute insert on root table
73
        $paramIndex = 1;
74 295
75 295
        foreach ($insertData[$rootTableName] as $columnName => $value) {
76
            $type = $this->columns[$columnName]->getType();
77 295
78
            $rootTableStmt->bindValue($paramIndex++, $value, $type);
79
        }
80 295
81
        $rootTableStmt->execute();
82 295
83 291
        if ($generationPlan->containsDeferred()) {
84 291
            $generationPlan->executeDeferred($this->em, $entity);
85
            $id = $this->getIdentifier($entity);
86 4
        } else {
87
            $id = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
88
        }
89 295
90 9
        if ($this->class->isVersioned()) {
91
            $this->assignDefaultVersionValue($entity, $id);
92
        }
93
94
        // Execute inserts on subtables.
95 295
        // The order doesn't matter because all child tables link to the root table via FK.
96
        foreach ($subTableStmts as $tableName => $stmt) {
97 289
            /** @var Statement $stmt */
98 289
            $paramIndex = 1;
99
            $data       = $insertData[$tableName] ?? [];
100 289
101 289
            foreach ((array) $id as $idName => $idVal) {
102
                $type = Type::getType('string');
103 289
104 289
                if (isset($this->columns[$idName])) {
105
                    $type = $this->columns[$idName]->getType();
106
                }
107 289
108
                $stmt->bindValue($paramIndex++, $idVal, $type);
109
            }
110 289
111 224
            foreach ($data as $columnName => $value) {
112 224
                if (! is_array($id) || ! isset($id[$columnName])) {
113
                    $type = $this->columns[$columnName]->getType();
114 224
115
                    $stmt->bindValue($paramIndex++, $value, $type);
116
                }
117
            }
118 289
119
            $stmt->execute();
120
        }
121 295
122
        $rootTableStmt->closeCursor();
123 295
124 289
        foreach ($subTableStmts as $stmt) {
125
            $stmt->closeCursor();
126 295
        }
127
    }
128
129
    /**
130
     * {@inheritdoc}
131 30
     */
132
    public function update($entity)
133 30
    {
134
        $updateData = $this->prepareUpdateData($entity);
135 30
136
        if (! $updateData) {
137
            return;
138
        }
139 30
140
        $isVersioned = $this->class->isVersioned();
141 30
142 30
        foreach ($updateData as $tableName => $data) {
143
            $versioned = $isVersioned && $this->class->versionProperty->getTableName() === $tableName;
144 30
145
            $this->updateTable($entity, $this->platform->quoteIdentifier($tableName), $data, $versioned);
146
        }
147
148
        // Make sure the table with the version column is updated even if no columns on that
149 29
        // table were affected.
150 5
        if ($isVersioned) {
151 5
            $versionedClass = $this->class->versionProperty->getDeclaringClass();
152
            $versionedTable = $versionedClass->getTableName();
153 5
154 2
            if (! isset($updateData[$versionedTable])) {
155
                $tableName = $versionedClass->table->getQuotedQualifiedName($this->platform);
156 2
157
                $this->updateTable($entity, $tableName, [], true);
158
            }
159 4
160
            $identifiers = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
161 4
162
            $this->assignDefaultVersionValue($entity, $identifiers);
163 28
        }
164
    }
165
166
    /**
167
     * {@inheritdoc}
168 4
     */
169
    public function delete($entity)
170 4
    {
171 4
        $identifier = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
172
        $id         = array_combine(array_keys($this->class->getIdentifierColumns($this->em)), $identifier);
173 4
174
        $this->deleteJoinTableRecords($identifier);
175
176
        // If the database platform supports FKs, just
177 4
        // delete the row from the root table. Cascades do the rest.
178
        if ($this->platform->supportsForeignKeyConstraints()) {
179
            $rootClass = $this->em->getClassMetadata($this->class->getRootClassName());
180
            $rootTable = $rootClass->table->getQuotedQualifiedName($this->platform);
181
182
            return (bool) $this->conn->delete($rootTable, $id);
183
        }
184
185 4
        // Delete from all tables individually, starting from this class' table up to the root table.
186 4
        $rootTable    = $this->class->table->getQuotedQualifiedName($this->platform);
187 4
        $affectedRows = $this->conn->delete($rootTable, $id);
188
        $parentClass  = $this->class;
189 4
190 3
        while (($parentClass = $parentClass->getParent()) !== null) {
191
            $parentTable = $parentClass->table->getQuotedQualifiedName($this->platform);
192 3
193
            $this->conn->delete($parentTable, $id);
194
        }
195 4
196
        return (bool) $affectedRows;
197
    }
198
199
    /**
200
     * {@inheritdoc}
201 68
     */
202
    public function getSelectSQL(
203
        $criteria,
204
        ?AssociationMetadata $association = null,
205
        $lockMode = null,
206
        $limit = null,
207
        $offset = null,
208
        array $orderBy = []
209 68
    ) {
210
        $this->switchPersisterContext($offset, $limit);
211 68
212 68
        $baseTableAlias = $this->getSQLTableAlias($this->class->getTableName());
213
        $joinSql        = $this->getJoinSql($baseTableAlias);
214 68
215 2
        if ($association instanceof ManyToManyAssociationMetadata) {
216
            $joinSql .= $this->getSelectManyToManyJoinSQL($association);
217
        }
218 68
219 1
        if ($association instanceof ToManyAssociationMetadata && $association->getOrderBy()) {
220
            $orderBy = $association->getOrderBy();
221
        }
222 68
223 68
        $orderBySql   = $this->getOrderBySQL($orderBy, $baseTableAlias);
224
        $conditionSql = $criteria instanceof Criteria
225 68
            ? $this->getSelectConditionCriteriaSQL($criteria)
226
            : $this->getSelectConditionSQL($criteria, $association);
227
228 68
        // If the current class in the root entity, add the filters
229 68
        $rootClass  = $this->em->getClassMetadata($this->class->getRootClassName());
230 68
        $tableAlias = $this->getSQLTableAlias($rootClass->getTableName());
231
        $filterSql  = $this->generateFilterConditionSQL($rootClass, $tableAlias);
232 68
233 4
        if ($filterSql) {
234 2
            $conditionSql .= $conditionSql
235 4
                ? ' AND ' . $filterSql
236
                : $filterSql;
237
        }
238 68
239
        $lockSql = '';
240
241 68
        switch ($lockMode) {
242
            case LockMode::PESSIMISTIC_READ:
243
                $lockSql = ' ' . $this->platform->getReadLockSQL();
244
                break;
245 68
246
            case LockMode::PESSIMISTIC_WRITE:
247
                $lockSql = ' ' . $this->platform->getWriteLockSQL();
248
                break;
249
        }
250 68
251 68
        $tableName  = $this->class->table->getQuotedQualifiedName($this->platform);
252 68
        $from       = ' FROM ' . $tableName . ' ' . $baseTableAlias;
253 68
        $where      = $conditionSql !== '' ? ' WHERE ' . $conditionSql : '';
254 68
        $lock       = $this->platform->appendLockHint($from, $lockMode);
255 68
        $columnList = $this->getSelectColumnsSQL();
256 68
        $query      = 'SELECT ' . $columnList
257 68
                    . $lock
258 68
                    . $joinSql
259 68
                    . $where
260
                    . $orderBySql;
261 68
262
        return $this->platform->modifyLimitQuery($query, $limit, $offset ?? 0) . $lockSql;
263
    }
264
265
    /**
266
     * {@inheritDoc}
267 6
     */
268
    public function getCountSQL($criteria = [])
269 6
    {
270 6
        $tableName      = $this->class->table->getQuotedQualifiedName($this->platform);
271 6
        $baseTableAlias = $this->getSQLTableAlias($this->class->getTableName());
272
        $joinSql        = $this->getJoinSql($baseTableAlias);
273 6
274 1
        $conditionSql = $criteria instanceof Criteria
275 6
            ? $this->getSelectConditionCriteriaSQL($criteria)
276
            : $this->getSelectConditionSQL($criteria);
277 6
278 6
        $rootClass  = $this->em->getClassMetadata($this->class->getRootClassName());
279 6
        $tableAlias = $this->getSQLTableAlias($rootClass->getTableName());
280
        $filterSql  = $this->generateFilterConditionSQL($rootClass, $tableAlias);
281 6
282 1
        if ($filterSql !== '') {
283 1
            $conditionSql = $conditionSql
284 1
                ? $conditionSql . ' AND ' . $filterSql
285
                : $filterSql;
286
        }
287
288 6
        return 'SELECT COUNT(*) '
289 6
            . 'FROM ' . $tableName . ' ' . $baseTableAlias
290 6
            . $joinSql
291
            . (empty($conditionSql) ? '' : ' WHERE ' . $conditionSql);
292
    }
293
294
    /**
295
     * {@inheritdoc}
296 6
     */
297
    protected function getLockTablesSql($lockMode)
298 6
    {
299 6
        $joinSql           = '';
300 6
        $identifierColumns = $this->class->getIdentifierColumns($this->em);
301
        $baseTableAlias    = $this->getSQLTableAlias($this->class->getTableName());
302
303 6
        // INNER JOIN parent tables
304
        $parentClass = $this->class;
305 6
306 5
        while (($parentClass = $parentClass->getParent()) !== null) {
307 5
            $conditions = [];
308 5
            $tableName  = $parentClass->table->getQuotedQualifiedName($this->platform);
309 5
            $tableAlias = $this->getSQLTableAlias($parentClass->getTableName());
310
            $joinSql   .= ' INNER JOIN ' . $tableName . ' ' . $tableAlias . ' ON ';
311 5
312 5
            foreach ($identifierColumns as $idColumn) {
313
                $quotedColumnName = $this->platform->quoteIdentifier($idColumn->getColumnName());
314 5
315
                $conditions[] = $baseTableAlias . '.' . $quotedColumnName . ' = ' . $tableAlias . '.' . $quotedColumnName;
316
            }
317 5
318
            $joinSql .= implode(' AND ', $conditions);
319
        }
320 6
321
        return parent::getLockTablesSql($lockMode) . $joinSql;
322
    }
323
324
    /**
325
     * Ensure this method is never called. This persister overrides getSelectEntitiesSQL directly.
326
     *
327
     * @return string
328 68
     */
329
    protected function getSelectColumnsSQL()
330
    {
331 68
        // Create the column list fragment only once
332 12
        if ($this->currentPersisterContext->selectColumnListSql !== null) {
333
            return $this->currentPersisterContext->selectColumnListSql;
334
        }
335 68
336
        $this->currentPersisterContext->rsm->addEntityResult($this->class->getClassName(), 'r');
337 68
338
        $columnList = [];
339
340 68
        // Add columns
341 68
        foreach ($this->class->getPropertiesIterator() as $fieldName => $property) {
342 68
            if ($property instanceof FieldMetadata) {
343
                $columnList[] = $this->getSelectColumnSQL($fieldName, $property->getDeclaringClass());
344 68
345
                continue;
346
            }
347 56
348 44
            if (! ($property instanceof ToOneAssociationMetadata) || ! $property->isOwningSide()) {
349
                continue;
350
            }
351 55
352
            $targetClass = $this->em->getClassMetadata($property->getTargetEntity());
353 55
354
            foreach ($property->getJoinColumns() as $joinColumn) {
355 55
                /** @var JoinColumnMetadata $joinColumn */
356
                $referencedColumnName = $joinColumn->getReferencedColumnName();
357 55
358
                if (! $joinColumn->getType()) {
359
                    $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
360
                }
361 55
362
                $columnList[] = $this->getSelectJoinColumnSQL($joinColumn);
363
            }
364
        }
365
366 68
        // Add discriminator column (DO NOT ALIAS, see AbstractEntityInheritancePersister#processSQLResult).
367 68
        $discrColumn      = $this->class->discriminatorColumn;
368 68
        $discrTableAlias  = $this->getSQLTableAlias($discrColumn->getTableName());
369 68
        $discrColumnName  = $discrColumn->getColumnName();
370 68
        $discrColumnType  = $discrColumn->getType();
371 68
        $resultColumnName = $this->platform->getSQLResultCasing($discrColumnName);
372
        $quotedColumnName = $this->platform->quoteIdentifier($discrColumn->getColumnName());
373 68
374 68
        $this->currentPersisterContext->rsm->setDiscriminatorColumn('r', $resultColumnName);
375
        $this->currentPersisterContext->rsm->addMetaResult('r', $resultColumnName, $discrColumnName, false, $discrColumnType);
376 68
377
        $columnList[] = $discrColumnType->convertToDatabaseValueSQL($discrTableAlias . '.' . $quotedColumnName, $this->platform);
378
379 68
        // sub tables
380 50
        foreach ($this->class->getSubClasses() as $subClassName) {
381
            $subClass = $this->em->getClassMetadata($subClassName);
382
383 50
            // Add columns
384 50
            foreach ($subClass->getPropertiesIterator() as $fieldName => $property) {
385 50
                if ($subClass->isInheritedProperty($fieldName)) {
386
                    continue;
387
                }
388
389 46
                switch (true) {
390 46
                    case $property instanceof FieldMetadata:
391 46
                        $columnList[] = $this->getSelectColumnSQL($fieldName, $subClass);
392
                        break;
393 32
394 32
                    case $property instanceof ToOneAssociationMetadata && $property->isOwningSide():
395
                        $targetClass = $this->em->getClassMetadata($property->getTargetEntity());
396 32
397
                        foreach ($property->getJoinColumns() as $joinColumn) {
398 32
                            /** @var JoinColumnMetadata $joinColumn */
399
                            $referencedColumnName = $joinColumn->getReferencedColumnName();
400 32
401
                            if (! $joinColumn->getType()) {
402
                                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
403
                            }
404 32
405
                            $columnList[] = $this->getSelectJoinColumnSQL($joinColumn);
406
                        }
407 32
408
                        break;
409
                }
410
            }
411
        }
412 68
413
        $this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList);
414 68
415
        return $this->currentPersisterContext->selectColumnListSql;
416
    }
417
418
    /**
419
     * {@inheritdoc}
420 295
     */
421
    protected function getInsertColumnList()
422
    {
423 295
        // Identifier columns must always come first in the column list of subclasses.
424 295
        $columns       = [];
425 289
        $parentColumns = $this->class->getParent()
426 295
            ? $this->class->getIdentifierColumns($this->em)
427
            : [];
428 295
429 289
        foreach ($parentColumns as $columnName => $column) {
430
            $columns[] = $columnName;
431 289
432
            $this->columns[$columnName] = $column;
433
        }
434 295
435 295
        foreach ($this->class->getPropertiesIterator() as $name => $property) {
436 295
            if (($property instanceof FieldMetadata && ($property->isVersioned() || $this->class->isInheritedProperty($name)))
0 ignored issues
show
Consider adding parentheses for clarity. Current Interpretation: ($property instanceof Do...pping\TransientMetadata, Probably Intended Meaning: $property instanceof Doc...ping\TransientMetadata)
Loading history...
437
                || ($property instanceof AssociationMetadata && $this->class->isInheritedProperty($name)
438 293
                || $property instanceof TransientMetadata)
439
                /*|| isset($this->class->embeddedClasses[$name])*/) {
440
                continue;
441 295
            }
442 265
443 264
            if ($property instanceof AssociationMetadata) {
444
                if ($property->isOwningSide() && $property instanceof ToOneAssociationMetadata) {
445 264
                    $targetClass = $this->em->getClassMetadata($property->getTargetEntity());
446
447 264
                    foreach ($property->getJoinColumns() as $joinColumn) {
448 264
                        /** @var JoinColumnMetadata $joinColumn */
449
                        $columnName           = $joinColumn->getColumnName();
450 264
                        $referencedColumnName = $joinColumn->getReferencedColumnName();
451 12
452
                        if (! $joinColumn->getType()) {
453
                            $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
454 264
                        }
455
456 264
                        $columns[] = $columnName;
457
458
                        $this->columns[$columnName] = $joinColumn;
459
                    }
460 265
                }
461
462
                continue;
463 295
            }
464 295
465 291
            if ($this->class->getClassName() !== $this->class->getRootClassName()
466 295
                || ! $this->class->getProperty($name)->hasValueGenerator()
467
                || $this->class->getProperty($name)->getValueGenerator()->getType() !== GeneratorType::IDENTITY
468 233
                || $this->class->identifier[0] !== $name
469
            ) {
470 233
                $columnName = $property->getColumnName();
471
472 233
                $columns[] = $columnName;
473
474
                $this->columns[$columnName] = $property;
475
            }
476
        }
477 295
478 295
        // Add discriminator column if it is the topmost class.
479 295
        if ($this->class->isRootEntity()) {
480
            $discrColumn     = $this->class->discriminatorColumn;
481 295
            $discrColumnName = $discrColumn->getColumnName();
482
483 295
            $columns[] = $discrColumnName;
484
485
            $this->columns[$discrColumnName] = $discrColumn;
486 295
        }
487
488
        return $columns;
489
    }
490
491
    /**
492
     * @param string $baseTableAlias
493
     *
494 73
     * @return string
495
     */
496 73
    private function getJoinSql($baseTableAlias)
497 73
    {
498
        $joinSql           = '';
499
        $identifierColumns = $this->class->getIdentifierColumns($this->em);
500 73
501
        // INNER JOIN parent tables
502 73
        $parentClass = $this->class;
503 49
504 49
        while (($parentClass = $parentClass->getParent()) !== null) {
505 49
            $conditions = [];
506 49
            $tableName  = $parentClass->table->getQuotedQualifiedName($this->platform);
507
            $tableAlias = $this->getSQLTableAlias($parentClass->getTableName());
508 49
            $joinSql   .= ' INNER JOIN ' . $tableName . ' ' . $tableAlias . ' ON ';
509 49
510
            foreach ($identifierColumns as $idColumn) {
511 49
                $quotedColumnName = $this->platform->quoteIdentifier($idColumn->getColumnName());
512
513
                $conditions[] = $baseTableAlias . '.' . $quotedColumnName . ' = ' . $tableAlias . '.' . $quotedColumnName;
514 49
            }
515
516
            $joinSql .= implode(' AND ', $conditions);
517
        }
518 73
519 52
        // OUTER JOIN sub tables
520 52
        foreach ($this->class->getSubClasses() as $subClassName) {
521 52
            $conditions = [];
522 52
            $subClass   = $this->em->getClassMetadata($subClassName);
523 52
            $tableName  = $subClass->table->getQuotedQualifiedName($this->platform);
524
            $tableAlias = $this->getSQLTableAlias($subClass->getTableName());
525 52
            $joinSql   .= ' LEFT JOIN ' . $tableName . ' ' . $tableAlias . ' ON ';
526 52
527
            foreach ($identifierColumns as $idColumn) {
528 52
                $quotedColumnName = $this->platform->quoteIdentifier($idColumn->getColumnName());
529
530
                $conditions[] = $baseTableAlias . '.' . $quotedColumnName . ' = ' . $tableAlias . '.' . $quotedColumnName;
531 52
            }
532
533
            $joinSql .= implode(' AND ', $conditions);
534 73
        }
535
536
        return $joinSql;
537
    }
538
}
539