Completed
Push — master ( 23a060...e747f7 )
by Luís
45s queued 37s
created

SchemaTool   F

Complexity

Total Complexity 144

Size/Duplication

Total Lines 885
Duplicated Lines 0 %

Test Coverage

Coverage 85.32%

Importance

Changes 0
Metric Value
eloc 386
dl 0
loc 885
ccs 337
cts 395
cp 0.8532
rs 2
c 0
b 0
f 0
wmc 144

18 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A dropSchema() 0 9 3
A gatherColumns() 0 17 6
A gatherColumnOptions() 0 10 2
A getUpdateSchemaSql() 0 15 2
C getDropSchemaSQL() 0 44 12
F gatherColumn() 0 63 16
A getDropDatabaseSQL() 0 9 1
A addDiscriminatorColumnDefinition() 0 24 4
A updateSchema() 0 7 2
B getDefiningClass() 0 41 8
F getSchemaFromMetadata() 0 232 46
B gatherRelationsSql() 0 70 9
A dropDatabase() 0 7 2
A getCreateSchemaSql() 0 5 1
F gatherRelationJoinColumns() 0 114 21
A createSchema() 0 10 3
A processingNotRequired() 0 6 5

How to fix   Complexity   

Complex Class

Complex classes like SchemaTool often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SchemaTool, and based on these observations, apply Extract Interface, too.

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_diff_key;
33
use function array_flip;
34
use function array_intersect_key;
35
use function array_keys;
36
use function count;
37
use function implode;
38
use function in_array;
39
use function is_int;
40
use function is_numeric;
41
use function reset;
42
use function strtolower;
43
44
/**
45
 * The SchemaTool is a tool to create/drop/update database schemas based on
46
 * <tt>ClassMetadata</tt> class descriptors.
47
 */
