Failed Conditions
Push — master ( 7c9ab7...fa4d3b )
by Marco
13:03
created

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

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\VersionFieldMetadata;
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
     */
36 296
    public function insert($entity)
37
    {
38 296
        $rootClass      = ! $this->class->isRootEntity()
39 290
            ? $this->em->getClassMetadata($this->class->getRootClassName())
40 296
            : $this->class;
41 296
        $generationPlan = $this->class->getValueGenerationPlan();
42
43
        // Prepare statement for the root table
44 296
        $rootPersister = $this->em->getUnitOfWork()->getEntityPersister($rootClass->getClassName());
45 296
        $rootTableName = $rootClass->getTableName();
46 296
        $rootTableStmt = $this->conn->prepare($rootPersister->getInsertSQL());
47
48
        // Prepare statements for sub tables.
49 296
        $subTableStmts = [];
50
51 296
        if ($rootClass !== $this->class) {
52 290
            $subTableStmts[$this->class->getTableName()] = $this->conn->prepare($this->getInsertSQL());
53
        }
54
55 296
        $parentClass = $this->class;
56
57 296
        while (($parentClass = $parentClass->getParent()) !== null) {
58 290
            $parentTableName = $parentClass->getTableName();
59
60 290
            if ($parentClass !== $rootClass) {
61 171
                $parentPersister = $this->em->getUnitOfWork()->getEntityPersister($parentClass->getClassName());
62
63 171
                $subTableStmts[$parentTableName] = $this->conn->prepare($parentPersister->getInsertSQL());
64
            }
65
        }
66
67
        // Execute all inserts. For each entity:
68
        // 1) Insert on root table
69
        // 2) Insert on sub tables
70 296
        $insertData = $this->prepareInsertData($entity);
71
72
        // Execute insert on root table
73 296
        $paramIndex = 1;
74
75 296
        foreach ($insertData[$rootTableName] as $columnName => $value) {
76 296
            $type = $this->columns[$columnName]->getType();
77
78 296
            $rootTableStmt->bindValue($paramIndex++, $value, $type);
79
        }
80
81 296
        $rootTableStmt->execute();
82
83 296
        if ($generationPlan->containsDeferred()) {
84 292
            $generationPlan->executeDeferred($this->em, $entity);
85 292
            $id = $this->getIdentifier($entity);
86
        } else {
87 4
            $id = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
88
        }
89
90 296
        if ($this->class->isVersioned()) {
91 9
            $this->assignDefaultVersionValue($entity, $id);
92
        }
93
94
        // Execute inserts on subtables.
95
        // The order doesn't matter because all child tables link to the root table via FK.
96 296
        foreach ($subTableStmts as $tableName => $stmt) {
97
            /** @var Statement $stmt */
98 290
            $paramIndex = 1;
99 290
            $data       = $insertData[$tableName] ?? [];
100
101 290
            foreach ((array) $id as $idName => $idVal) {
102 290
                $type = Type::getType('string');
103
104 290
                if (isset($this->columns[$idName])) {
105 290
                    $type = $this->columns[$idName]->getType();
106
                }
107
108 290
                $stmt->bindValue($paramIndex++, $idVal, $type);
109
            }
110
111 290
            foreach ($data as $columnName => $value) {
112 225
                if (! is_array($id) || ! isset($id[$columnName])) {
113 225
                    $type = $this->columns[$columnName]->getType();
114
115 225
                    $stmt->bindValue($paramIndex++, $value, $type);
116
                }
117
            }
118
119 290
            $stmt->execute();
120
        }
121
122 296
        $rootTableStmt->closeCursor();
123
124 296
        foreach ($subTableStmts as $stmt) {
125 290
            $stmt->closeCursor();
126
        }
127 296
    }
128
129
    /**
130
     * {@inheritdoc}
131
     */
132 30
    public function update($entity)
