SchemaTool::dropDatabase()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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

213
                                $column     = $this->gatherColumn($class, /** @scrutinizer ignore-type */ $idProperty, $table);
Loading history...
214 59
                                $columnName = $column->getQuotedName($this->platform);
215
216
                                // TODO: This seems rather hackish, can we optimize it?
217 59
                                $column->setAutoincrement(false);
218
219 59
                                $pkColumns[]           = $columnName;
220 59
                                $inheritedKeyColumns[] = $columnName;
221
                            }
222
                        }
223
224 59
                        if (! empty($inheritedKeyColumns)) {
225
                            // Add a FK constraint on the ID column
226 59
                            $rootClass = $this->em->getClassMetadata($class->getRootClassName());
227
228 59
                            $table->addForeignKeyConstraint(
229 59
                                $rootClass->table->getQuotedQualifiedName($this->platform),
0 ignored issues
show
Bug introduced by
Accessing table on the interface Doctrine\Persistence\Mapping\ClassMetadata suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
230
                                $inheritedKeyColumns,
231
                                $inheritedKeyColumns,
232 59
                                ['onDelete' => 'CASCADE']
233
                            );
234
                        }
235
                    }
236
237 60
                    $table->setPrimaryKey($pkColumns);
238
239 60
                    break;
240
241
                case InheritanceType::TABLE_PER_CLASS:
242
                    throw NotSupported::create();
243
244
                default:
245 241
                    $this->gatherColumns($class, $table);
246 241
                    $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
247
248 241
                    break;
249
            }
250
251 267
            $pkColumns = [];
252
253 267
            foreach ($class->identifier as $identifierField) {
254 267
                $property = $class->getProperty($identifierField);
255
256 267
                if ($property instanceof FieldMetadata) {
257 266
                    $pkColumns[] = $this->platform->quoteIdentifier($property->getColumnName());
258
259 266
                    continue;
260
                }
261
262 32
                if ($property instanceof ToOneAssociationMetadata) {
263 32
                    foreach ($property->getJoinColumns() as $joinColumn) {
264 32
                        $pkColumns[] = $this->platform->quoteIdentifier($joinColumn->getColumnName());
265
                    }
266
                }
267
            }
268
269 267
            if (! $table->hasIndex('primary')) {
270 248
                $table->setPrimaryKey($pkColumns);
271
            }
272
273
            // there can be unique indexes automatically created for join column
274
            // if join column is also primary key we should keep only primary key on this column
275
            // so, remove indexes overruled by primary key
276 267
            $primaryKey = $table->getIndex('primary');
277
278 267
            foreach ($table->getIndexes() as $idxKey => $existingIndex) {
279 267
                if ($primaryKey->overrules($existingIndex)) {
280 2
                    $table->dropIndex($idxKey);
281
                }
282
            }
283
284 267
            if ($class->table->getIndexes()) {
285 1
                foreach ($class->table->getIndexes() as $indexName => $indexData) {
286 1
                    $indexName = is_numeric($indexName) ? null : $indexName;
287 1
                    $index     = new Index($indexName, $indexData['columns'], $indexData['unique'], false, $indexData['flags'], $indexData['options']);
288
289 1
                    foreach ($table->getIndexes() as $tableIndexName => $tableIndex) {
290 1
                        if ($tableIndex->isFullfilledBy($index)) {
291
                            $table->dropIndex($tableIndexName);
292
                            break;
293
                        }
294
                    }
295
296 1
                    if ($indexData['unique']) {
297
                        $table->addUniqueIndex($indexData['columns'], $indexName, $indexData['options']);
298
                    } else {
299 1
                        $table->addIndex($indexData['columns'], $indexName, $indexData['flags'], $indexData['options']);
300
                    }
301
                }
302
            }
303
304 267
            if ($class->table->getUniqueConstraints()) {
305 4
                foreach ($class->table->getUniqueConstraints() as $indexName => $indexData) {
306 4
                    $indexName = is_numeric($indexName) ? null : $indexName;
307 4
                    $uniqIndex = new Index($indexName, $indexData['columns'], true, false, $indexData['flags'], $indexData['options']);
308
309 4
                    foreach ($table->getIndexes() as $tableIndexName => $tableIndex) {
310 4
                        if ($tableIndex->isFullfilledBy($uniqIndex)) {
311 3
                            $table->dropIndex($tableIndexName);
312 3
                            break;
313
                        }
314
                    }
315
316 4
                    $table->addUniqueConstraint($indexData['columns'], $indexName, $indexData['flags'], $indexData['options']);
317
                }
318
            }
