Failed Conditions
Push — master ( 8be1e3...e3936d )
by Marco
14s
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\Types\Type;
10
use Doctrine\ORM\Mapping\AssociationMetadata;
11
use Doctrine\ORM\Mapping\FieldMetadata;
12
use Doctrine\ORM\Mapping\GeneratorType;
13
use Doctrine\ORM\Mapping\JoinColumnMetadata;
14
use Doctrine\ORM\Mapping\ManyToManyAssociationMetadata;
15
use Doctrine\ORM\Mapping\ToManyAssociationMetadata;
16
use Doctrine\ORM\Mapping\ToOneAssociationMetadata;
17
use Doctrine\ORM\Mapping\VersionFieldMetadata;
18
use Doctrine\ORM\Utility\PersisterHelper;
19
20
/**
21
 * The joined subclass persister maps a single entity instance to several tables in the
22
 * database as it is defined by the <tt>Class Table Inheritance</tt> strategy.
23
 *
24
 * @see http://martinfowler.com/eaaCatalog/classTableInheritance.html
25
 */
26
class JoinedSubclassPersister extends AbstractEntityInheritancePersister
27
{
28
    /**
29
     * {@inheritdoc}
30
     */
31 293
    public function insert($entity)
32
    {
33 293
        $rootClass      = ! $this->class->isRootEntity()
34 287
            ? $this->em->getClassMetadata($this->class->getRootClassName())
35 293
            : $this->class;
36 293
        $generationPlan = $this->class->getValueGenerationPlan();
37
38
        // Prepare statement for the root table
39 293
        $rootPersister = $this->em->getUnitOfWork()->getEntityPersister($rootClass->getClassName());
40 293
        $rootTableName = $rootClass->getTableName();
41 293
        $rootTableStmt = $this->conn->prepare($rootPersister->getInsertSQL());
42
43
        // Prepare statements for sub tables.
44 293
        $subTableStmts = [];
45
46 293
        if ($rootClass !== $this->class) {
47 287
            $subTableStmts[$this->class->getTableName()] = $this->conn->prepare($this->getInsertSQL());
48
        }
49
50 293
        $parentClass = $this->class;
51
52 293
        while (($parentClass = $parentClass->getParent()) !== null) {
53 287
            $parentTableName = $parentClass->getTableName();
54
55 287
            if ($parentClass !== $rootClass) {
56 166
                $parentPersister = $this->em->getUnitOfWork()->getEntityPersister($parentClass->getClassName());
57
58 166
                $subTableStmts[$parentTableName] = $this->conn->prepare($parentPersister->getInsertSQL());
59
            }
60
        }
61
62
        // Execute all inserts. For each entity:
63
        // 1) Insert on root table
64
        // 2) Insert on sub tables
65 293
        $insertData = $this->prepareInsertData($entity);
66
67
        // Execute insert on root table
68 293
        $paramIndex = 1;
69
70 293
        foreach ($insertData[$rootTableName] as $columnName => $value) {
71 293
            $type = $this->columns[$columnName]->getType();
72
73 293
            $rootTableStmt->bindValue($paramIndex++, $value, $type);
74
        }
75
76 293
        $rootTableStmt->execute();
77
78 293
        if ($generationPlan->containsDeferred()) {
79 289
            $generationPlan->executeDeferred($this->em, $entity);
80 289
            $id = $this->getIdentifier($entity);
81
        } else {
82 4
            $id = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
83
        }
84
85 293
        if ($this->class->isVersioned()) {
86 9
            $this->assignDefaultVersionValue($entity, $id);
87
        }
88
89
        // Execute inserts on subtables.
90
        // The order doesn't matter because all child tables link to the root table via FK.
91 293
        foreach ($subTableStmts as $tableName => $stmt) {
92
            /** @var \Doctrine\DBAL\Statement $stmt */
93 287
            $paramIndex = 1;
94 287
            $data       = $insertData[$tableName] ?? [];
95
96 287
            foreach ((array) $id as $idName => $idVal) {
97 287
                $type = Type::getType('string');
98
99 287
                if (isset($this->columns[$idName])) {
100 287
                    $type = $this->columns[$idName]->getType();
101
                }
102
103 287
                $stmt->bindValue($paramIndex++, $idVal, $type);
104
            }
105
106 287
            foreach ($data as $columnName => $value) {
107 222
                if (! is_array($id) || ! isset($id[$columnName])) {
108 222
                    $type = $this->columns[$columnName]->getType();
109
110 222
                    $stmt->bindValue($paramIndex++, $value, $type);
111
                }
112
            }
113
114 287
            $stmt->execute();
115
        }
116
117 293
        $rootTableStmt->closeCursor();
118
119 293
        foreach ($subTableStmts as $stmt) {
120 287
            $stmt->closeCursor();
121
        }
122 293
    }
123
124
    /**
125
     * {@inheritdoc}
126
     */
127 30
    public function update($entity)
128
    {
129 30
        $updateData = $this->prepareUpdateData($entity);
130
131 30
        if (! $updateData) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $updateData of type array<mixed,mixed> is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
132
            return;
133
        }
134
135 30
        $isVersioned = $this->class->isVersioned();
136
137 30
        foreach ($updateData as $tableName => $data) {
138 30
            $versioned = $isVersioned && $this->class->versionProperty->getTableName() === $tableName;
139
140 30
            $this->updateTable($entity, $this->platform->quoteIdentifier($tableName), $data, $versioned);
141
        }
142
143
        // Make sure the table with the version column is updated even if no columns on that
144
        // table were affected.
145 29
        if ($isVersioned) {
146 5
            $versionedClass = $this->class->versionProperty->getDeclaringClass();
147 5
            $versionedTable = $versionedClass->getTableName();
148
149 5
            if (! isset($updateData[$versionedTable])) {
150 2
                $tableName = $versionedClass->table->getQuotedQualifiedName($this->platform);
151
152 2
                $this->updateTable($entity, $tableName, [], true);
153
            }
154
155 4
            $identifiers = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
156
157 4
            $this->assignDefaultVersionValue($entity, $identifiers);
158
        }
159 28
    }
