Passed
Pull Request — master (#7725)
by Guilherme
09:09
created

SchemaTool::getDropSchemaSQL()   C

Complexity

Conditions 12
Paths 12

Size

Total Lines 44
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 30

Importance

Changes 0
Metric Value
cc 12
eloc 24
nc 12
nop 1
dl 0
loc 44
ccs 12
cts 24
cp 0.5
crap 30
rs 6.9666
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM\Tools;
6
7
use Doctrine\DBAL\Platforms\AbstractPlatform;
8
use Doctrine\DBAL\Schema\Column;
9
use Doctrine\DBAL\Schema\Comparator;
10
use Doctrine\DBAL\Schema\Index;
11
use Doctrine\DBAL\Schema\Schema;
12
use Doctrine\DBAL\Schema\Table;
13
use Doctrine\DBAL\Schema\Visitor\DropSchemaSqlCollector;
14
use Doctrine\DBAL\Schema\Visitor\RemoveNamespacedAssets;
15
use Doctrine\ORM\EntityManagerInterface;
16
use Doctrine\ORM\Mapping\AssociationMetadata;
17
use Doctrine\ORM\Mapping\ClassMetadata;
18
use Doctrine\ORM\Mapping\FieldMetadata;
19
use Doctrine\ORM\Mapping\GeneratorType;
20
use Doctrine\ORM\Mapping\InheritanceType;
21
use Doctrine\ORM\Mapping\JoinColumnMetadata;
22
use Doctrine\ORM\Mapping\ManyToManyAssociationMetadata;
23
use Doctrine\ORM\Mapping\MappingException;
24
use Doctrine\ORM\Mapping\OneToManyAssociationMetadata;
25
use Doctrine\ORM\Mapping\ToOneAssociationMetadata;
26
use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs;
27
use Doctrine\ORM\Tools\Event\GenerateSchemaTableEventArgs;
28
use Doctrine\ORM\Tools\Exception\MissingColumnException;
29
use Doctrine\ORM\Tools\Exception\NotSupported;
30
use Throwable;
31
use function array_diff;
32
use function array_key_exists;
0 ignored issues
show
introduced by
Type array_key_exists is not used in this file.
Loading history...
33
use function array_keys;
34
use function count;
35
use function implode;
36
use function in_array;
37
use function is_int;
38
use function is_numeric;
39
use function reset;
40
use function strtolower;
41
42
/**
43
 * The SchemaTool is a tool to create/drop/update database schemas based on
44
 * <tt>ClassMetadata</tt> class descriptors.
45
 */
46
class SchemaTool
47
{
48
    private const KNOWN_COLUMN_OPTIONS = ['comment', 'unsigned', 'fixed', 'default'];
49
50
    /** @var EntityManagerInterface */
51
    private $em;
52
53
    /** @var AbstractPlatform */
54
    private $platform;
55
56
    /**
57
     * Initializes a new SchemaTool instance that uses the connection of the
58
     * provided EntityManager.
59
     */
60 1231
    public function __construct(EntityManagerInterface $em)
61
    {
62 1231
        $this->em       = $em;
63 1231
        $this->platform = $em->getConnection()->getDatabasePlatform();
64 1231
    }
65
66
    /**
67
     * Creates the database schema for the given array of ClassMetadata instances.
68
     *
69
     * @param ClassMetadata[] $classes
70
     *
71
     * @throws ToolsException
72
     */
73 257
    public function createSchema(array $classes)
74
    {
75 257
        $createSchemaSql = $this->getCreateSchemaSql($classes);
76 257
        $conn            = $this->em->getConnection();
77
78 257
        foreach ($createSchemaSql as $sql) {
79
            try {
80 257
                $conn->executeQuery($sql);
81 69
            } catch (Throwable $e) {
82 69
                throw ToolsException::schemaToolFailure($sql, $e);
83
            }
84
        }
85 188
    }
86
87
    /**
88
     * Gets the list of DDL statements that are required to create the database schema for
89
     * the given list of ClassMetadata instances.
90
     *
91
     * @param ClassMetadata[] $classes
92
     *
93
     * @return string[] The SQL statements needed to create the schema for the classes.
94
     */
95 257
    public function getCreateSchemaSql(array $classes)
96
    {
97 257
        $schema = $this->getSchemaFromMetadata($classes);
98
99 257
        return $schema->toSql($this->platform);
100
    }
101
102
    /**
103
     * Detects instances of ClassMetadata that don't need to be processed in the SchemaTool context.
104
     *
105
     * @param ClassMetadata   $class
106
     * @param ClassMetadata[] $processedClasses
107
     *
108
     * @return bool
109
     */
110 267
    private function processingNotRequired($class, array $processedClasses)
111
    {
112 267
        return isset($processedClasses[$class->getClassName()]) ||
113 267
            $class->isMappedSuperclass ||
114 267
            $class->isEmbeddedClass ||
115 267
            ($class->inheritanceType === InheritanceType::SINGLE_TABLE && ! $class->isRootEntity());
116
    }
117
118
    /**
119
     * Creates a Schema instance from a given set of metadata classes.
120
     *
121
     * @param ClassMetadata[] $classes
122
     *
123
     * @return Schema
124
     *
125
     * @throws ORMException
126
     */
127 267
    public function getSchemaFromMetadata(array $classes)
128
    {
129
        // Reminder for processed classes, used for hierarchies
130 267
        $processedClasses     = [];
131 267
        $eventManager         = $this->em->getEventManager();
132 267
        $schemaManager        = $this->em->getConnection()->getSchemaManager();
133 267
        $metadataSchemaConfig = $schemaManager->createSchemaConfig();
134
135 267
        $metadataSchemaConfig->setExplicitForeignKeyIndexes(false);
136 267
        $schema = new Schema([], [], $metadataSchemaConfig);
137
138 267
        $addedFks       = [];
139 267
        $blacklistedFks = [];
140
141 267
        foreach ($classes as $class) {
142
            /** @var ClassMetadata $class */
143 267
            if ($this->processingNotRequired($class, $processedClasses)) {
144 19
                continue;
145
            }
146
147 267
            $table = $schema->createTable($class->table->getQuotedQualifiedName($this->platform));
148
149 267
            switch ($class->inheritanceType) {
150
                case InheritanceType::SINGLE_TABLE:
151 21
                    $this->gatherColumns($class, $table);
152 21
                    $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
153
154
                    // Add the discriminator column
155 21
                    $this->addDiscriminatorColumnDefinition($class, $table);
156
157
                    // Aggregate all the information from all classes in the hierarchy
158 21
                    $parentClass = $class;
159
160 21
                    while (($parentClass = $parentClass->getParent()) !== null) {
161
                        // Parent class information is already contained in this class
162
                        $processedClasses[$parentClass->getClassName()] = true;
163
                    }
164
165 21
                    foreach ($class->getSubClasses() as $subClassName) {
166 19
                        $subClass = $this->em->getClassMetadata($subClassName);
167
168 19
                        $this->gatherColumns($subClass, $table);
0 ignored issues
show
Bug introduced by
$subClass of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Tools\SchemaTool::gatherColumns(). ( Ignorable by Annotation )

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

168
                        $this->gatherColumns(/** @scrutinizer ignore-type */ $subClass, $table);
Loading history...
169 19
                        $this->gatherRelationsSql($subClass, $table, $schema, $addedFks, $blacklistedFks);
0 ignored issues
show
Bug introduced by
$subClass of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Tools\Schem...l::gatherRelationsSql(). ( Ignorable by Annotation )

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

169
                        $this->gatherRelationsSql(/** @scrutinizer ignore-type */ $subClass, $table, $schema, $addedFks, $blacklistedFks);
Loading history...
170
171 19
                        $processedClasses[$subClassName] = true;
172
                    }
173
174 21
                    break;
175
176
                case InheritanceType::JOINED:
177
                    // Add all non-inherited fields as columns
178 60
                    $pkColumns = [];
179
180 60
                    foreach ($class->getDeclaredPropertiesIterator() as $fieldName => $property) {
181 60
                        if (! ($property instanceof FieldMetadata)) {
182 16
                            continue;
183
                        }
184
185 60
                        if (! $class->isInheritedProperty($fieldName)) {
186 60
                            $columnName = $this->platform->quoteIdentifier($property->getColumnName());
187
188 60
                            $this->gatherColumn($class, $property, $table);
189
190 60
                            if ($class->isIdentifier($fieldName)) {
191 60
                                $pkColumns[] = $columnName;
192
                            }
193
                        }
194
                    }
195
196 60
                    $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
197
198
                    // Add the discriminator column only to the root table
199 60
                    if ($class->isRootEntity()) {
200 60
                        $this->addDiscriminatorColumnDefinition($class, $table);
201
                    } else {
202
                        // Add an ID FK column to child tables
203 59
                        $inheritedKeyColumns = [];
204
205 59
                        foreach ($class->identifier as $identifierField) {
206 59
                            $idProperty = $class->getProperty($identifierField);
207
208 59
                            if ($class->isInheritedProperty($identifierField)) {
209 59
                                $column     = $this->gatherColumn($class, $idProperty, $table);
0 ignored issues
show
Bug introduced by
It seems like $idProperty can also be of type null; however, parameter $fieldMetadata of Doctrine\ORM\Tools\SchemaTool::gatherColumn() does only seem to accept Doctrine\ORM\Mapping\FieldMetadata, maybe add an additional type check? ( Ignorable by Annotation )

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

209
                                $column     = $this->gatherColumn($class, /** @scrutinizer ignore-type */ $idProperty, $table);
Loading history...
210 59
                                $columnName = $column->getQuotedName($this->platform);
211
212
                                // TODO: This seems rather hackish, can we optimize it?
213 59
                                $column->setAutoincrement(false);
214
215 59
                                $pkColumns[]           = $columnName;
216 59
                                $inheritedKeyColumns[] = $columnName;
217
                            }
218
                        }
219
220 59
                        if (! empty($inheritedKeyColumns)) {
221
                            // Add a FK constraint on the ID column
222 59
                            $rootClass = $this->em->getClassMetadata($class->getRootClassName());
223
224 59
                            $table->addForeignKeyConstraint(
225 59
                                $rootClass->table->getQuotedQualifiedName($this->platform),
0 ignored issues
show
Bug introduced by
Accessing table on the interface Doctrine\Common\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
226 59
                                $inheritedKeyColumns,
227 59
                                $inheritedKeyColumns,
228 59
                                ['onDelete' => 'CASCADE']
229
                            );
230
                        }
231
                    }
232
233 60
                    $table->setPrimaryKey($pkColumns);
234
235 60
                    break;
236
237
                case InheritanceType::TABLE_PER_CLASS:
238
                    throw NotSupported::create();
239
240
                default:
241 241
                    $this->gatherColumns($class, $table);
242 241
                    $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
243
244 241
                    break;
245
            }
246
247 267
            $pkColumns = [];
248
249 267
            foreach ($class->identifier as $identifierField) {
250 267
                $property = $class->getProperty($identifierField);
251
252 267
                if ($property instanceof FieldMetadata) {
253 266
                    $pkColumns[] = $this->platform->quoteIdentifier($property->getColumnName());
254
255 266
                    continue;
256
                }
257
258 32
                if ($property instanceof ToOneAssociationMetadata) {
259 32
                    foreach ($property->getJoinColumns() as $joinColumn) {
260 32
                        $pkColumns[] = $this->platform->quoteIdentifier($joinColumn->getColumnName());
261
                    }
262
                }
263
            }
264
265 267
            if (! $table->hasIndex('primary')) {
266 248
                $table->setPrimaryKey($pkColumns);
267
            }
268
269
            // there can be unique indexes automatically created for join column
270
            // if join column is also primary key we should keep only primary key on this column
271
            // so, remove indexes overruled by primary key
272 267
            $primaryKey = $table->getIndex('primary');
273
274 267
            foreach ($table->getIndexes() as $idxKey => $existingIndex) {
275 267
                if ($primaryKey->overrules($existingIndex)) {
276 2
                    $table->dropIndex($idxKey);
277
                }
278
            }
279
280 267
            if ($class->table->getIndexes()) {
281 1
                foreach ($class->table->getIndexes() as $indexName => $indexData) {
282 1
                    $indexName = is_numeric($indexName) ? null : $indexName;
283 1
                    $index     = new Index($indexName, $indexData['columns'], $indexData['unique'], $indexData['flags'], $indexData['options']);
284
285 1
                    foreach ($table->getIndexes() as $tableIndexName => $tableIndex) {
286 1
                        if ($tableIndex->isFullfilledBy($index)) {
287
                            $table->dropIndex($tableIndexName);
288
                            break;
289
                        }
290
                    }
291
292 1
                    if ($indexData['unique']) {
293
                        $table->addUniqueIndex($indexData['columns'], $indexName, $indexData['options']);
294
                    } else {
295 1
                        $table->addIndex($indexData['columns'], $indexName, $indexData['flags'], $indexData['options']);
296
                    }
297
                }
298
            }
299
300 267
            if ($class->table->getUniqueConstraints()) {
301 4
                foreach ($class->table->getUniqueConstraints() as $indexName => $indexData) {
302 4
                    $indexName = is_numeric($indexName) ? null : $indexName;
303 4
                    $uniqIndex = new Index($indexName, $indexData['columns'], true, false, $indexData['flags'], $indexData['options']);
304
305 4
                    foreach ($table->getIndexes() as $tableIndexName => $tableIndex) {
306 4
                        if ($tableIndex->isFullfilledBy($uniqIndex)) {
307 3
                            $table->dropIndex($tableIndexName);
308 3
                            break;
309
                        }
310
                    }
311
312 4
                    $table->addUniqueConstraint($indexData['columns'], $indexName, $indexData['flags'], $indexData['options']);
313
                }
314
            }
315
316 267
            if ($class->table->getOptions()) {
317 1
                foreach ($class->table->getOptions() as $key => $val) {
318 1
                    $table->addOption($key, $val);
319
                }
320
            }
321
322 267
            $processedClasses[$class->getClassName()] = true;
323
324 267
            foreach ($class->getDeclaredPropertiesIterator() as $property) {
325 267
                if (! $property instanceof FieldMetadata
326 267
                    || ! $property->hasValueGenerator()
327 234
                    || $property->getValueGenerator()->getType() !== GeneratorType::SEQUENCE
328 267
                    || $class->getClassName() !== $class->getRootClassName()) {
329 267
                    continue;
330
                }
331
332
                $quotedName = $this->platform->quoteIdentifier($property->getValueGenerator()->getDefinition()['sequenceName']);
333
334
                if (! $schema->hasSequence($quotedName)) {
335
                    $schema->createSequence($quotedName, $property->getValueGenerator()->getDefinition()['allocationSize']);
336
                }
337
            }
338
339 267
            if ($eventManager->hasListeners(ToolEvents::postGenerateSchemaTable)) {
340 1
                $eventManager->dispatchEvent(
341 1
                    ToolEvents::postGenerateSchemaTable,
342 1
                    new GenerateSchemaTableEventArgs($class, $schema, $table)
343
                );
344
            }
345
        }
346
347 267
        if (! $this->platform->supportsSchemas() && ! $this->platform->canEmulateSchemas()) {
348 9
            $schema->visit(new RemoveNamespacedAssets());
349
        }
350
351 267
        if ($eventManager->hasListeners(ToolEvents::postGenerateSchema)) {
352 1
            $eventManager->dispatchEvent(
353 1
                ToolEvents::postGenerateSchema,
354 1
                new GenerateSchemaEventArgs($this->em, $schema)
355
            );
356
        }
357
358 267
        return $schema;
359
    }
360
361
    /**
362
     * Gets a portable column definition as required by the DBAL for the discriminator
363
     * column of a class.
364
     *
365
     * @param ClassMetadata $class
366
     */
367 76
    private function addDiscriminatorColumnDefinition($class, Table $table)
368
    {
369 76
        $discrColumn     = $class->discriminatorColumn;
370 76
        $discrColumnType = $discrColumn->getTypeName();
371
        $options         = [
372 76
            'notnull' => ! $discrColumn->isNullable(),
373
        ];
374
375 76
        switch ($discrColumnType) {
376 76
            case 'string':
377 69
                $options['length'] = $discrColumn->getLength() ?? 255;
378 69
                break;
379
380 7
            case 'decimal':
381
                $options['scale']     = $discrColumn->getScale();
382
                $options['precision'] = $discrColumn->getPrecision();
383
                break;
384
        }
385
386 76
        if (! empty($discrColumn->getColumnDefinition())) {
387
            $options['columnDefinition'] = $discrColumn->getColumnDefinition();
388
        }
389
390 76
        $table->addColumn($discrColumn->getColumnName(), $discrColumnType, $options);
391 76
    }
392
393
    /**
394
     * Gathers the column definitions as required by the DBAL of all field mappings
395
     * found in the given class.
396
     *
397
     * @param ClassMetadata $class
398
     */
399 248
    private function gatherColumns($class, Table $table)
400
    {
401 248
        $pkColumns = [];
402
403 248
        foreach ($class->getDeclaredPropertiesIterator() as $fieldName => $property) {
404 248
            if (! ($property instanceof FieldMetadata)) {
405 183
                continue;
406
            }
407
408 248
            if ($class->inheritanceType === InheritanceType::SINGLE_TABLE && $class->isInheritedProperty($fieldName)) {
409 19
                continue;
410
            }
411
412 248
            $this->gatherColumn($class, $property, $table);
413
414 248
            if ($property->isPrimaryKey()) {
415 247
                $pkColumns[] = $this->platform->quoteIdentifier($property->getColumnName());
416
            }
417
        }
418 248
    }
419
420
    /**
421
     * Creates a column definition as required by the DBAL from an ORM field mapping definition.
422
     *
423
     * @param ClassMetadata $classMetadata The class that owns the field mapping.
424
     * @param FieldMetadata $fieldMetadata The field mapping.
425
     *
426
     * @return Column The portable column definition as required by the DBAL.
427
     */
428 267
    private function gatherColumn($classMetadata, FieldMetadata $fieldMetadata, Table $table)
429
    {
430 267
        $fieldName  = $fieldMetadata->getName();
431 267
        $columnName = $fieldMetadata->getColumnName();
432 267
        $columnType = $fieldMetadata->getTypeName();
433
434
        $options = [
435 267
            'length'          => $fieldMetadata->getLength(),
436 267
            'notnull'         => ! $fieldMetadata->isNullable(),
437
            'platformOptions' => [
438 267
                'version' => ($classMetadata->isVersioned() && $classMetadata->versionProperty->getName() === $fieldName),
439
            ],
440
        ];
441
442 267
        if ($classMetadata->inheritanceType === InheritanceType::SINGLE_TABLE && $classMetadata->getParent()) {
443 7
            $options['notnull'] = false;
444
        }
445
446 267
        if (strtolower($columnType) === 'string' && $options['length'] === null) {
447
            $options['length'] = 255;
448
        }
449
450 267
        if (is_int($fieldMetadata->getPrecision())) {
451 267
            $options['precision'] = $fieldMetadata->getPrecision();
452
        }
453
454 267
        if (is_int($fieldMetadata->getScale())) {
455 267
            $options['scale'] = $fieldMetadata->getScale();
456
        }
457
458 267
        if ($fieldMetadata->getColumnDefinition()) {
459 1
            $options['columnDefinition'] = $fieldMetadata->getColumnDefinition();
460
        }
461
462 267
        $fieldOptions = $fieldMetadata->getOptions();
463
464
        // the 'default' option can be overwritten here
465 267
        $options = $this->gatherColumnOptions($fieldOptions) + $options;
466
467 267
        if ($fieldMetadata->hasValueGenerator() && $fieldMetadata->getValueGenerator()->getType() === GeneratorType::IDENTITY && $classMetadata->getIdentifierFieldNames() === [$fieldName]) {
468 233
            $options['autoincrement'] = true;
469
        }
470
471 267
        if ($classMetadata->inheritanceType === InheritanceType::JOINED && ! $classMetadata->isRootEntity()) {
472 59
            $options['autoincrement'] = false;
473
        }
474
475 267
        $quotedColumnName = $this->platform->quoteIdentifier($fieldMetadata->getColumnName());
476
477 267
        if ($table->hasColumn($quotedColumnName)) {
478
            // required in some inheritance scenarios
479
            $table->changeColumn($quotedColumnName, $options);
480
481
            $column = $table->getColumn($quotedColumnName);
482
        } else {
483 267
            $column = $table->addColumn($quotedColumnName, $columnType, $options);
484
        }
485
486 267
        if ($fieldMetadata->isUnique()) {
487 17
            $table->addUniqueIndex([$columnName]);
488
        }
489
490 267
        return $column;
491
    }
492
493
    /**
494
     * Gathers the SQL for properly setting up the relations of the given class.
495
     * This includes the SQL for foreign key constraints and join tables.
496
     *
497
     * @param ClassMetadata $class
498
     * @param Table         $table
499
     * @param Schema        $schema
500
     * @param mixed[][]     $addedFks
501
     * @param bool[]        $blacklistedFks
502
     *
503
     * @throws ORMException
504
     */
505 267
    private function gatherRelationsSql($class, $table, $schema, &$addedFks, &$blacklistedFks)
506
    {
507 267
        foreach ($class->getDeclaredPropertiesIterator() as $fieldName => $property) {
508 267
            if (! ($property instanceof AssociationMetadata)) {
509 267
                continue;
510
            }
511
512 188
            if ($class->isInheritedProperty($fieldName) && ! $property->getDeclaringClass()->isMappedSuperclass) {
513 21
                continue;
514
            }
515
516 188
            if (! $property->isOwningSide()) {
517 134
                continue;
518
            }
519
520 188
            $foreignClass = $this->em->getClassMetadata($property->getTargetEntity());
521
522
            switch (true) {
523 188
                case $property instanceof ToOneAssociationMetadata:
524 171
                    $primaryKeyColumns = []; // PK is unnecessary for this relation-type
525
526 171
                    $this->gatherRelationJoinColumns(
527 171
                        $property->getJoinColumns(),
528 171
                        $table,
529 171
                        $foreignClass,
0 ignored issues
show
Bug introduced by
$foreignClass of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Tools\Schem...erRelationJoinColumns(). ( Ignorable by Annotation )

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

529
                        /** @scrutinizer ignore-type */ $foreignClass,
Loading history...
530 171
                        $property,
531 171
                        $primaryKeyColumns,
532 171
                        $addedFks,
533 171
                        $blacklistedFks
534
                    );
535
536 171
                    break;
537
538 42
                case $property instanceof OneToManyAssociationMetadata:
539
                    //... create join table, one-many through join table supported later
540
                    throw NotSupported::create();
541
542 42
                case $property instanceof ManyToManyAssociationMetadata:
543
                    // create join table
544 42
                    $joinTable     = $property->getJoinTable();
545 42
                    $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
546 42
                    $theJoinTable  = $schema->createTable($joinTableName);
547
548 42
                    $primaryKeyColumns = [];
549
550
                    // Build first FK constraint (relation table => source table)
551 42
                    $this->gatherRelationJoinColumns(
552 42
                        $joinTable->getJoinColumns(),
553 42
                        $theJoinTable,
554 42
                        $class,
555 42
                        $property,
556 42
                        $primaryKeyColumns,
557 42
                        $addedFks,
558 42
                        $blacklistedFks
559
                    );
560
561
                    // Build second FK constraint (relation table => target table)
562 42
                    $this->gatherRelationJoinColumns(
563 42
                        $joinTable->getInverseJoinColumns(),
564 42
                        $theJoinTable,
565 42
                        $foreignClass,
566 42
                        $property,
567 42
                        $primaryKeyColumns,
568 42
                        $addedFks,
569 42
                        $blacklistedFks
570
                    );
571
572 42
                    $theJoinTable->setPrimaryKey($primaryKeyColumns);
573
574 42
                    break;
575
            }
576
        }
577 267
    }
578
579
    /**
580
     * Gets the class metadata that is responsible for the definition of the referenced column name.
581
     *
582
     * Previously this was a simple task, but with DDC-117 this problem is actually recursive. If its
583
     * not a simple field, go through all identifier field names that are associations recursively and
584
     * find that referenced column name.
585
     *
586
     * TODO: Is there any way to make this code more pleasing?
587
     *
588
     * @param ClassMetadata $class
589
     * @param string        $referencedColumnName
590
     *
591
     * @return mixed[] (ClassMetadata, referencedFieldName)
592
     */
593 188
    private function getDefiningClass($class, $referencedColumnName)
594
    {
595 188
        if (isset($class->fieldNames[$referencedColumnName])) {
596 188
            $propertyName = $class->fieldNames[$referencedColumnName];
597
598 188
            if ($class->hasField($propertyName)) {
599 188
                return [$class, $propertyName];
600
            }
601
        }
602
603 10
        $idColumns        = $class->getIdentifierColumns($this->em);
604 10
        $idColumnNameList = array_keys($idColumns);
605
606 10
        if (! in_array($referencedColumnName, $idColumnNameList, true)) {
607
            return null;
608
        }
609
610
        // it seems to be an entity as foreign key
611 10
        foreach ($class->getIdentifierFieldNames() as $fieldName) {
612 10
            $property = $class->getProperty($fieldName);
613
614 10
            if (! ($property instanceof AssociationMetadata)) {
615 5
                continue;
616
            }
617
618 10
            $joinColumns = $property->getJoinColumns();
0 ignored issues
show
Bug introduced by
The method getJoinColumns() does not exist on Doctrine\ORM\Mapping\AssociationMetadata. It seems like you code against a sub-type of Doctrine\ORM\Mapping\AssociationMetadata such as Doctrine\ORM\Mapping\ToOneAssociationMetadata. ( Ignorable by Annotation )

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

618
            /** @scrutinizer ignore-call */ 
619
            $joinColumns = $property->getJoinColumns();
Loading history...
619
620 10
            if (count($joinColumns) > 1) {
621
                throw MappingException::noSingleAssociationJoinColumnFound($class->getClassName(), $fieldName);
622
            }
623
624 10
            $joinColumn = reset($joinColumns);
625
626 10
            if ($joinColumn->getColumnName() === $referencedColumnName) {
627 10
                $targetEntity = $this->em->getClassMetadata($property->getTargetEntity());
628
629 10
                return $this->getDefiningClass($targetEntity, $joinColumn->getReferencedColumnName());
0 ignored issues
show
Bug introduced by
$targetEntity of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Tools\SchemaTool::getDefiningClass(). ( Ignorable by Annotation )

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

629
                return $this->getDefiningClass(/** @scrutinizer ignore-type */ $targetEntity, $joinColumn->getReferencedColumnName());
Loading history...
630
            }
631
        }
632
633
        return null;
634
    }
635
636
    /**
637
     * Gathers columns and fk constraints that are required for one part of relationship.
638
     *
639
     * @param JoinColumnMetadata[] $joinColumns
640
     * @param Table                $theJoinTable
641
     * @param ClassMetadata        $class
642
     * @param AssociationMetadata  $mapping
643
     * @param string[]             $primaryKeyColumns
644
     * @param mixed[][]            $addedFks
645
     * @param bool[]               $blacklistedFks
646
     *
647
     * @throws ORMException
648
     */
649 188
    private function gatherRelationJoinColumns(
650
        $joinColumns,
651
        $theJoinTable,
652
        $class,
653
        $mapping,
654
        &$primaryKeyColumns,
655
        &$addedFks,
656
        &$blacklistedFks
657
    ) {
658 188
        $localColumns      = [];
659 188
        $foreignColumns    = [];
660 188
        $fkOptions         = [];
661 188
        $foreignTableName  = $class->table->getQuotedQualifiedName($this->platform);
662 188
        $uniqueConstraints = [];
663
664 188
        foreach ($joinColumns as $joinColumn) {
665 188
            [$definingClass, $referencedFieldName] = $this->getDefiningClass(
666 188
                $class,
667 188
                $joinColumn->getReferencedColumnName()
668
            );
669
670 188
            if (! $definingClass) {
671
                throw MissingColumnException::fromColumnSourceAndTarget(
672
                    $joinColumn->getReferencedColumnName(),
673
                    $mapping->getSourceEntity(),
674
                    $mapping->getTargetEntity()
675
                );
676
            }
677
678 188
            $quotedColumnName           = $this->platform->quoteIdentifier($joinColumn->getColumnName());
679 188
            $quotedReferencedColumnName = $this->platform->quoteIdentifier($joinColumn->getReferencedColumnName());
680
681 188
            $primaryKeyColumns[] = $quotedColumnName;
682 188
            $localColumns[]      = $quotedColumnName;
683 188
            $foreignColumns[]    = $quotedReferencedColumnName;
684
685 188
            if (! $theJoinTable->hasColumn($quotedColumnName)) {
686
                // Only add the column to the table if it does not exist already.
687
                // It might exist already if the foreign key is mapped into a regular
688
                // property as well.
689 186
                $property  = $definingClass->getProperty($referencedFieldName);
690 186
                $columnDef = null;
691
692 186
                if (! empty($joinColumn->getColumnDefinition())) {
693
                    $columnDef = $joinColumn->getColumnDefinition();
694 186
                } elseif ($property->getColumnDefinition()) {
695 1
                    $columnDef = $property->getColumnDefinition();
696
                }
697
698 186
                $columnType    = $property->getTypeName();
699
                $columnOptions = [
700 186
                    'notnull'          => ! $joinColumn->isNullable(),
701 186
                    'columnDefinition' => $columnDef,
702
                ];
703
704 186
                $columnOptions = $columnOptions + $this->gatherColumnOptions($property->getOptions());
0 ignored issues
show
introduced by
Use "+=" operator instead of "=" and "+".
Loading history...
705
706 186
                switch ($columnType) {
707 186
                    case 'string':
708 9
                        $columnOptions['length'] = is_int($property->getLength()) ? $property->getLength() : 255;
709 9
                        break;
710
711 182
                    case 'decimal':
712
                        $columnOptions['scale']     = $property->getScale();
713
                        $columnOptions['precision'] = $property->getPrecision();
714
                        break;
715
                }
716
717 186
                $theJoinTable->addColumn($quotedColumnName, $columnType, $columnOptions);
718
            }
719
720 188
            if ($joinColumn->isUnique()) {
721 58
                $uniqueConstraints[] = ['columns' => [$quotedColumnName]];
722
            }
723
724 188
            if (! empty($joinColumn->getOnDelete())) {
725 15
                $fkOptions['onDelete'] = $joinColumn->getOnDelete();
726
            }
727
        }
728
729
        // Prefer unique constraints over implicit simple indexes created for foreign keys.
730
        // Also avoids index duplication.
731 188
        foreach ($uniqueConstraints as $indexName => $unique) {
732 58
            $theJoinTable->addUniqueIndex($unique['columns'], is_numeric($indexName) ? null : $indexName);
733
        }
734
735 188
        $compositeName = $theJoinTable->getName() . '.' . implode('', $localColumns);
736
737 188
        if (isset($addedFks[$compositeName])
738 1
            && ($foreignTableName !== $addedFks[$compositeName]['foreignTableName']
739 188
            || 0 < count(array_diff($foreignColumns, $addedFks[$compositeName]['foreignColumns'])))
740
        ) {
741 1
            foreach ($theJoinTable->getForeignKeys() as $fkName => $key) {
742 1
                if (count(array_diff($key->getLocalColumns(), $localColumns)) === 0
743 1
                    && (($key->getForeignTableName() !== $foreignTableName)
744 1
                    || 0 < count(array_diff($key->getForeignColumns(), $foreignColumns)))
745
                ) {
746 1
                    $theJoinTable->removeForeignKey($fkName);
747 1
                    break;
748
                }
749
            }
750
751 1
            $blacklistedFks[$compositeName] = true;
752 188
        } elseif (! isset($blacklistedFks[$compositeName])) {
753 188
            $addedFks[$compositeName] = [
754 188
                'foreignTableName' => $foreignTableName,
755 188
                'foreignColumns'   => $foreignColumns,
756
            ];
757
758 188
            $theJoinTable->addForeignKeyConstraint(
759 188
                $foreignTableName,
760 188
                $localColumns,
761 188
                $foreignColumns,
762 188
                $fkOptions
763
            );
764
        }
765 188
    }
766
767
    /**
768
     * @param mixed[] $mapping
769
     *
770
     * @return mixed[]
771
     */
772 267
    private function gatherColumnOptions(array $mapping) : array
773
    {
774 267
        if (empty($mapping)) {
775 267
            return [];
776
        }
777
778 36
        $options = array_intersect_key($mapping, array_flip(self::KNOWN_COLUMN_OPTIONS));
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 24 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
introduced by
Function array_intersect_key() should not be referenced via a fallback global name, but via a use statement.
Loading history...
introduced by
Function array_flip() should not be referenced via a fallback global name, but via a use statement.
Loading history...
779 36
        $options['customSchemaOptions'] = array_diff_key($mapping, $options);
0 ignored issues
show
introduced by
Function array_diff_key() should not be referenced via a fallback global name, but via a use statement.
Loading history...
780
781 36
        return $options;
782
    }
783
784
    /**
785
     * Drops the database schema for the given classes.
786
     *
787
     * In any way when an exception is thrown it is suppressed since drop was
788
     * issued for all classes of the schema and some probably just don't exist.
789
     *
790
     * @param ClassMetadata[] $classes
791
     */
792 8
    public function dropSchema(array $classes)
793
    {
794 8
        $dropSchemaSql = $this->getDropSchemaSQL($classes);
795 8
        $conn          = $this->em->getConnection();
796
797 8
        foreach ($dropSchemaSql as $sql) {
798
            try {
799 8
                $conn->executeQuery($sql);
800 1
            } catch (Throwable $e) {
801
                // ignored
802
            }
803
        }
804 8
    }
805
806
    /**
807
     * Drops all elements in the database of the current connection.
808
     */
809
    public function dropDatabase()
810
    {
811
        $dropSchemaSql = $this->getDropDatabaseSQL();
812
        $conn          = $this->em->getConnection();
813
814
        foreach ($dropSchemaSql as $sql) {
815
            $conn->executeQuery($sql);
816
        }
817
    }
818
819
    /**
820
     * Gets the SQL needed to drop the database schema for the connections database.
821
     *
822
     * @return string[]
823
     */
824
    public function getDropDatabaseSQL()
825
    {
826
        $sm     = $this->em->getConnection()->getSchemaManager();
827
        $schema = $sm->createSchema();
828
829
        $visitor = new DropSchemaSqlCollector($this->platform);
830
        $schema->visit($visitor);
831
832
        return $visitor->getQueries();
833
    }
834
835
    /**
836
     * Gets SQL to drop the tables defined by the passed classes.
837
     *
838
     * @param ClassMetadata[] $classes
839
     *
840
     * @return string[]
841
     */
842 8
    public function getDropSchemaSQL(array $classes)
843
    {
844 8
        $visitor = new DropSchemaSqlCollector($this->platform);
845 8
        $schema  = $this->getSchemaFromMetadata($classes);
846
847 8
        $sm         = $this->em->getConnection()->getSchemaManager();
848 8
        $fullSchema = $sm->createSchema();
849
850 8
        foreach ($fullSchema->getTables() as $table) {
851 8
            if (! $schema->hasTable($table->getName())) {
852 6
                foreach ($table->getForeignKeys() as $foreignKey) {
853
                    /** @var $foreignKey \Doctrine\DBAL\Schema\ForeignKeyConstraint */
854
                    if ($schema->hasTable($foreignKey->getForeignTableName())) {
855
                        $visitor->acceptForeignKey($table, $foreignKey);
856
                    }
857
                }
858
            } else {
859 8
                $visitor->acceptTable($table);
860 8
                foreach ($table->getForeignKeys() as $foreignKey) {
861
                    $visitor->acceptForeignKey($table, $foreignKey);
862
                }
863
            }
864
        }
865
866 8
        if ($this->platform->supportsSequences()) {
867
            foreach ($schema->getSequences() as $sequence) {
868
                $visitor->acceptSequence($sequence);
869
            }
870
871
            foreach ($schema->getTables() as $table) {
872
                /** @var $sequence Table */
873
                if ($table->hasPrimaryKey()) {
874
                    $columns = $table->getPrimaryKey()->getColumns();
875
                    if (count($columns) === 1) {
876
                        $checkSequence = $table->getName() . '_' . $columns[0] . '_seq';
877
                        if ($fullSchema->hasSequence($checkSequence)) {
878
                            $visitor->acceptSequence($fullSchema->getSequence($checkSequence));
879
                        }
880
                    }
881
                }
882
            }
883
        }
884
885 8
        return $visitor->getQueries();
886
    }
887
888
    /**
889
     * Updates the database schema of the given classes by comparing the ClassMetadata
890
     * instances to the current database schema that is inspected.
891
     *
892
     * @param ClassMetadata[] $classes
893
     * @param bool            $saveMode If TRUE, only performs a partial update
894
     *                                  without dropping assets which are scheduled for deletion.
895
     */
896
    public function updateSchema(array $classes, $saveMode = false)
897
    {
898
        $updateSchemaSql = $this->getUpdateSchemaSql($classes, $saveMode);
899
        $conn            = $this->em->getConnection();
900
901
        foreach ($updateSchemaSql as $sql) {
902
            $conn->executeQuery($sql);
903
        }
904
    }
905
906
    /**
907
     * Gets the sequence of SQL statements that need to be performed in order
908
     * to bring the given class mappings in-synch with the relational schema.
909
     *
910
     * @param ClassMetadata[] $classes  The classes to consider.
911
     * @param bool            $saveMode If TRUE, only generates SQL for a partial update
912
     *                                  that does not include SQL for dropping assets which are scheduled for deletion.
913
     *
914
     * @return string[] The sequence of SQL statements.
915
     */
916 1
    public function getUpdateSchemaSql(array $classes, $saveMode = false)
917
    {
918 1
        $sm = $this->em->getConnection()->getSchemaManager();
919
920 1
        $fromSchema = $sm->createSchema();
921 1
        $toSchema   = $this->getSchemaFromMetadata($classes);
922
923 1
        $comparator = new Comparator();
924 1
        $schemaDiff = $comparator->compare($fromSchema, $toSchema);
925
926 1
        if ($saveMode) {
927
            return $schemaDiff->toSaveSql($this->platform);
928
        }
929
930 1
        return $schemaDiff->toSql($this->platform);
931
    }
932
}
933