319
320 267
            if ($class->table->getOptions()) {
321 1
                foreach ($class->table->getOptions() as $key => $val) {
322 1
                    $table->addOption($key, $val);
323
                }
324
            }
325
326 267
            $processedClasses[$class->getClassName()] = true;
327
328 267
            foreach ($class->getPropertiesIterator() as $property) {
329 267
                if (! $property instanceof FieldMetadata
330 267
                    || ! $property->hasValueGenerator()
331 234
                    || $property->getValueGenerator()->getType() !== GeneratorType::SEQUENCE
332 267
                    || $class->getClassName() !== $class->getRootClassName()) {
333 267
                    continue;
334
                }
335
336
                $generator  = $property->getValueGenerator()->getGenerator();
337
                $quotedName = $generator->getSequenceName();
0 ignored issues
show
Bug introduced by
The method getSequenceName() does not exist on Doctrine\ORM\Sequencing\Generator\Generator. It seems like you code against a sub-type of Doctrine\ORM\Sequencing\Generator\Generator such as Doctrine\ORM\Sequencing\...rator\SequenceGenerator. ( Ignorable by Annotation )

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

337
                /** @scrutinizer ignore-call */ 
338
                $quotedName = $generator->getSequenceName();
Loading history...
338
339
                if (! $schema->hasSequence($quotedName)) {
340
                    $schema->createSequence($quotedName, $generator->getAllocationSize());
0 ignored issues
show
Bug introduced by
The method getAllocationSize() does not exist on Doctrine\ORM\Sequencing\Generator\Generator. It seems like you code against a sub-type of Doctrine\ORM\Sequencing\Generator\Generator such as Doctrine\ORM\Sequencing\...rator\SequenceGenerator. ( Ignorable by Annotation )

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

340
                    $schema->createSequence($quotedName, $generator->/** @scrutinizer ignore-call */ getAllocationSize());
Loading history...
341
                }
342
            }
343
344 267
            if ($eventManager->hasListeners(ToolEvents::postGenerateSchemaTable)) {
345 1
                $eventManager->dispatchEvent(
346 1
                    ToolEvents::postGenerateSchemaTable,
347 1
                    new GenerateSchemaTableEventArgs($class, $schema, $table)
348
                );
349
            }
350
        }
351
352 267
        if (! $this->platform->supportsSchemas() && ! $this->platform->canEmulateSchemas()) {
353 9
            $schema->visit(new RemoveNamespacedAssets());
354
        }
355
356 267
        if ($eventManager->hasListeners(ToolEvents::postGenerateSchema)) {
357 1
            $eventManager->dispatchEvent(
358 1
                ToolEvents::postGenerateSchema,
359 1
                new GenerateSchemaEventArgs($this->em, $schema)
360
            );
361
        }
362
363 267
        return $schema;
364
    }
365
366
    /**
367
     * Gets a portable column definition as required by the DBAL for the discriminator
368
     * column of a class.
369
     *
370
     * @param ClassMetadata $class
371
     */
372 76
    private function addDiscriminatorColumnDefinition($class, Table $table)
373
    {
374 76
        $discrColumn     = $class->discriminatorColumn;
375 76
        $discrColumnType = $discrColumn->getTypeName();
376
        $options         = [
377 76
            'notnull' => ! $discrColumn->isNullable(),
378
        ];
379
380 76
        switch ($discrColumnType) {
381 76
            case 'string':
382 69
                $options['length'] = $discrColumn->getLength() ?? 255;
383 69
                break;
384
385 7
            case 'decimal':
386
                $options['scale']     = $discrColumn->getScale();
387
                $options['precision'] = $discrColumn->getPrecision();
388
                break;
389
        }
390
391 76
        if (! empty($discrColumn->getColumnDefinition())) {
392
            $options['columnDefinition'] = $discrColumn->getColumnDefinition();
393
        }
394
395 76
        $table->addColumn($discrColumn->getColumnName(), $discrColumnType, $options);
396 76
    }
397
398
    /**
399
     * Gathers the column definitions as required by the DBAL of all field mappings
400
     * found in the given class.
401
     */