160
161
    /**
162
     * {@inheritdoc}
163
     */
164 5
    public function delete($entity)
165
    {
166 5
        $identifier = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
167 5
        $id         = array_combine(array_keys($this->class->getIdentifierColumns($this->em)), $identifier);
168
169 5
        $this->deleteJoinTableRecords($identifier);
170
171
        // If the database platform supports FKs, just
172
        // delete the row from the root table. Cascades do the rest.
173 5
        if ($this->platform->supportsForeignKeyConstraints()) {
174
            $rootClass = $this->em->getClassMetadata($this->class->getRootClassName());
175
            $rootTable = $rootClass->table->getQuotedQualifiedName($this->platform);
176
177
            return (bool) $this->conn->delete($rootTable, $id);
178
        }
179
180
        // Delete from all tables individually, starting from this class' table up to the root table.
181 5
        $rootTable    = $this->class->table->getQuotedQualifiedName($this->platform);
182 5
        $affectedRows = $this->conn->delete($rootTable, $id);
183 5
        $parentClass  = $this->class;
184
185 5
        while (($parentClass = $parentClass->getParent()) !== null) {
186 4
            $parentTable = $parentClass->table->getQuotedQualifiedName($this->platform);
187
188 4
            $this->conn->delete($parentTable, $id);
189
        }
190
191 5
        return (bool) $affectedRows;
192
    }
193
194
    /**
195
     * {@inheritdoc}
196
     */
197 68
    public function getSelectSQL(
198
        $criteria,
199
        ?AssociationMetadata $association = null,
200
        $lockMode = null,
201
        $limit = null,
202
        $offset = null,
203
        array $orderBy = []
204
    ) {
205 68
        $this->switchPersisterContext($offset, $limit);
206
207 68
        $baseTableAlias = $this->getSQLTableAlias($this->class->getTableName());
208 68
        $joinSql        = $this->getJoinSql($baseTableAlias);
209
210 68
        if ($association instanceof ManyToManyAssociationMetadata) {
211 2
            $joinSql .= $this->getSelectManyToManyJoinSQL($association);
212
        }
213
214 68
        if ($association instanceof ToManyAssociationMetadata && $association->getOrderBy()) {
215 1
            $orderBy = $association->getOrderBy();
216
        }
217
218 68
        $orderBySql   = $this->getOrderBySQL($orderBy, $baseTableAlias);
219 68
        $conditionSql = ($criteria instanceof Criteria)
220
            ? $this->getSelectConditionCriteriaSQL($criteria)
221 68
            : $this->getSelectConditionSQL($criteria, $association);
222
223
        // If the current class in the root entity, add the filters
224 68
        $rootClass  = $this->em->getClassMetadata($this->class->getRootClassName());
225 68
        $tableAlias = $this->getSQLTableAlias($rootClass->getTableName());
226 68
        $filterSql  = $this->generateFilterConditionSQL($rootClass, $tableAlias);
227
228 68
        if ($filterSql) {
229 4
            $conditionSql .= $conditionSql
230 2
                ? ' AND ' . $filterSql
231 4
                : $filterSql;
232
        }
233
234 68
        $lockSql = '';
235
236
        switch ($lockMode) {
237 68
            case LockMode::PESSIMISTIC_READ:
238
                $lockSql = ' ' . $this->platform->getReadLockSQL();
239
                break;
240
241 68
            case LockMode::PESSIMISTIC_WRITE:
242
                $lockSql = ' ' . $this->platform->getWriteLockSQL();
243
                break;
244
        }
245
246 68
        $tableName  = $this->class->table->getQuotedQualifiedName($this->platform);
247 68
        $from       = ' FROM ' . $tableName . ' ' . $baseTableAlias;
248 68
        $where      = $conditionSql !== '' ? ' WHERE ' . $conditionSql : '';
249 68
        $lock       = $this->platform->appendLockHint($from, $lockMode);
250 68
        $columnList = $this->getSelectColumnsSQL();
251 68
        $query      = 'SELECT ' . $columnList
252 68
                    . $lock
253 68
                    . $joinSql
254 68
                    . $where
255 68
                    . $orderBySql;
256
257 68
        return $this->platform->modifyLimitQuery($query, $limit, $offset) . $lockSql;
258
    }
