Failed Conditions
Pull Request — master (#7898)
by Guilherme
63:09
created

SchemaTool::gatherColumns()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 7

Importance

Changes 0
Metric Value
cc 7
eloc 14
nc 6
nop 3
dl 0
loc 25
ccs 10
cts 10
cp 1
crap 7
rs 8.8333
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 1230
     * provided EntityManager.
63
     */
64 1230
    public function __construct(EntityManagerInterface $em)
65 1230
    {
66 1230
        $this->em       = $em;
67
        $this->platform = $em->getConnection()->getDatabasePlatform();
68
    }
69
70
    /**
71
     * Creates the database schema for the given array of ClassMetadata instances.
72
     *
73
     * @param ClassMetadata[] $classes
74
     *
75 256
     * @throws ToolsException
76
     */
77 256
    public function createSchema(array $classes)
78 256
    {
79
        $createSchemaSql = $this->getCreateSchemaSql($classes);
80 256
        $conn            = $this->em->getConnection();
81
82 256
        foreach ($createSchemaSql as $sql) {
83 69
            try {
84 69
                $conn->executeQuery($sql);
85
            } catch (Throwable $e) {
86
                throw ToolsException::schemaToolFailure($sql, $e);
87 187
            }
88
        }
89
    }
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 256
     * @return string[] The SQL statements needed to create the schema for the classes.
98
     */
99 256
    public function getCreateSchemaSql(array $classes)
100
    {
101 256
        $schema = $this->getSchemaFromMetadata($classes);
102
103
        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 266
     * @return bool
113
     */
114 266
    private function processingNotRequired($class, array $processedClasses)
115 266
    {
116 266
        return isset($processedClasses[$class->getClassName()]) ||
117 266
            $class->isMappedSuperclass ||
118
            $class->isEmbeddedClass ||
119
            ($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 266
     * @throws ORMException
130
     */
131
    public function getSchemaFromMetadata(array $classes)
132 266
    {
133 266
        // Reminder for processed classes, used for hierarchies
134 266
        $processedClasses     = [];
135 266
        $eventManager         = $this->em->getEventManager();
136
        $schemaManager        = $this->em->getConnection()->getSchemaManager();
137 266
        $metadataSchemaConfig = $schemaManager->createSchemaConfig();
138 266
139
        $metadataSchemaConfig->setExplicitForeignKeyIndexes(false);
140 266
        $schema = new Schema([], [], $metadataSchemaConfig);
141 266
142
        $addedFks       = [];
143 266
        $blacklistedFks = [];
144
145 266
        foreach ($classes as $class) {
146 19
            /** @var ClassMetadata $class */
147
            if ($this->processingNotRequired($class, $processedClasses)) {
148
                continue;
149 266
            }
150
151 266
            $table = $schema->createTable($class->table->getQuotedQualifiedName($this->platform));
152
153 21
            switch ($class->inheritanceType) {
154 21
                case InheritanceType::SINGLE_TABLE:
155
                    $this->gatherColumns($class, $table);
156
                    $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
157 21
158
                    // Add the discriminator column
159
                    $this->addDiscriminatorColumnDefinition($class, $table);
160 21
161
                    // Aggregate all the information from all classes in the hierarchy
162 21
                    $parentClass = $class;
163
164
                    while (($parentClass = $parentClass->getParent()) !== null) {
165
                        // Parent class information is already contained in this class
166
                        $processedClasses[$parentClass->getClassName()] = true;
167 21
                    }
168 19
169
                    foreach ($class->getSubClasses() as $subClassName) {
170 19
                        $subClass = $this->em->getClassMetadata($subClassName);
171 19
172
                        $this->gatherColumns($subClass, $table);
173 19
                        $this->gatherRelationsSql($subClass, $table, $schema, $addedFks, $blacklistedFks);
174
175
                        $processedClasses[$subClassName] = true;
176 21
                    }
177
178
                    break;
179
180 59
                case InheritanceType::JOINED:
181
                    // Add all non-inherited fields as columns
182 59
                    $pkColumns = [];
183 59
184 16
                    foreach ($class->getPropertiesIterator() as $fieldName => $property) {
185
                        if (! ($property instanceof FieldMetadata)) {
186
                            continue;
187 59
                        }
188 59
189
                        if (! $class->isInheritedProperty($fieldName)) {
190 59
                            $columnName = $this->platform->quoteIdentifier($property->getColumnName());
191
192 59
                            $this->gatherColumn($class, $property, $table);
193 59
194
                            if ($class->isIdentifier($fieldName)) {
195
                                $pkColumns[] = $columnName;
196
                            }
197
                        }
198 59
                    }
199
200
                    $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
201 59
202 59
                    // Add the discriminator column only to the root table
203
                    if ($class->isRootEntity()) {
204
                        $this->addDiscriminatorColumnDefinition($class, $table);
205 58
                    } else {
206
                        // Add an ID FK column to child tables
207 58
                        $inheritedKeyColumns = [];
208 58
209
                        foreach ($class->identifier as $identifierField) {
210 58
                            $idProperty = $class->getProperty($identifierField);
211 58
212 58
                            if ($class->isInheritedProperty($identifierField)) {
213
                                $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
                                $columnName = $column->getQuotedName($this->platform);
215 58
216
                                // TODO: This seems rather hackish, can we optimize it?
217 58
                                $column->setAutoincrement(false);
218 58
219
                                $pkColumns[]           = $columnName;
220
                                $inheritedKeyColumns[] = $columnName;
221
                            }
222 58
                        }
223
224 58
                        if (! empty($inheritedKeyColumns)) {
225
                            // Add a FK constraint on the ID column
226 58
                            $rootClass = $this->em->getClassMetadata($class->getRootClassName());
227 58
228 58
                            $table->addForeignKeyConstraint(
229 58
                                $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...
230 58
                                $inheritedKeyColumns,
231
                                $inheritedKeyColumns,
232
                                ['onDelete' => 'CASCADE']
233
                            );
234
                        }
235 59
                    }
236
237 59
                    $table->setPrimaryKey($pkColumns);
238
239
                    break;
240
241
                case InheritanceType::TABLE_PER_CLASS:
242
                    throw NotSupported::create();
243 241
244 241
                default:
245
                    $this->gatherColumns($class, $table);
246 241
                    $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
247
248
                    break;
249 266
            }
250
251 266
            $pkColumns = [];
252 266
253
            foreach ($class->identifier as $identifierField) {
254 266
                $property = $class->getProperty($identifierField);
255 265
256
                if ($property instanceof FieldMetadata) {
257 265
                    $pkColumns[] = $this->platform->quoteIdentifier($property->getColumnName());
258
259
                    continue;
260 32
                }
261 32
262 32
                if ($property instanceof ToOneAssociationMetadata) {
263
                    foreach ($property->getJoinColumns() as $joinColumn) {
264
                        $pkColumns[] = $this->platform->quoteIdentifier($joinColumn->getColumnName());
265
                    }
266
                }
267 266
            }
268 248
269
            if (! $table->hasIndex('primary')) {
270
                $table->setPrimaryKey($pkColumns);
271
            }
272
273
            // there can be unique indexes automatically created for join column
274 266
            // 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 266
            $primaryKey = $table->getIndex('primary');
277 266
278 2
            foreach ($table->getIndexes() as $idxKey => $existingIndex) {
279
                if ($primaryKey->overrules($existingIndex)) {
280
                    $table->dropIndex($idxKey);
281
                }
282 266
            }
283 1
284 1
            if ($class->table->getIndexes()) {
285 1
                foreach ($class->table->getIndexes() as $indexName => $indexData) {
286
                    $indexName = is_numeric($indexName) ? null : $indexName;
287 1
                    $index     = new Index($indexName, $indexData['columns'], $indexData['unique'], $indexData['flags'], $indexData['options']);
288 1
289
                    foreach ($table->getIndexes() as $tableIndexName => $tableIndex) {
290
                        if ($tableIndex->isFullfilledBy($index)) {
291
                            $table->dropIndex($tableIndexName);
292
                            break;
293
                        }
294 1
                    }
295
296
                    if ($indexData['unique']) {
297 1
                        $table->addUniqueIndex($indexData['columns'], $indexName, $indexData['options']);
298
                    } else {
299
                        $table->addIndex($indexData['columns'], $indexName, $indexData['flags'], $indexData['options']);
300
                    }
301
                }
302 266
            }
303 4
304 4
            if ($class->table->getUniqueConstraints()) {
305 4
                foreach ($class->table->getUniqueConstraints() as $indexName => $indexData) {
306
                    $indexName = is_numeric($indexName) ? null : $indexName;
307 4
                    $uniqIndex = new Index($indexName, $indexData['columns'], true, false, $indexData['flags'], $indexData['options']);
308 4
309 3
                    foreach ($table->getIndexes() as $tableIndexName => $tableIndex) {
310 3
                        if ($tableIndex->isFullfilledBy($uniqIndex)) {
311
                            $table->dropIndex($tableIndexName);
312
                            break;
313
                        }
314 4
                    }
315
316
                    $table->addUniqueConstraint($indexData['columns'], $indexName, $indexData['flags'], $indexData['options']);
317
                }
318 266
            }
319 1
320 1
            if ($class->table->getOptions()) {
321
                foreach ($class->table->getOptions() as $key => $val) {
322
                    $table->addOption($key, $val);
323
                }
324 266
            }
325
326 266
            $processedClasses[$class->getClassName()] = true;
327 266
328 266
            foreach ($class->getPropertiesIterator() as $property) {
329 233
                if (! $property instanceof FieldMetadata
330 266
                    || ! $property->hasValueGenerator()
331 266
                    || $property->getValueGenerator()->getType() !== GeneratorType::SEQUENCE
332
                    || $class->getClassName() !== $class->getRootClassName()) {
333
                    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 266
            }
343 1
344 1
            if ($eventManager->hasListeners(ToolEvents::postGenerateSchemaTable)) {
345 1
                $eventManager->dispatchEvent(
346
                    ToolEvents::postGenerateSchemaTable,
347
                    new GenerateSchemaTableEventArgs($class, $schema, $table)
348
                );
349
            }
350 266
        }
351 9
352
        if (! $this->platform->supportsSchemas() && ! $this->platform->canEmulateSchemas()) {
353
            $schema->visit(new RemoveNamespacedAssets());
354 266
        }
355 1
356 1
        if ($eventManager->hasListeners(ToolEvents::postGenerateSchema)) {
357 1
            $eventManager->dispatchEvent(
358
                ToolEvents::postGenerateSchema,
359
                new GenerateSchemaEventArgs($this->em, $schema)
360
            );
361 266
        }
362
363
        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 75
     * @param ClassMetadata $class
371
     */
372 75
    private function addDiscriminatorColumnDefinition($class, Table $table)
373 75
    {
374
        $discrColumn     = $class->discriminatorColumn;
375 75
        $discrColumnType = $discrColumn->getTypeName();
376
        $options         = [
377
            'notnull' => ! $discrColumn->isNullable(),
378 75
        ];
379 75
380 68
        switch ($discrColumnType) {
381 68
            case 'string':
382
                $options['length'] = $discrColumn->getLength() ?? 255;
383 7
                break;
384
385
            case 'decimal':
386
                $options['scale']     = $discrColumn->getScale();
387
                $options['precision'] = $discrColumn->getPrecision();
388
                break;
389 75
        }
390
391
        if (! empty($discrColumn->getColumnDefinition())) {
392
            $options['columnDefinition'] = $discrColumn->getColumnDefinition();
393 75
        }
394 75
395
        $table->addColumn($discrColumn->getColumnName(), $discrColumnType, $options);
396
    }
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
     * @param ClassMetadata $class
403
     */
404 248
    private function gatherColumns($class, Table $table, ?string $columnPrefix = null)
405
    {
406 248
        $pkColumns = [];
407 248
408 183
        foreach ($class->getPropertiesIterator() as $fieldName => $property) {
409
            if ($class->inheritanceType === InheritanceType::SINGLE_TABLE && $class->isInheritedProperty($fieldName)) {
410
                continue;
411 248
            }
412 19
413
            switch (true) {
414
                case $property instanceof FieldMetadata:
415 248
                    $this->gatherColumn($class, $property, $table, $columnPrefix);
416
417 248
                    if ($property->isPrimaryKey()) {
418 247
                        $pkColumns[] = $this->platform->quoteIdentifier($property->getColumnName());
419
                    }
420
421 248
                    break;
422
423
                case $property instanceof EmbeddedMetadata:
424
                    $foreignClass = $this->em->getClassMetadata($property->getTargetEntity());
425
426
                    $this->gatherColumns($foreignClass, $table, $property->getColumnPrefix());
427
428
                    break;
429
            }
430
        }
431 266
    }
432
433 266
    /**
434 266
     * Creates a column definition as required by the DBAL from an ORM field mapping definition.
435 266
     *
436
     * @return Column The portable column definition as required by the DBAL.
437
     */
438 266
    private function gatherColumn(
439 266
        $classMetadata,
440
        FieldMetadata $fieldMetadata,
441 266
        Table $table,
442
        ?string $columnPrefix = null
443
    ) {
444
        $fieldName  = $fieldMetadata->getName();
445 266
        $columnName = sprintf('%s%s', $columnPrefix, $fieldMetadata->getColumnName());
446 7
        $columnType = $fieldMetadata->getTypeName();
447
448
        $options = [
449 266
            'length'          => $fieldMetadata->getLength(),
450
            'notnull'         => ! $fieldMetadata->isNullable(),
451
            'platformOptions' => [
452
                'version' => ($classMetadata->isVersioned() && $classMetadata->versionProperty->getName() === $fieldName),
453 266
            ],
454 266
        ];
455
456
        if ($classMetadata->inheritanceType === InheritanceType::SINGLE_TABLE && $classMetadata->getParent()) {
457 266
            $options['notnull'] = false;
458 266
        }
459
460
        if (strtolower($columnType) === 'string' && $options['length'] === null) {
461 266
            $options['length'] = 255;
462 1
        }
463
464
        if (is_int($fieldMetadata->getPrecision())) {
465 266
            $options['precision'] = $fieldMetadata->getPrecision();
466
        }
467
468 266
        if (is_int($fieldMetadata->getScale())) {
469
            $options['scale'] = $fieldMetadata->getScale();
470 266
        }
471 232
472
        if ($fieldMetadata->getColumnDefinition()) {
473
            $options['columnDefinition'] = $fieldMetadata->getColumnDefinition();
474 266
        }
475 58
476
        $fieldOptions = $fieldMetadata->getOptions();
477
478 266
        // the 'default' option can be overwritten here
479
        $options = $this->gatherColumnOptions($fieldOptions) + $options;
480 266
481
        if ($fieldMetadata->hasValueGenerator() && $fieldMetadata->getValueGenerator()->getType() === GeneratorType::IDENTITY && $classMetadata->getIdentifierFieldNames() === [$fieldName]) {
482
            $options['autoincrement'] = true;
483
        }
484
485
        if ($classMetadata->inheritanceType === InheritanceType::JOINED && ! $classMetadata->isRootEntity()) {
486 266
            $options['autoincrement'] = false;
487
        }
488
489 266
        $quotedColumnName = $this->platform->quoteIdentifier($columnName);
490 17
491
        if ($table->hasColumn($quotedColumnName)) {
492
            // required in some inheritance scenarios
493 266
            $table->changeColumn($quotedColumnName, $options);
494
495
            $column = $table->getColumn($quotedColumnName);
496
        } else {
497
            $column = $table->addColumn($quotedColumnName, $columnType, $options);
498
        }
499
500
        if ($fieldMetadata->isUnique()) {
501
            $table->addUniqueIndex([$columnName]);
502
        }
503
504
        return $column;
505
    }
506
507
    /**
508 266
     * Gathers the SQL for properly setting up the relations of the given class.
509
     * This includes the SQL for foreign key constraints and join tables.
510 266
     *
511 266
     * @param ClassMetadata $class
512 266
     * @param Table         $table
513
     * @param Schema        $schema
514
     * @param mixed[][]     $addedFks
515 188
     * @param bool[]        $blacklistedFks
516 21
     *
517
     * @throws ORMException
518
     */
519 188
    private function gatherRelationsSql($class, $table, $schema, &$addedFks, &$blacklistedFks)
520 134
    {
521
        foreach ($class->getPropertiesIterator() as $fieldName => $property) {
522
            if (! ($property instanceof AssociationMetadata)) {
523 188
                continue;
524
            }
525
526 188
            if ($class->isInheritedProperty($fieldName) && ! $property->getDeclaringClass()->isMappedSuperclass) {
527 171
                continue;
528
            }
529 171
530 171
            if (! $property->isOwningSide()) {
531 171
                continue;
532 171
            }
533 171
534 171
            $foreignClass = $this->em->getClassMetadata($property->getTargetEntity());
535 171
536 171
            switch (true) {
537
                case $property instanceof ToOneAssociationMetadata:
538
                    $primaryKeyColumns = []; // PK is unnecessary for this relation-type
539 171
540
                    $this->gatherRelationJoinColumns(
541 42
                        $property->getJoinColumns(),
542
                        $table,
543
                        $foreignClass,
544
                        $property,
545 42
                        $primaryKeyColumns,
546
                        $addedFks,
547 42
                        $blacklistedFks
548 42
                    );
549 42
550
                    break;
551 42
552
                case $property instanceof OneToManyAssociationMetadata:
553
                    //... create join table, one-many through join table supported later
554 42
                    throw NotSupported::create();
555 42
556 42
                case $property instanceof ManyToManyAssociationMetadata:
557 42
                    // create join table
558 42
                    $joinTable     = $property->getJoinTable();
559 42
                    $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
560 42
                    $theJoinTable  = $schema->createTable($joinTableName);
561 42
562
                    $primaryKeyColumns = [];
563
564
                    // Build first FK constraint (relation table => source table)
565 42
                    $this->gatherRelationJoinColumns(
566 42
                        $joinTable->getJoinColumns(),
567 42
                        $theJoinTable,
568 42
                        $class,
569 42
                        $property,
570 42
                        $primaryKeyColumns,
571 42
                        $addedFks,
572 42
                        $blacklistedFks
573
                    );
574
575 42
                    // Build second FK constraint (relation table => target table)
576
                    $this->gatherRelationJoinColumns(
577 42
                        $joinTable->getInverseJoinColumns(),
578
                        $theJoinTable,
579
                        $foreignClass,
580 266
                        $property,
581
                        $primaryKeyColumns,
582
                        $addedFks,
583
                        $blacklistedFks
584
                    );
585
586
                    $theJoinTable->setPrimaryKey($primaryKeyColumns);
587
588
                    break;
589
            }
590
        }
591
    }
592
593
    /**
594
     * Gets the class metadata that is responsible for the definition of the referenced column name.
595
     *
596 188
     * Previously this was a simple task, but with DDC-117 this problem is actually recursive. If its
597
     * not a simple field, go through all identifier field names that are associations recursively and
598 188
     * find that referenced column name.
599 188
     *
600
     * TODO: Is there any way to make this code more pleasing?
601 188
     *
602 188
     * @param ClassMetadata $class
603
     * @param string        $referencedColumnName
604
     *
605
     * @return mixed[] (ClassMetadata, referencedFieldName)
606 10
     */
607 10
    private function getDefiningClass($class, $referencedColumnName)
608
    {
609 10
        if (isset($class->fieldNames[$referencedColumnName])) {
610
            $propertyName = $class->fieldNames[$referencedColumnName];
611
612
            if ($class->hasField($propertyName)) {
613
                return [$class, $propertyName];
614 10
            }
615 10
        }
616
617 10
        $idColumns        = $class->getIdentifierColumns($this->em);
618 5
        $idColumnNameList = array_keys($idColumns);
619
620
        if (! in_array($referencedColumnName, $idColumnNameList, true)) {
621 10
            return null;
622
        }
623 10
624
        // it seems to be an entity as foreign key
625
        foreach ($class->getIdentifierFieldNames() as $fieldName) {
626
            $property = $class->getProperty($fieldName);
627 10
628
            if (! ($property instanceof AssociationMetadata)) {
629 10
                continue;
630 10
            }
631
632 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

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