402 248
    private function gatherColumns(ClassMetadata $class, Table $table, ?string $columnPrefix = null)
403
    {
404 248
        $pkColumns = [];
405
406 248
        foreach ($class->getPropertiesIterator() as $fieldName => $property) {
407 248
            if ($class->inheritanceType === InheritanceType::SINGLE_TABLE && $class->isInheritedProperty($fieldName)) {
408 19
                continue;
409
            }
410
411
            switch (true) {
412 248
                case $property instanceof FieldMetadata:
413 248
                    $this->gatherColumn($class, $property, $table, $columnPrefix);
414
415 248
                    if ($property->isPrimaryKey()) {
416 247
                        $pkColumns[] = $this->platform->quoteIdentifier($property->getColumnName());
417
                    }
418
419 248
                    break;
420
421 183
                case $property instanceof EmbeddedMetadata:
422
                    $foreignClass = $this->em->getClassMetadata($property->getTargetEntity());
423
424
                    $this->gatherColumns($foreignClass, $table, $property->getColumnPrefix());
425
426
                    break;
427
            }
428
        }
429 248
    }
430
431
    /**
432
     * Creates a column definition as required by the DBAL from an ORM field mapping definition.
433
     *
434
     * @return Column The portable column definition as required by the DBAL.
435
     */
436 267
    private function gatherColumn(
437
        ClassMetadata $classMetadata,
438
        FieldMetadata $fieldMetadata,
439
        Table $table,
440
        ?string $columnPrefix = null
441
    ) {
442 267
        $fieldName  = $fieldMetadata->getName();
443 267
        $columnName = sprintf('%s%s', $columnPrefix, $fieldMetadata->getColumnName());
444 267
        $columnType = $fieldMetadata->getTypeName();
445
446
        $options = [
447 267
            'length'          => $fieldMetadata->getLength(),
448 267
            'notnull'         => ! $fieldMetadata->isNullable(),
449
            'platformOptions' => [
450 267
                'version' => ($classMetadata->isVersioned() && $classMetadata->versionProperty->getName() === $fieldName),
451
            ],
452
        ];
453
454 267
        if ($classMetadata->inheritanceType === InheritanceType::SINGLE_TABLE && $classMetadata->getParent()) {
455 7
            $options['notnull'] = false;
456
        }
457
458 267
        if (strtolower($columnType) === 'string' && $options['length'] === null) {
459
            $options['length'] = 255;
460
        }
461
462 267
        if (is_int($fieldMetadata->getPrecision())) {
463 267
            $options['precision'] = $fieldMetadata->getPrecision();
464
        }
465
466 267
        if (is_int($fieldMetadata->getScale())) {
467 267
            $options['scale'] = $fieldMetadata->getScale();
468
        }
469
470 267
        if ($fieldMetadata->getColumnDefinition()) {
471 1
            $options['columnDefinition'] = $fieldMetadata->getColumnDefinition();
472
        }
473
474 267
        $fieldOptions = $fieldMetadata->getOptions();
475
476
        // the 'default' option can be overwritten here
477 267
        $options = $this->gatherColumnOptions($fieldOptions) + $options;
478
479 267
        if ($fieldMetadata->hasValueGenerator() && $fieldMetadata->getValueGenerator()->getType() === GeneratorType::IDENTITY && $classMetadata->getIdentifierFieldNames() === [$fieldName]) {
480 233
            $options['autoincrement'] = true;
481
        }
482
483 267
        if ($classMetadata->inheritanceType === InheritanceType::JOINED && ! $classMetadata->isRootEntity()) {
484 59
            $options['autoincrement'] = false;
485
        }
486
487 267
        $quotedColumnName = $this->platform->quoteIdentifier($columnName);
488
489 267
        if ($table->hasColumn($quotedColumnName)) {
490
            // required in some inheritance scenarios
491
            $table->changeColumn($quotedColumnName, $options);
492
493
            $column = $table->getColumn($quotedColumnName);
494
        } else {
495 267
            $column = $table->addColumn($quotedColumnName, $columnType, $options);
496
        }
497
498 267
        if ($fieldMetadata->isUnique()) {
499 17
            $table->addUniqueIndex([$columnName]);
500
        }
501
502 267
        return $column;
503
    }