259
260
    /**
261
     * {@inheritDoc}
262
     */
263 6
    public function getCountSQL($criteria = [])
264
    {
265 6
        $tableName      = $this->class->table->getQuotedQualifiedName($this->platform);
266 6
        $baseTableAlias = $this->getSQLTableAlias($this->class->getTableName());
267 6
        $joinSql        = $this->getJoinSql($baseTableAlias);
268
269 6
        $conditionSql = ($criteria instanceof Criteria)
270 1
            ? $this->getSelectConditionCriteriaSQL($criteria)
271 6
            : $this->getSelectConditionSQL($criteria);
272
273 6
        $rootClass  = $this->em->getClassMetadata($this->class->getRootClassName());
274 6
        $tableAlias = $this->getSQLTableAlias($rootClass->getTableName());
275 6
        $filterSql  = $this->generateFilterConditionSQL($rootClass, $tableAlias);
276
277 6
        if ($filterSql !== '') {
278 1
            $conditionSql = $conditionSql
279 1
                ? $conditionSql . ' AND ' . $filterSql
280 1
                : $filterSql;
281
        }
282
283
        $sql = 'SELECT COUNT(*) '
284 6
            . 'FROM ' . $tableName . ' ' . $baseTableAlias
285 6
            . $joinSql
286 6
            . (empty($conditionSql) ? '' : ' WHERE ' . $conditionSql);
287
288 6
        return $sql;
289
    }
290
291
    /**
292
     * {@inheritdoc}
293
     */
294 6
    protected function getLockTablesSql($lockMode)
295
    {
296 6
        $joinSql           = '';
297 6
        $identifierColumns = $this->class->getIdentifierColumns($this->em);
298 6
        $baseTableAlias    = $this->getSQLTableAlias($this->class->getTableName());
299
300
        // INNER JOIN parent tables
301 6
        $parentClass = $this->class;
302
303 6
        while (($parentClass = $parentClass->getParent()) !== null) {
304 5
            $conditions = [];
305 5
            $tableName  = $parentClass->table->getQuotedQualifiedName($this->platform);
306 5
            $tableAlias = $this->getSQLTableAlias($parentClass->getTableName());
307 5
            $joinSql   .= ' INNER JOIN ' . $tableName . ' ' . $tableAlias . ' ON ';
308
309 5
            foreach ($identifierColumns as $idColumn) {
310 5
                $quotedColumnName = $this->platform->quoteIdentifier($idColumn->getColumnName());
311
312 5
                $conditions[] = $baseTableAlias . '.' . $quotedColumnName . ' = ' . $tableAlias . '.' . $quotedColumnName;
313
            }
314
315 5
            $joinSql .= implode(' AND ', $conditions);
316
        }
317
318 6
        return parent::getLockTablesSql($lockMode) . $joinSql;
319
    }
320
321
    /**
322
     * Ensure this method is never called. This persister overrides getSelectEntitiesSQL directly.
323
     *
324
     * @return string
325
     */
326 68
    protected function getSelectColumnsSQL()