133
    {
134 30
        $updateData = $this->prepareUpdateData($entity);
135
136 30
        if (! $updateData) {
137
            return;
138
        }
139
140 30
        $isVersioned = $this->class->isVersioned();
141
142 30
        foreach ($updateData as $tableName => $data) {
143 30
            $versioned = $isVersioned && $this->class->versionProperty->getTableName() === $tableName;
144
145 30
            $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
        // table were affected.
150 29
        if ($isVersioned) {
151 5
            $versionedClass = $this->class->versionProperty->getDeclaringClass();
152 5
            $versionedTable = $versionedClass->getTableName();
153
154 5
            if (! isset($updateData[$versionedTable])) {
155 2
                $tableName = $versionedClass->table->getQuotedQualifiedName($this->platform);
156
157 2
                $this->updateTable($entity, $tableName, [], true);
158
            }
159
160 4
            $identifiers = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
161
162 4
            $this->assignDefaultVersionValue($entity, $identifiers);
163
        }
164 28
    }
165
166
    /**
167
     * {@inheritdoc}
168
     */
169 5
    public function delete($entity)
170
    {
171 5
        $identifier = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
172 5
        $id         = array_combine(array_keys($this->class->getIdentifierColumns($this->em)), $identifier);
173
174 5
        $this->deleteJoinTableRecords($identifier);
175
176
        // If the database platform supports FKs, just
177
        // delete the row from the root table. Cascades do the rest.
178 5
        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
        // Delete from all tables individually, starting from this class' table up to the root table.
186 5
        $rootTable    = $this->class->table->getQuotedQualifiedName($this->platform);
187 5
        $affectedRows = $this->conn->delete($rootTable, $id);
188 5
        $parentClass  = $this->class;
189
190 5
        while (($parentClass = $parentClass->getParent()) !== null) {
191 4
            $parentTable = $parentClass->table->getQuotedQualifiedName($this->platform);
192
193 4
            $this->conn->delete($parentTable, $id);
194
        }
195
196 5
        return (bool) $affectedRows;
197
    }
198
199
    /**
200
     * {@inheritdoc}
201
     */
202 69
    public function getSelectSQL(
203
        $criteria,
204
        ?AssociationMetadata $association = null,
205
        $lockMode = null,
206
        $limit = null,
207
        $offset = null,
208
        array $orderBy = []
209
    ) {
210 69
        $this->switchPersisterContext($offset, $limit);
211
212 69
        $baseTableAlias = $this->getSQLTableAlias($this->class->getTableName());
213 69
        $joinSql        = $this->getJoinSql($baseTableAlias);
214
215 69
        if ($association instanceof ManyToManyAssociationMetadata) {
216 2
            $joinSql .= $this->getSelectManyToManyJoinSQL($association);
217
        }
218
219 69
        if ($association instanceof ToManyAssociationMetadata && $association->getOrderBy()) {
220 1
            $orderBy = $association->getOrderBy();
221
        }
222
223 69
        $orderBySql   = $this->getOrderBySQL($orderBy, $baseTableAlias);
224 69
        $conditionSql = $criteria instanceof Criteria
225
            ? $this->getSelectConditionCriteriaSQL($criteria)
226 69
            : $this->getSelectConditionSQL($criteria, $association);
227
228
        // If the current class in the root entity, add the filters
229 69
        $rootClass  = $this->em->getClassMetadata($this->class->getRootClassName());
230 69
        $tableAlias = $this->getSQLTableAlias($rootClass->getTableName());
231 69
        $filterSql  = $this->generateFilterConditionSQL($rootClass, $tableAlias);
0 ignored issues
show
$rootClass of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $targetEntity of Doctrine\ORM\Persisters\...ateFilterConditionSQL(). ( Ignorable by Annotation )

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

231
        $filterSql  = $this->generateFilterConditionSQL(/** @scrutinizer ignore-type */ $rootClass, $tableAlias);
Loading history...
232
233 69
        if ($filterSql) {
234 4
            $conditionSql .= $conditionSql
235 2
                ? ' AND ' . $filterSql
236 4
                : $filterSql;
237
        }
238
239 69
        $lockSql = '';
240
241
        switch ($lockMode) {
242 69
            case LockMode::PESSIMISTIC_READ:
243
                $lockSql = ' ' . $this->platform->getReadLockSQL();
244
                break;
245
246 69
            case LockMode::PESSIMISTIC_WRITE:
247
                $lockSql = ' ' . $this->platform->getWriteLockSQL();
248
                break;
249
        }
250
251 69
        $tableName  = $this->class->table->getQuotedQualifiedName($this->platform);
252 69
        $from       = ' FROM ' . $tableName . ' ' . $baseTableAlias;
253 69
        $where      = $conditionSql !== '' ? ' WHERE ' . $conditionSql : '';
254 69
        $lock       = $this->platform->appendLockHint($from, $lockMode);
255 69
        $columnList = $this->getSelectColumnsSQL();
256 69
        $query      = 'SELECT ' . $columnList
257 69
                    . $lock
258 69
                    . $joinSql
259 69
                    . $where
260 69
                    . $orderBySql;
261
262 69
        return $this->platform->modifyLimitQuery($query, $limit, $offset) . $lockSql;
263
    }
264
265
    /**
266
     * {@inheritDoc}
267
     */
268 6
    public function getCountSQL($criteria = [])
269
    {
270 6
        $tableName      = $this->class->table->getQuotedQualifiedName($this->platform);
271 6
        $baseTableAlias = $this->getSQLTableAlias($this->class->getTableName());
272 6
        $joinSql        = $this->getJoinSql($baseTableAlias);
273
274 6
        $conditionSql = $criteria instanceof Criteria
275 1
            ? $this->getSelectConditionCriteriaSQL($criteria)
276 6
            : $this->getSelectConditionSQL($criteria);
277
278 6
        $rootClass  = $this->em->getClassMetadata($this->class->getRootClassName());
279 6
        $tableAlias = $this->getSQLTableAlias($rootClass->getTableName());
280 6
        $filterSql  = $this->generateFilterConditionSQL($rootClass, $tableAlias);
281
282 6
        if ($filterSql !== '') {
283 1
            $conditionSql = $conditionSql
284 1
                ? $conditionSql . ' AND ' . $filterSql
285 1
                : $filterSql;
286
        }
287
288
        return 'SELECT COUNT(*) '
289 6
            . 'FROM ' . $tableName . ' ' . $baseTableAlias
290 6
            . $joinSql
291 6
            . (empty($conditionSql) ? '' : ' WHERE ' . $conditionSql);
292
    }
293
294
    /**
295
     * {@inheritdoc}
296
     */
297 6
    protected function getLockTablesSql($lockMode)
298
    {
299 6
        $joinSql           = '';
300 6
        $identifierColumns = $this->class->getIdentifierColumns($this->em);
301 6
        $baseTableAlias    = $this->getSQLTableAlias($this->class->getTableName());
302
303
        // INNER JOIN parent tables
304 6
        $parentClass = $this->class;
305
306 6
        while (($parentClass = $parentClass->getParent()) !== null) {
307 5
            $conditions = [];
308 5
            $tableName  = $parentClass->table->getQuotedQualifiedName($this->platform);
309 5
            $tableAlias = $this->getSQLTableAlias($parentClass->getTableName());
310 5
            $joinSql   .= ' INNER JOIN ' . $tableName . ' ' . $tableAlias . ' ON ';
311
312 5
            foreach ($identifierColumns as $idColumn) {
313 5
                $quotedColumnName = $this->platform->quoteIdentifier($idColumn->getColumnName());
314
315 5
                $conditions[] = $baseTableAlias . '.' . $quotedColumnName . ' = ' . $tableAlias . '.' . $quotedColumnName;
316
            }
317
318 5
            $joinSql .= implode(' AND ', $conditions);
319
        }
320
321 6
        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
     */
329 69
    protected function getSelectColumnsSQL()
330
    {
331
        // Create the column list fragment only once
332 69
        if ($this->currentPersisterContext->selectColumnListSql !== null) {
333 12
            return $this->currentPersisterContext->selectColumnListSql;
334
        }
335
336 69
        $this->currentPersisterContext->rsm->addEntityResult($this->class->getClassName(), 'r');
337
338 69
        $columnList = [];
339
340
        // Add columns
341 69
        foreach ($this->class->getDeclaredPropertiesIterator() as $fieldName => $property) {
342 69
            if ($property instanceof FieldMetadata) {
343 69
                $columnList[] = $this->getSelectColumnSQL($fieldName, $property->getDeclaringClass());
344
345 69
                continue;
346
            }
347
348 56
            if (! ($property instanceof ToOneAssociationMetadata) || ! $property->isOwningSide()) {
349 44
                continue;
350
            }
351
352 55
            $targetClass = $this->em->getClassMetadata($property->getTargetEntity());
353
354 55
            foreach ($property->getJoinColumns() as $joinColumn) {
355
                /** @var JoinColumnMetadata $joinColumn */
356 55
                $referencedColumnName = $joinColumn->getReferencedColumnName();
357
358 55
                if (! $joinColumn->getType()) {
359
                    $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
360
                }
361
362 55
                $columnList[] = $this->getSelectJoinColumnSQL($joinColumn);
363
            }
364
        }
365
366
        // Add discriminator column (DO NOT ALIAS, see AbstractEntityInheritancePersister#processSQLResult).
367 69
        $discrColumn      = $this->class->discriminatorColumn;
368 69
        $discrTableAlias  = $this->getSQLTableAlias($discrColumn->getTableName());
369 69
        $discrColumnName  = $discrColumn->getColumnName();
370 69
        $discrColumnType  = $discrColumn->getType();
371 69
        $resultColumnName = $this->platform->getSQLResultCasing($discrColumnName);
372 69
        $quotedColumnName = $this->platform->quoteIdentifier($discrColumn->getColumnName());
373
374 69
        $this->currentPersisterContext->rsm->setDiscriminatorColumn('r', $resultColumnName);
375 69
        $this->currentPersisterContext->rsm->addMetaResult('r', $resultColumnName, $discrColumnName, false, $discrColumnType);
376
377 69
        $columnList[] = $discrColumnType->convertToDatabaseValueSQL($discrTableAlias . '.' . $quotedColumnName, $this->platform);
378
379
        // sub tables
380 69
        foreach ($this->class->getSubClasses() as $subClassName) {
381 50
            $subClass = $this->em->getClassMetadata($subClassName);
382
383
            // Add columns
384 50
            foreach ($subClass->getDeclaredPropertiesIterator() as $fieldName => $property) {
385 50
                if ($subClass->isInheritedProperty($fieldName)) {
386 50
                    continue;
387
                }
388
389
                switch (true) {
390 46
                    case $property instanceof FieldMetadata:
391 46
                        $columnList[] = $this->getSelectColumnSQL($fieldName, $subClass);
392 46
                        break;
393
394 32
                    case $property instanceof ToOneAssociationMetadata && $property->isOwningSide():
395 32
                        $targetClass = $this->em->getClassMetadata($property->getTargetEntity());
396
397 32
                        foreach ($property->getJoinColumns() as $joinColumn) {
398
                            /** @var JoinColumnMetadata $joinColumn */
399 32
                            $referencedColumnName = $joinColumn->getReferencedColumnName();
400
401 32
                            if (! $joinColumn->getType()) {
402
                                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
403
                            }
404
405 32
                            $columnList[] = $this->getSelectJoinColumnSQL($joinColumn);
406
                        }
407
408 50
                        break;
409
                }
410
            }
411
        }
412
413 69
        $this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList);
414
415 69
        return $this->currentPersisterContext->selectColumnListSql;
416
    }
417
418
    /**
419
     * {@inheritdoc}
420
     */
421 296
    protected function getInsertColumnList()
422
    {
423
        // Identifier columns must always come first in the column list of subclasses.
424 296
        $columns       = [];
425 296
        $parentColumns = $this->class->getParent()
426 290
            ? $this->class->getIdentifierColumns($this->em)
427 296
            : [];
428
429 296
        foreach ($parentColumns as $columnName => $column) {
430 290
            $columns[] = $columnName;
431
432 290
            $this->columns[$columnName] = $column;
433
        }
434
435 296
        foreach ($this->class->getDeclaredPropertiesIterator() as $name => $property) {
436 296
            if (($property instanceof FieldMetadata && ($property instanceof VersionFieldMetadata || $this->class->isInheritedProperty($name)))
437 296
                || ($property instanceof AssociationMetadata && $this->class->isInheritedProperty($name))
438
                /*|| isset($this->class->embeddedClasses[$name])*/) {
439 294
                continue;
440
            }
441
442 296
            if ($property instanceof AssociationMetadata) {
443 265
                if ($property->isOwningSide() && $property instanceof ToOneAssociationMetadata) {
444 264
                    $targetClass = $this->em->getClassMetadata($property->getTargetEntity());
445
446 264
                    foreach ($property->getJoinColumns() as $joinColumn) {
447
                        /** @var JoinColumnMetadata $joinColumn */
448 264
                        $columnName           = $joinColumn->getColumnName();
449 264
                        $referencedColumnName = $joinColumn->getReferencedColumnName();
450
451 264
                        if (! $joinColumn->getType()) {
452 12
                            $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
453
                        }
454
455 264
                        $columns[] = $columnName;
456
457 264
                        $this->columns[$columnName] = $joinColumn;
458
                    }
459
                }
460
461 265
                continue;
462
            }
463
464 296
            if ($this->class->getClassName() !== $this->class->getRootClassName()
465 296
                || ! $this->class->getProperty($name)->hasValueGenerator()
466 292
                || $this->class->getProperty($name)->getValueGenerator()->getType() !== GeneratorType::IDENTITY
467 296
                || $this->class->identifier[0] !== $name
468
            ) {
469 234
                $columnName = $property->getColumnName();
470
471 234
                $columns[] = $columnName;
472
473 296
                $this->columns[$columnName] = $property;
474
            }
475
        }
476
477
        // Add discriminator column if it is the topmost class.
478 296
        if ($this->class->isRootEntity()) {
479 296
            $discrColumn     = $this->class->discriminatorColumn;
480 296
            $discrColumnName = $discrColumn->getColumnName();
481
482 296
            $columns[] = $discrColumnName;
483
484 296
            $this->columns[$discrColumnName] = $discrColumn;
485
        }
486
487 296
        return $columns;
488
    }
489
490
    /**
491
     * @param string $baseTableAlias
492
     *
493
     * @return string
494
     */
495 74
    private function getJoinSql($baseTableAlias)
496
    {
497 74
        $joinSql           = '';
498 74
        $identifierColumns = $this->class->getIdentifierColumns($this->em);
499
500
        // INNER JOIN parent tables
501 74
        $parentClass = $this->class;
502
503 74
        while (($parentClass = $parentClass->getParent()) !== null) {
504 50
            $conditions = [];
505 50
            $tableName  = $parentClass->table->getQuotedQualifiedName($this->platform);
506 50
            $tableAlias = $this->getSQLTableAlias($parentClass->getTableName());
507 50
            $joinSql   .= ' INNER JOIN ' . $tableName . ' ' . $tableAlias . ' ON ';
508
509 50
            foreach ($identifierColumns as $idColumn) {
510 50
                $quotedColumnName = $this->platform->quoteIdentifier($idColumn->getColumnName());
511
512 50
                $conditions[] = $baseTableAlias . '.' . $quotedColumnName . ' = ' . $tableAlias . '.' . $quotedColumnName;
513
            }
514
515 50
            $joinSql .= implode(' AND ', $conditions);
516
        }
517
518
        // OUTER JOIN sub tables
519 74
        foreach ($this->class->getSubClasses() as $subClassName) {
520 52
            $conditions = [];
521 52
            $subClass   = $this->em->getClassMetadata($subClassName);
522 52
            $tableName  = $subClass->table->getQuotedQualifiedName($this->platform);
523 52
            $tableAlias = $this->getSQLTableAlias($subClass->getTableName());
524 52
            $joinSql   .= ' LEFT JOIN ' . $tableName . ' ' . $tableAlias . ' ON ';
525
526 52
            foreach ($identifierColumns as $idColumn) {
527 52
                $quotedColumnName = $this->platform->quoteIdentifier($idColumn->getColumnName());
528
529 52
                $conditions[] = $baseTableAlias . '.' . $quotedColumnName . ' = ' . $tableAlias . '.' . $quotedColumnName;
530
            }
531
532 52
            $joinSql .= implode(' AND ', $conditions);
533
        }
534
535 74
        return $joinSql;
536
    }
537
}
538