504
505
    /**
506
     * Gathers the SQL for properly setting up the relations of the given class.
507
     * This includes the SQL for foreign key constraints and join tables.
508
     *
509
     * @param ClassMetadata $class
510
     * @param Table         $table
511
     * @param Schema        $schema
512
     * @param mixed[][]     $addedFks
513
     * @param bool[]        $blacklistedFks
514
     *
515
     * @throws ORMException
516
     */
517 267
    private function gatherRelationsSql($class, $table, $schema, &$addedFks, &$blacklistedFks)
518
    {
519 267
        foreach ($class->getPropertiesIterator() as $fieldName => $property) {
520 267
            if (! ($property instanceof AssociationMetadata)) {
521 267
                continue;
522
            }
523
524 188
            if ($class->isInheritedProperty($fieldName) && ! $property->getDeclaringClass()->isMappedSuperclass) {
525 21
                continue;
526
            }
527
528 188
            if (! $property->isOwningSide()) {
529 134
                continue;
530
            }
531
532 188
            $foreignClass = $this->em->getClassMetadata($property->getTargetEntity());
533
534
            switch (true) {
535 188
                case $property instanceof ToOneAssociationMetadata:
536 171
                    $primaryKeyColumns = []; // PK is unnecessary for this relation-type
537
538 171
                    $this->gatherRelationJoinColumns(
539 171
                        $property->getJoinColumns(),
540
                        $table,
541
                        $foreignClass,
542
                        $property,
543
                        $primaryKeyColumns,
544
                        $addedFks,
545
                        $blacklistedFks
546
                    );
547
548 171
                    break;
549
550 42
                case $property instanceof OneToManyAssociationMetadata:
551
                    //... create join table, one-many through join table supported later
552
                    throw NotSupported::create();
553
554 42
                case $property instanceof ManyToManyAssociationMetadata:
555
                    // create join table
556 42
                    $joinTable     = $property->getJoinTable();
557 42
                    $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
558 42
                    $theJoinTable  = $schema->createTable($joinTableName);
559
560 42
                    $primaryKeyColumns = [];
561
562
                    // Build first FK constraint (relation table => source table)
563 42
                    $this->gatherRelationJoinColumns(
564 42
                        $joinTable->getJoinColumns(),
565
                        $theJoinTable,
566
                        $class,
567
                        $property,
568
                        $primaryKeyColumns,
569
                        $addedFks,
570
                        $blacklistedFks
571
                    );
572
573
                    // Build second FK constraint (relation table => target table)
574 42
                    $this->gatherRelationJoinColumns(
575 42
                        $joinTable->getInverseJoinColumns(),
576
                        $theJoinTable,
577
                        $foreignClass,
578
                        $property,
579
                        $primaryKeyColumns,
580
                        $addedFks,
581
                        $blacklistedFks
582
                    );
583
584 42
                    $theJoinTable->setPrimaryKey($primaryKeyColumns);
585
586 42
                    break;
587
            }
588
        }
589 267
    }
590
591
    /**
592
     * Gets the class metadata that is responsible for the definition of the referenced column name.
593
     *
594
     * Previously this was a simple task, but with DDC-117 this problem is actually recursive. If its
595
     * not a simple field, go through all identifier field names that are associations recursively and
596
     * find that referenced column name.
597
     *
598
     * TODO: Is there any way to make this code more pleasing?
599
     *
600
     * @param ClassMetadata $class
601
     * @param string        $referencedColumnName
602
     *
603
     * @return mixed[] (ClassMetadata, referencedFieldName)
604
     */
605 188
    private function getDefiningClass($class, $referencedColumnName)
606
    {
607 188
        if (isset($class->fieldNames[$referencedColumnName])) {
608 188
            $propertyName = $class->fieldNames[$referencedColumnName];
609
610 188
            if ($class->hasField($propertyName)) {
611 188
                return [$class, $propertyName];
612
            }
613
        }
614
615 10
        $idColumns        = $class->getIdentifierColumns($this->em);
616 10
        $idColumnNameList = array_keys($idColumns);
617
618 10
        if (! in_array($referencedColumnName, $idColumnNameList, true)) {
619
            return null;
620
        }
621
622
        // it seems to be an entity as foreign key
623 10
        foreach ($class->getIdentifierFieldNames() as $fieldName) {
624 10
            $property = $class->getProperty($fieldName);
625
626 10
            if (! ($property instanceof AssociationMetadata)) {
627 5
                continue;
628
            }
629
630 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

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