327
    {
328
        // Create the column list fragment only once
329 68
        if ($this->currentPersisterContext->selectColumnListSql !== null) {
330 12
            return $this->currentPersisterContext->selectColumnListSql;
331
        }
332
333 68
        $this->currentPersisterContext->rsm->addEntityResult($this->class->getClassName(), 'r');
334
335 68
        $columnList = [];
336
337
        // Add columns
338 68
        foreach ($this->class->getDeclaredPropertiesIterator() as $fieldName => $property) {
339 68
            if ($property instanceof FieldMetadata) {
340 68
                $columnList[] = $this->getSelectColumnSQL($fieldName, $property->getDeclaringClass());
341
342 68
                continue;
343
            }
344
345 56
            if (! ($property instanceof ToOneAssociationMetadata) || ! $property->isOwningSide()) {
346 44
                continue;
347
            }
348
349 55
            $targetClass = $this->em->getClassMetadata($property->getTargetEntity());
350
351 55
            foreach ($property->getJoinColumns() as $joinColumn) {
352
                /** @var JoinColumnMetadata $joinColumn */
353 55
                $referencedColumnName = $joinColumn->getReferencedColumnName();
354
355 55
                if (! $joinColumn->getType()) {
356
                    $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
357
                }
358
359 55
                $columnList[] = $this->getSelectJoinColumnSQL($joinColumn);
360
            }
361
        }
362
363
        // Add discriminator column (DO NOT ALIAS, see AbstractEntityInheritancePersister#processSQLResult).
364 68
        $discrColumn      = $this->class->discriminatorColumn;
365 68
        $discrTableAlias  = $this->getSQLTableAlias($discrColumn->getTableName());
366 68
        $discrColumnName  = $discrColumn->getColumnName();
367 68
        $discrColumnType  = $discrColumn->getType();
368 68
        $resultColumnName = $this->platform->getSQLResultCasing($discrColumnName);
369 68
        $quotedColumnName = $this->platform->quoteIdentifier($discrColumn->getColumnName());
370
371 68
        $this->currentPersisterContext->rsm->setDiscriminatorColumn('r', $resultColumnName);
372 68
        $this->currentPersisterContext->rsm->addMetaResult('r', $resultColumnName, $discrColumnName, false, $discrColumnType);
373
374 68
        $columnList[] = $discrColumnType->convertToDatabaseValueSQL($discrTableAlias . '.' . $quotedColumnName, $this->platform);
375
376
        // sub tables
377 68
        foreach ($this->class->getSubClasses() as $subClassName) {
378 49
            $subClass = $this->em->getClassMetadata($subClassName);
379
380
            // Add columns
381 49
            foreach ($subClass->getDeclaredPropertiesIterator() as $fieldName => $property) {
382 49
                if ($subClass->isInheritedProperty($fieldName)) {
383 49
                    continue;
384
                }
385
386
                switch (true) {
387 45
                    case ($property instanceof FieldMetadata):
388 45
                        $columnList[] = $this->getSelectColumnSQL($fieldName, $subClass);
389 45
                        break;
390
391 32
                    case ($property instanceof ToOneAssociationMetadata && $property->isOwningSide()):
392 32
                        $targetClass = $this->em->getClassMetadata($property->getTargetEntity());
393
394 32
                        foreach ($property->getJoinColumns() as $joinColumn) {
395
                            /** @var JoinColumnMetadata $joinColumn */
396 32
                            $referencedColumnName = $joinColumn->getReferencedColumnName();
397
398 32
                            if (! $joinColumn->getType()) {
399
                                $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
400
                            }
401
402 32
                            $columnList[] = $this->getSelectJoinColumnSQL($joinColumn);
403
                        }
404
405 49
                        break;
406
                }
407
            }
408
        }
409
410 68
        $this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList);
411
412 68
        return $this->currentPersisterContext->selectColumnListSql;
413
    }
414
415
    /**
416
     * {@inheritdoc}
417
     */
418 293
    protected function getInsertColumnList()