48
class SchemaTool
49
{
50
    private const KNOWN_COLUMN_OPTIONS = ['comment', 'unsigned', 'fixed', 'default'];
51
52
    /** @var EntityManagerInterface */
53
    private $em;
54
55
    /** @var AbstractPlatform */
56
    private $platform;
57
58
    /**
59
     * Initializes a new SchemaTool instance that uses the connection of the
60
     * provided EntityManager.
61
     */
62 1231
    public function __construct(EntityManagerInterface $em)
63
    {
64 1231
        $this->em       = $em;
65 1231
        $this->platform = $em->getConnection()->getDatabasePlatform();
66 1231
    }
67
68
    /**
69
     * Creates the database schema for the given array of ClassMetadata instances.
70
     *
71
     * @param ClassMetadata[] $classes
72
     *
73
     * @throws ToolsException
74
     */
75 257
    public function createSchema(array $classes)
76
    {
77 257
        $createSchemaSql = $this->getCreateSchemaSql($classes);
78 257
        $conn            = $this->em->getConnection();
79
80 257
        foreach ($createSchemaSql as $sql) {
81
            try {
82 257
                $conn->executeQuery($sql);
83 69
            } catch (Throwable $e) {
84 69
                throw ToolsException::schemaToolFailure($sql, $e);
85
            }
86
        }
87 188
    }
88
89
    /**
90
     * Gets the list of DDL statements that are required to create the database schema for
91
     * the given list of ClassMetadata instances.
92
     *
93
     * @param ClassMetadata[] $classes
94
     *
95
     * @return string[] The SQL statements needed to create the schema for the classes.
96
     */
97 257
    public function getCreateSchemaSql(array $classes)
98
    {
99 257
        $schema = $this->getSchemaFromMetadata($classes);
100
101 257
        return $schema->toSql($this->platform);
102
    }
103
104
    /**
105
     * Detects instances of ClassMetadata that don't need to be processed in the SchemaTool context.
106
     *
107
     * @param ClassMetadata   $class
108
     * @param ClassMetadata[] $processedClasses
109
     *
110
     * @return bool
111
     */
112 267
    private function processingNotRequired($class, array $processedClasses)
113
    {
114 267
        return isset($processedClasses[$class->getClassName()]) ||
115 267
            $class->isMappedSuperclass ||
116 267
            $class->isEmbeddedClass ||
117 267
            ($class->inheritanceType === InheritanceType::SINGLE_TABLE && ! $class->isRootEntity());
118
    }
119
120
    /**
121
     * Creates a Schema instance from a given set of metadata classes.
122
     *
123
     * @param ClassMetadata[] $classes
124
     *
125
     * @return Schema
126
     *
127
     * @throws ORMException
128
     */
129 267
    public function getSchemaFromMetadata(array $classes)
130
    {
131
        // Reminder for processed classes, used for hierarchies
132 267
        $processedClasses     = [];
133 267
        $eventManager         = $this->em->getEventManager();
134 267
        $schemaManager        = $this->em->getConnection()->getSchemaManager();
135 267
        $metadataSchemaConfig = $schemaManager->createSchemaConfig();
136
137 267
        $metadataSchemaConfig->setExplicitForeignKeyIndexes(false);
138 267
        $schema = new Schema([], [], $metadataSchemaConfig);
139
140 267
        $addedFks       = [];
141 267
        $blacklistedFks = [];
142
143 267
        foreach ($classes as $class) {
144
            /** @var ClassMetadata $class */
145 267
            if ($this->processingNotRequired($class, $processedClasses)) {
146 19
                continue;
147
            }
148
149 267
            $table = $schema->createTable($class->table->getQuotedQualifiedName($this->platform));
150
151 267
            switch ($class->inheritanceType) {
152
                case InheritanceType::SINGLE_TABLE:
153 21
                    $this->gatherColumns($class, $table);
154 21
                    $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
155
156
                    // Add the discriminator column
157 21
                    $this->addDiscriminatorColumnDefinition($class, $table);
158
159
                    // Aggregate all the information from all classes in the hierarchy
160 21
                    $parentClass = $class;
161
162 21
                    while (($parentClass = $parentClass->getParent()) !== null) {
163
                        // Parent class information is already contained in this class
164
                        $processedClasses[$parentClass->getClassName()] = true;
165
                    }
166
167 21
                    foreach ($class->getSubClasses() as $subClassName) {
168 19
                        $subClass = $this->em->getClassMetadata($subClassName);
169
170 19
                        $this->gatherColumns($subClass, $table);
171 19
                        $this->gatherRelationsSql($subClass, $table, $schema, $addedFks, $blacklistedFks);
172
173 19
                        $processedClasses[$subClassName] = true;
174
                    }
175
176 21
                    break;
177
178
                case InheritanceType::JOINED:
179
                    // Add all non-inherited fields as columns
180 60
                    $pkColumns = [];
181
182 60
                    foreach ($class->getDeclaredPropertiesIterator() as $fieldName => $property) {
183 60
                        if (! ($property instanceof FieldMetadata)) {
184 16
                            continue;
185
                        }
186
187 60
                        if (! $class->isInheritedProperty($fieldName)) {
188 60
                            $columnName = $this->platform->quoteIdentifier($property->getColumnName());
189
190 60
                            $this->gatherColumn($class, $property, $table);
191
192 60
                            if ($class->isIdentifier($fieldName)) {
193 60
                                $pkColumns[] = $columnName;
194
                            }
195
                        }
196
                    }
197
198 60
                    $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
199
200
                    // Add the discriminator column only to the root table
201 60
                    if ($class->isRootEntity()) {
202 60
                        $this->addDiscriminatorColumnDefinition($class, $table);
203
                    } else {
204
                        // Add an ID FK column to child tables
205 59
                        $inheritedKeyColumns = [];
206
207 59
                        foreach ($class->identifier as $identifierField) {
208 59
                            $idProperty = $class->getProperty($identifierField);
209
210 59
                            if ($class->isInheritedProperty($identifierField)) {
211 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

211
                                $column     = $this->gatherColumn($class, /** @scrutinizer ignore-type */ $idProperty, $table);
Loading history...
212 59
                                $columnName = $column->getQuotedName($this->platform);
213
214
                                // TODO: This seems rather hackish, can we optimize it?
215 59
                                $column->setAutoincrement(false);
216
217 59
                                $pkColumns[]           = $columnName;
218 59
                                $inheritedKeyColumns[] = $columnName;
219
                            }
220
                        }
221
222 59
                        if (! empty($inheritedKeyColumns)) {
223
                            // Add a FK constraint on the ID column
224 59
                            $rootClass = $this->em->getClassMetadata($class->getRootClassName());
225
226 59
                            $table->addForeignKeyConstraint(
227 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...
228 59
                                $inheritedKeyColumns,
229 59
                                $inheritedKeyColumns,
230 59
                                ['onDelete' => 'CASCADE']
231
                            );
232
                        }
233
                    }
234
235 60
                    $table->setPrimaryKey($pkColumns);
236
237 60
                    break;
238
239
                case InheritanceType::TABLE_PER_CLASS:
240
                    throw NotSupported::create();
241
242
                default:
243 241
                    $this->gatherColumns($class, $table);
244 241
                    $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
245
246 241
                    break;
247
            }
248
249 267
            $pkColumns = [];
250
251 267
            foreach ($class->identifier as $identifierField) {
252 267
                $property = $class->getProperty($identifierField);
253
254 267
                if ($property instanceof FieldMetadata) {
255 266
                    $pkColumns[] = $this->platform->quoteIdentifier($property->getColumnName());
256
257 266
                    continue;
258
                }
259
260 32
                if ($property instanceof ToOneAssociationMetadata) {
261 32
                    foreach ($property->getJoinColumns() as $joinColumn) {
262 32
                        $pkColumns[] = $this->platform->quoteIdentifier($joinColumn->getColumnName());
263
                    }
264
                }
265
            }
266
267 267
            if (! $table->hasIndex('primary')) {
268 248
                $table->setPrimaryKey($pkColumns);
269
            }
270
271
            // there can be unique indexes automatically created for join column
272
            // if join column is also primary key we should keep only primary key on this column
273
            // so, remove indexes overruled by primary key
274 267
            $primaryKey = $table->getIndex('primary');
275
276 267
            foreach ($table->getIndexes() as $idxKey => $existingIndex) {
277 267
                if ($primaryKey->overrules($existingIndex)) {
278 2
                    $table->dropIndex($idxKey);
279
                }
280
            }
281
282 267
            if ($class->table->getIndexes()) {
283 1
                foreach ($class->table->getIndexes() as $indexName => $indexData) {
284 1
                    $indexName = is_numeric($indexName) ? null : $indexName;
285 1
                    $index     = new Index($indexName, $indexData['columns'], $indexData['unique'], $indexData['flags'], $indexData['options']);
286
287 1
                    foreach ($table->getIndexes() as $tableIndexName => $tableIndex) {
288 1
                        if ($tableIndex->isFullfilledBy($index)) {
289
                            $table->dropIndex($tableIndexName);
290
                            break;
291
                        }
292
                    }
293
294 1
                    if ($indexData['unique']) {
295
                        $table->addUniqueIndex($indexData['columns'], $indexName, $indexData['options']);
296
                    } else {
297 1
                        $table->addIndex($indexData['columns'], $indexName, $indexData['flags'], $indexData['options']);
298
                    }
299
                }
300
            }
301
302 267
            if ($class->table->getUniqueConstraints()) {
303 4
                foreach ($class->table->getUniqueConstraints() as $indexName => $indexData) {
304 4
                    $indexName = is_numeric($indexName) ? null : $indexName;
305 4
                    $uniqIndex = new Index($indexName, $indexData['columns'], true, false, $indexData['flags'], $indexData['options']);
306
307 4
                    foreach ($table->getIndexes() as $tableIndexName => $tableIndex) {
308 4
                        if ($tableIndex->isFullfilledBy($uniqIndex)) {
309 3
                            $table->dropIndex($tableIndexName);
310 3
                            break;
311
                        }
312
                    }
313
314 4
                    $table->addUniqueConstraint($indexData['columns'], $indexName, $indexData['flags'], $indexData['options']);
315
                }
316
            }
317
318 267
            if ($class->table->getOptions()) {
319 1
                foreach ($class->table->getOptions() as $key => $val) {
320 1
                    $table->addOption($key, $val);
321
                }
322
            }
323
324 267
            $processedClasses[$class->getClassName()] = true;
325
326 267
            foreach ($class->getDeclaredPropertiesIterator() as $property) {
327 267
                if (! $property instanceof FieldMetadata
328 267
                    || ! $property->hasValueGenerator()
329 234
                    || $property->getValueGenerator()->getType() !== GeneratorType::SEQUENCE
330 267
                    || $class->getClassName() !== $class->getRootClassName()) {
331 267
                    continue;
332
                }
333
334
                $quotedName = $this->platform->quoteIdentifier($property->getValueGenerator()->getDefinition()['sequenceName']);
335
336
                if (! $schema->hasSequence($quotedName)) {
337
                    $schema->createSequence($quotedName, $property->getValueGenerator()->getDefinition()['allocationSize']);
338
                }
339
            }
340
341 267
            if ($eventManager->hasListeners(ToolEvents::postGenerateSchemaTable)) {
342 1
                $eventManager->dispatchEvent(
343 1
                    ToolEvents::postGenerateSchemaTable,
344 1
                    new GenerateSchemaTableEventArgs($class, $schema, $table)
345
                );
346
            }
347
        }
348
349 267
        if (! $this->platform->supportsSchemas() && ! $this->platform->canEmulateSchemas()) {
350 9
            $schema->visit(new RemoveNamespacedAssets());
351
        }
352
353 267
        if ($eventManager->hasListeners(ToolEvents::postGenerateSchema)) {
354 1
            $eventManager->dispatchEvent(
355 1
                ToolEvents::postGenerateSchema,
356 1
                new GenerateSchemaEventArgs($this->em, $schema)
357
            );
358
        }
359
360 267
        return $schema;
361
    }
362
363
    /**
364
     * Gets a portable column definition as required by the DBAL for the discriminator
365
     * column of a class.
366
     *
367
     * @param ClassMetadata $class
368
     */
369 76
    private function addDiscriminatorColumnDefinition($class, Table $table)
370
    {
371 76
        $discrColumn     = $class->discriminatorColumn;
372 76
        $discrColumnType = $discrColumn->getTypeName();
373
        $options         = [
374 76
            'notnull' => ! $discrColumn->isNullable(),
375
        ];
376
377 76
        switch ($discrColumnType) {
378 76
            case 'string':
379 69
                $options['length'] = $discrColumn->getLength() ?? 255;
380 69
                break;
381
382 7
            case 'decimal':
383
                $options['scale']     = $discrColumn->getScale();
384
                $options['precision'] = $discrColumn->getPrecision();
385
                break;
386
        }
387
388 76
        if (! empty($discrColumn->getColumnDefinition())) {
389
            $options['columnDefinition'] = $discrColumn->getColumnDefinition();
390
        }
391
392 76
        $table->addColumn($discrColumn->getColumnName(), $discrColumnType, $options);
393 76
    }
394
395
    /**
396
     * Gathers the column definitions as required by the DBAL of all field mappings
397
     * found in the given class.
398
     *
399
     * @param ClassMetadata $class
400
     */
401 248
    private function gatherColumns($class, Table $table)
402
    {
403 248
        $pkColumns = [];
404
405 248
        foreach ($class->getDeclaredPropertiesIterator() as $fieldName => $property) {
406 248
            if (! ($property instanceof FieldMetadata)) {
407 183
                continue;
408
            }
409
410 248
            if ($class->inheritanceType === InheritanceType::SINGLE_TABLE && $class->isInheritedProperty($fieldName)) {
411 19
                continue;
412
            }
413
414 248
            $this->gatherColumn($class, $property, $table);
415
416 248
            if ($property->isPrimaryKey()) {
417 247
                $pkColumns[] = $this->platform->quoteIdentifier($property->getColumnName());
418
            }
419
        }
420 248
    }
421
422
    /**
423
     * Creates a column definition as required by the DBAL from an ORM field mapping definition.
424
     *
425
     * @param ClassMetadata $classMetadata The class that owns the field mapping.
426
     * @param FieldMetadata $fieldMetadata The field mapping.
427
     *
428
     * @return Column The portable column definition as required by the DBAL.
429
     */
430 267
    private function gatherColumn($classMetadata, FieldMetadata $fieldMetadata, Table $table)
431
    {
432 267
        $fieldName  = $fieldMetadata->getName();
433 267
        $columnName = $fieldMetadata->getColumnName();
434 267
        $columnType = $fieldMetadata->getTypeName();
435
436
        $options = [
437 267
            'length'          => $fieldMetadata->getLength(),
438 267
            'notnull'         => ! $fieldMetadata->isNullable(),
439
            'platformOptions' => [
440 267
                'version' => ($classMetadata->isVersioned() && $classMetadata->versionProperty->getName() === $fieldName),
441
            ],
442
        ];
443
444 267
        if ($classMetadata->inheritanceType === InheritanceType::SINGLE_TABLE && $classMetadata->getParent()) {
445 7
            $options['notnull'] = false;
446
        }
447
448 267
        if (strtolower($columnType) === 'string' && $options['length'] === null) {
449
            $options['length'] = 255;
450
        }
451
452 267
        if (is_int($fieldMetadata->getPrecision())) {
453 267
            $options['precision'] = $fieldMetadata->getPrecision();
454
        }
455
456 267
        if (is_int($fieldMetadata->getScale())) {
457 267
            $options['scale'] = $fieldMetadata->getScale();
458
        }
459
460 267
        if ($fieldMetadata->getColumnDefinition()) {
461 1
            $options['columnDefinition'] = $fieldMetadata->getColumnDefinition();
462
        }
463
464 267
        $fieldOptions = $fieldMetadata->getOptions();
465
466
        // the 'default' option can be overwritten here
467 267
        $options = $this->gatherColumnOptions($fieldOptions) + $options;
468
469 267
        if ($fieldMetadata->hasValueGenerator() && $fieldMetadata->getValueGenerator()->getType() === GeneratorType::IDENTITY && $classMetadata->getIdentifierFieldNames() === [$fieldName]) {
470 233
            $options['autoincrement'] = true;
471
        }
472
473 267
        if ($classMetadata->inheritanceType === InheritanceType::JOINED && ! $classMetadata->isRootEntity()) {
474 59
            $options['autoincrement'] = false;
475
        }
476
477 267
        $quotedColumnName = $this->platform->quoteIdentifier($fieldMetadata->getColumnName());
478
479 267
        if ($table->hasColumn($quotedColumnName)) {
480
            // required in some inheritance scenarios
481
            $table->changeColumn($quotedColumnName, $options);
482
483
            $column = $table->getColumn($quotedColumnName);
484
        } else {
485 267
            $column = $table->addColumn($quotedColumnName, $columnType, $options);
486
        }
487
488 267
        if ($fieldMetadata->isUnique()) {
489 17
            $table->addUniqueIndex([$columnName]);
490
        }
491
492 267
        return $column;
493
    }
494
495
    /**
496
     * Gathers the SQL for properly setting up the relations of the given class.
497
     * This includes the SQL for foreign key constraints and join tables.
498
     *
499
     * @param ClassMetadata $class
500
     * @param Table         $table
501
     * @param Schema        $schema
502
     * @param mixed[][]     $addedFks
503
     * @param bool[]        $blacklistedFks
504
     *
505
     * @throws ORMException
506
     */
507 267
    private function gatherRelationsSql($class, $table, $schema, &$addedFks, &$blacklistedFks)
508
    {
509 267
        foreach ($class->getDeclaredPropertiesIterator() as $fieldName => $property) {
510 267
            if (! ($property instanceof AssociationMetadata)) {
511 267
                continue;
512
            }
513
514 188
            if ($class->isInheritedProperty($fieldName) && ! $property->getDeclaringClass()->isMappedSuperclass) {
515 21
                continue;
516
            }
517
518 188
            if (! $property->isOwningSide()) {
519 134
                continue;
520
            }
521
522 188
            $foreignClass = $this->em->getClassMetadata($property->getTargetEntity());
523
524
            switch (true) {
525 188
                case $property instanceof ToOneAssociationMetadata:
526 171
                    $primaryKeyColumns = []; // PK is unnecessary for this relation-type
527
528 171
                    $this->gatherRelationJoinColumns(
529 171
                        $property->getJoinColumns(),
530 171
                        $table,
531 171
                        $foreignClass,
532 171
                        $property,
533 171
                        $primaryKeyColumns,
534 171
                        $addedFks,
535 171
                        $blacklistedFks
536
                    );
537
538 171
                    break;
539
540 42
                case $property instanceof OneToManyAssociationMetadata:
541
                    //... create join table, one-many through join table supported later
542
                    throw NotSupported::create();
543
544 42
                case $property instanceof ManyToManyAssociationMetadata:
545
                    // create join table
546 42
                    $joinTable     = $property->getJoinTable();
547 42
                    $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
548 42
                    $theJoinTable  = $schema->createTable($joinTableName);
549
550 42
                    $primaryKeyColumns = [];
551
552
                    // Build first FK constraint (relation table => source table)
553 42
                    $this->gatherRelationJoinColumns(
554 42
                        $joinTable->getJoinColumns(),
555 42
                        $theJoinTable,
556 42
                        $class,
557 42
                        $property,
558 42
                        $primaryKeyColumns,
559 42
                        $addedFks,
560 42
                        $blacklistedFks
561
                    );
562
563
                    // Build second FK constraint (relation table => target table)
564 42
                    $this->gatherRelationJoinColumns(
565 42
                        $joinTable->getInverseJoinColumns(),
566 42
                        $theJoinTable,
567 42
                        $foreignClass,
568 42
                        $property,
569 42
                        $primaryKeyColumns,
570 42
                        $addedFks,
571 42
                        $blacklistedFks
572
                    );
573
574 42
                    $theJoinTable->setPrimaryKey($primaryKeyColumns);
575
576 42
                    break;
577
            }
578
        }
579 267
    }
580
581
    /**
582
     * Gets the class metadata that is responsible for the definition of the referenced column name.
583
     *
584
     * Previously this was a simple task, but with DDC-117 this problem is actually recursive. If its
585
     * not a simple field, go through all identifier field names that are associations recursively and
586
     * find that referenced column name.
587
     *
588
     * TODO: Is there any way to make this code more pleasing?
589
     *
590
     * @param ClassMetadata $class
591
     * @param string        $referencedColumnName
592
     *
593
     * @return mixed[] (ClassMetadata, referencedFieldName)
594
     */
595 188
    private function getDefiningClass($class, $referencedColumnName)
596
    {
597 188
        if (isset($class->fieldNames[$referencedColumnName])) {
598 188
            $propertyName = $class->fieldNames[$referencedColumnName];
599
600 188
            if ($class->hasField($propertyName)) {
601 188
                return [$class, $propertyName];
602
            }
603
        }
604
605 10
        $idColumns        = $class->getIdentifierColumns($this->em);
606 10
        $idColumnNameList = array_keys($idColumns);
607
608 10
        if (! in_array($referencedColumnName, $idColumnNameList, true)) {
609
            return null;
610
        }
611
612
        // it seems to be an entity as foreign key
613 10
        foreach ($class->getIdentifierFieldNames() as $fieldName) {
614 10
            $property = $class->getProperty($fieldName);
615
616 10
            if (! ($property instanceof AssociationMetadata)) {
617 5
                continue;
618
            }
619
620 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

620
            /** @scrutinizer ignore-call */ 
621
            $joinColumns = $property->getJoinColumns();
Loading history...
621
622 10
            if (count($joinColumns) > 1) {
623
                throw MappingException::noSingleAssociationJoinColumnFound($class->getClassName(), $fieldName);
624
            }
625
626 10
            $joinColumn = reset($joinColumns);
627
628 10
            if ($joinColumn->getColumnName() === $referencedColumnName) {
629 10
                $targetEntity = $this->em->getClassMetadata($property->getTargetEntity());
630
631 10
                return $this->getDefiningClass($targetEntity, $joinColumn->getReferencedColumnName());
632
            }
633
        }
634
635
        return null;
636
    }
637
638
    /**
639
     * Gathers columns and fk constraints that are required for one part of relationship.
640
     *
641
     * @param JoinColumnMetadata[] $joinColumns
642
     * @param Table                $theJoinTable
643
     * @param ClassMetadata        $class
644
     * @param AssociationMetadata  $mapping
645
     * @param string[]             $primaryKeyColumns
646
     * @param mixed[][]            $addedFks
647
     * @param bool[]               $blacklistedFks
648
     *
649
     * @throws ORMException
650
     */
651 188
    private function gatherRelationJoinColumns(
652
        $joinColumns,
653
        $theJoinTable,
654
        $class,
655
        $mapping,
656
        &$primaryKeyColumns,
657
        &$addedFks,
658
        &$blacklistedFks
659
    ) {
660 188
        $localColumns      = [];
661 188
        $foreignColumns    = [];
662 188
        $fkOptions         = [];
663 188
        $foreignTableName  = $class->table->getQuotedQualifiedName($this->platform);
664 188
        $uniqueConstraints = [];
665
666 188
        foreach ($joinColumns as $joinColumn) {
667 188
            [$definingClass, $referencedFieldName] = $this->getDefiningClass(
668 188
                $class,
669 188
                $joinColumn->getReferencedColumnName()
670
            );
671
672 188
            if (! $definingClass) {
673
                throw MissingColumnException::fromColumnSourceAndTarget(
674
                    $joinColumn->getReferencedColumnName(),
675
                    $mapping->getSourceEntity(),
676
                    $mapping->getTargetEntity()
677
                );
678
            }
679
680 188
            $quotedColumnName           = $this->platform->quoteIdentifier($joinColumn->getColumnName());
681 188
            $quotedReferencedColumnName = $this->platform->quoteIdentifier($joinColumn->getReferencedColumnName());
682
683 188
            $primaryKeyColumns[] = $quotedColumnName;
684 188
            $localColumns[]      = $quotedColumnName;
685 188
            $foreignColumns[]    = $quotedReferencedColumnName;
686
687 188
            if (! $theJoinTable->hasColumn($quotedColumnName)) {
688
                // Only add the column to the table if it does not exist already.
689
                // It might exist already if the foreign key is mapped into a regular
690
                // property as well.
691 186
                $property  = $definingClass->getProperty($referencedFieldName);
692 186
                $columnDef = null;
693
694 186
                if (! empty($joinColumn->getColumnDefinition())) {
695
                    $columnDef = $joinColumn->getColumnDefinition();
696 186
                } elseif ($property->getColumnDefinition()) {
697 1
                    $columnDef = $property->getColumnDefinition();
698
                }
699
700 186
                $columnType    = $property->getTypeName();
701
                $columnOptions = [
702 186
                    'notnull'          => ! $joinColumn->isNullable(),
703 186
                    'columnDefinition' => $columnDef,
704
                ];
705
706 186
                $columnOptions += $this->gatherColumnOptions($property->getOptions());
707
708 186
                switch ($columnType) {
709 186
                    case 'string':
710 9
                        $columnOptions['length'] = is_int($property->getLength()) ? $property->getLength() : 255;
711 9
                        break;
712
713 182
                    case 'decimal':
714
                        $columnOptions['scale']     = $property->getScale();
715
                        $columnOptions['precision'] = $property->getPrecision();
716
                        break;
717
                }
718
719 186
                $theJoinTable->addColumn($quotedColumnName, $columnType, $columnOptions);
720
            }
721
722 188
            if ($joinColumn->isUnique()) {
723 58
                $uniqueConstraints[] = ['columns' => [$quotedColumnName]];
724
            }
725
726 188
            if (! empty($joinColumn->getOnDelete())) {
727 15
                $fkOptions['onDelete'] = $joinColumn->getOnDelete();
728
            }
729
        }
730
731
        // Prefer unique constraints over implicit simple indexes created for foreign keys.
732
        // Also avoids index duplication.
733 188
        foreach ($uniqueConstraints as $indexName => $unique) {
734 58
            $theJoinTable->addUniqueIndex($unique['columns'], is_numeric($indexName) ? null : $indexName);
735
        }
736
737 188
        $compositeName = $theJoinTable->getName() . '.' . implode('', $localColumns);
738
739 188
        if (isset($addedFks[$compositeName])
740 1
            && ($foreignTableName !== $addedFks[$compositeName]['foreignTableName']
741 188
            || 0 < count(array_diff($foreignColumns, $addedFks[$compositeName]['foreignColumns'])))
742
        ) {
743 1
            foreach ($theJoinTable->getForeignKeys() as $fkName => $key) {
744 1
                if (count(array_diff($key->getLocalColumns(), $localColumns)) === 0
745 1
                    && (($key->getForeignTableName() !== $foreignTableName)
746 1
                    || 0 < count(array_diff($key->getForeignColumns(), $foreignColumns)))
747
                ) {
748 1
                    $theJoinTable->removeForeignKey($fkName);
749 1
                    break;
750
                }
751
            }
752
753 1
            $blacklistedFks[$compositeName] = true;
754 188
        } elseif (! isset($blacklistedFks[$compositeName])) {
755 188
            $addedFks[$compositeName] = [
756 188
                'foreignTableName' => $foreignTableName,
757 188
                'foreignColumns'   => $foreignColumns,
758
            ];
759
760 188
            $theJoinTable->addForeignKeyConstraint(
761 188
                $foreignTableName,
762 188
                $localColumns,
763 188
                $foreignColumns,
764 188
                $fkOptions
765
            );
766
        }
767 188
    }
768
769
    /**
770
     * @param mixed[] $mapping
771
     *
772
     * @return mixed[]
773
     */
774 267
    private function gatherColumnOptions(array $mapping) : array
775
    {
776 267
        if ($mapping === []) {
777 267
            return [];
778
        }
779
780 36
        $options                        = array_intersect_key($mapping, array_flip(self::KNOWN_COLUMN_OPTIONS));
781 36
        $options['customSchemaOptions'] = array_diff_key($mapping, $options);
782
783 36
        return $options;
784
    }
785
786
    /**
787
     * Drops the database schema for the given classes.
788
     *
789
     * In any way when an exception is thrown it is suppressed since drop was
790
     * issued for all classes of the schema and some probably just don't exist.
791
     *
792
     * @param ClassMetadata[] $classes
793
     */
794 8
    public function dropSchema(array $classes)
795
    {
796 8
        $dropSchemaSql = $this->getDropSchemaSQL($classes);
797 8
        $conn          = $this->em->getConnection();
798
799 8
        foreach ($dropSchemaSql as $sql) {
800
            try {
801 8
                $conn->executeQuery($sql);
802 1
            } catch (Throwable $e) {
803
                // ignored
804
            }
805
        }
806 8
    }
807
808
    /**
809
     * Drops all elements in the database of the current connection.
810
     */
811
    public function dropDatabase()
812
    {
813
        $dropSchemaSql = $this->getDropDatabaseSQL();
814
        $conn          = $this->em->getConnection();
815
816
        foreach ($dropSchemaSql as $sql) {
817
            $conn->executeQuery($sql);
818
        }
819
    }
820
821
    /**
822
     * Gets the SQL needed to drop the database schema for the connections database.
823
     *
824
     * @return string[]
825
     */
826
    public function getDropDatabaseSQL()
827
    {
828
        $sm     = $this->em->getConnection()->getSchemaManager();
829
        $schema = $sm->createSchema();
830
831
        $visitor = new DropSchemaSqlCollector($this->platform);
832
        $schema->visit($visitor);
833
834
        return $visitor->getQueries();
835
    }
836
837
    /**
838
     * Gets SQL to drop the tables defined by the passed classes.
839
     *
840
     * @param ClassMetadata[] $classes
841
     *
842
     * @return string[]
843
     */
844 8
    public function getDropSchemaSQL(array $classes)
845
    {
846 8
        $visitor = new DropSchemaSqlCollector($this->platform);
847 8
        $schema  = $this->getSchemaFromMetadata($classes);
848
849 8
        $sm         = $this->em->getConnection()->getSchemaManager();
850 8
        $fullSchema = $sm->createSchema();
851
852 8
        foreach ($fullSchema->getTables() as $table) {
853 8
            if (! $schema->hasTable($table->getName())) {
854 6
                foreach ($table->getForeignKeys() as $foreignKey) {
855
                    /** @var $foreignKey \Doctrine\DBAL\Schema\ForeignKeyConstraint */
856
                    if ($schema->hasTable($foreignKey->getForeignTableName())) {
857
                        $visitor->acceptForeignKey($table, $foreignKey);
858
                    }
859
                }
860
            } else {
861 8
                $visitor->acceptTable($table);
862 8
                foreach ($table->getForeignKeys() as $foreignKey) {
863
                    $visitor->acceptForeignKey($table, $foreignKey);
864
                }
865
            }
866
        }
867
868 8
        if ($this->platform->supportsSequences()) {
869
            foreach ($schema->getSequences() as $sequence) {
870
                $visitor->acceptSequence($sequence);
871
            }
872
873
            foreach ($schema->getTables() as $table) {
874
                /** @var $sequence Table */
875
                if ($table->hasPrimaryKey()) {
876
                    $columns = $table->getPrimaryKey()->getColumns();
877
                    if (count($columns) === 1) {
878
                        $checkSequence = $table->getName() . '_' . $columns[0] . '_seq';
879
                        if ($fullSchema->hasSequence($checkSequence)) {
880
                            $visitor->acceptSequence($fullSchema->getSequence($checkSequence));
881
                        }
882
                    }
883
                }
884
            }
885
        }
886
887 8
        return $visitor->getQueries();
888
    }
889
890
    /**
891
     * Updates the database schema of the given classes by comparing the ClassMetadata
892
     * instances to the current database schema that is inspected.
893
     *
894
     * @param ClassMetadata[] $classes
895
     * @param bool            $saveMode If TRUE, only performs a partial update
896
     *                                  without dropping assets which are scheduled for deletion.
897
     */
898
    public function updateSchema(array $classes, $saveMode = false)
899
    {
900
        $updateSchemaSql = $this->getUpdateSchemaSql($classes, $saveMode);
901
        $conn            = $this->em->getConnection();
902
903
        foreach ($updateSchemaSql as $sql) {
904
            $conn->executeQuery($sql);
905
        }
906
    }
907
908
    /**
909
     * Gets the sequence of SQL statements that need to be performed in order
910
     * to bring the given class mappings in-synch with the relational schema.
911
     *
912
     * @param ClassMetadata[] $classes  The classes to consider.
913
     * @param bool            $saveMode If TRUE, only generates SQL for a partial update
914
     *                                  that does not include SQL for dropping assets which are scheduled for deletion.
915
     *
916
     * @return string[] The sequence of SQL statements.
917
     */
918 1
    public function getUpdateSchemaSql(array $classes, $saveMode = false)
919
    {
920 1
        $sm = $this->em->getConnection()->getSchemaManager();
921
922 1
        $fromSchema = $sm->createSchema();
923 1
        $toSchema   = $this->getSchemaFromMetadata($classes);
924
925 1
        $comparator = new Comparator();
926 1
        $schemaDiff = $comparator->compare($fromSchema, $toSchema);
927
928 1
        if ($saveMode) {
929
            return $schemaDiff->toSaveSql($this->platform);
930
        }
931
932 1
        return $schemaDiff->toSql($this->platform);
933
    }
934
}
935