419
    {
420
        // Identifier columns must always come first in the column list of subclasses.
421 293
        $columns       = [];
422 293
        $parentColumns = $this->class->getParent()
423 287
            ? $this->class->getIdentifierColumns($this->em)
424 293
            : [];
425
426 293
        foreach ($parentColumns as $columnName => $column) {
427 287
            $columns[] = $columnName;
428
429 287
            $this->columns[$columnName] = $column;
430
        }
431
432 293
        foreach ($this->class->getDeclaredPropertiesIterator() as $name => $property) {
433 293
            if (($property instanceof FieldMetadata && ($property instanceof VersionFieldMetadata || $this->class->isInheritedProperty($name)))
434 293
                || ($property instanceof AssociationMetadata && $this->class->isInheritedProperty($name))
435
                /*|| isset($this->class->embeddedClasses[$name])*/) {
436 291
                continue;
437
            }
438
439 293
            if ($property instanceof AssociationMetadata) {
440 264
                if ($property->isOwningSide() && $property instanceof ToOneAssociationMetadata) {
441 263
                    $targetClass = $this->em->getClassMetadata($property->getTargetEntity());
442
443 263
                    foreach ($property->getJoinColumns() as $joinColumn) {
444
                        /** @var JoinColumnMetadata $joinColumn */
445 263
                        $columnName           = $joinColumn->getColumnName();
446 263
                        $referencedColumnName = $joinColumn->getReferencedColumnName();
447
448 263
                        if (! $joinColumn->getType()) {
449 12
                            $joinColumn->setType(PersisterHelper::getTypeOfColumn($referencedColumnName, $targetClass, $this->em));
450
                        }
451
452 263
                        $columns[] = $columnName;
453
454 263
                        $this->columns[$columnName] = $joinColumn;
455
                    }
456
                }
457
458 264
                continue;
459
            }
460
461 293
            if ($this->class->getClassName() !== $this->class->getRootClassName()
462 293
                || ! $this->class->getProperty($name)->hasValueGenerator()
463 289
                || $this->class->getProperty($name)->getValueGenerator()->getType() !== GeneratorType::IDENTITY
464 293
                || $this->class->identifier[0] !== $name
465
            ) {
466 231
                $columnName = $property->getColumnName();
467
468 231
                $columns[] = $columnName;
469
470 293
                $this->columns[$columnName] = $property;
471
            }
472
        }
473
474
        // Add discriminator column if it is the topmost class.
475 293
        if ($this->class->isRootEntity()) {
476 293
            $discrColumn     = $this->class->discriminatorColumn;
477 293
            $discrColumnName = $discrColumn->getColumnName();
478
479 293
            $columns[] = $discrColumnName;
480
481 293
            $this->columns[$discrColumnName] = $discrColumn;
482
        }
483
484 293
        return $columns;
485
    }
486
487
    /**
488
     * @param string $baseTableAlias
489
     *
490
     * @return string
491
     */
492 73
    private function getJoinSql($baseTableAlias)
493
    {
494 73
        $joinSql           = '';
495 73
        $identifierColumns = $this->class->getIdentifierColumns($this->em);
496
497
        // INNER JOIN parent tables
498 73
        $parentClass = $this->class;
499
500 73
        while (($parentClass = $parentClass->getParent()) !== null) {
501 50
            $conditions = [];
502 50
            $tableName  = $parentClass->table->getQuotedQualifiedName($this->platform);
503 50
            $tableAlias = $this->getSQLTableAlias($parentClass->getTableName());
504 50
            $joinSql   .= ' INNER JOIN ' . $tableName . ' ' . $tableAlias . ' ON ';
505
506 50
            foreach ($identifierColumns as $idColumn) {
507 50
                $quotedColumnName = $this->platform->quoteIdentifier($idColumn->getColumnName());
508
509 50
                $conditions[] = $baseTableAlias . '.' . $quotedColumnName . ' = ' . $tableAlias . '.' . $quotedColumnName;
510
            }
511
512 50
            $joinSql .= implode(' AND ', $conditions);
513
        }
514
515
        // OUTER JOIN sub tables
516 73
        foreach ($this->class->getSubClasses() as $subClassName) {
517 51
            $conditions = [];
518 51
            $subClass   = $this->em->getClassMetadata($subClassName);
519 51
            $tableName  = $subClass->table->getQuotedQualifiedName($this->platform);
520 51
            $tableAlias = $this->getSQLTableAlias($subClass->getTableName());
521 51
            $joinSql   .= ' LEFT JOIN ' . $tableName . ' ' . $tableAlias . ' ON ';
522
523 51
            foreach ($identifierColumns as $idColumn) {
524 51
                $quotedColumnName = $this->platform->quoteIdentifier($idColumn->getColumnName());
525
526 51
                $conditions[] = $baseTableAlias . '.' . $quotedColumnName . ' = ' . $tableAlias . '.' . $quotedColumnName;
527
            }
528
529 51
            $joinSql .= implode(' AND ', $conditions);
530
        }
531
532 73
        return $joinSql;
533
    }
534
}
535