SchemaTool::gatherColumn()   F
last analyzed

Complexity

Conditions 16
Paths 1024

Size

Total Lines 67
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 16.2322

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 16
eloc 33
c 1
b 0
f 0
nc 1024
nop 4
dl 0
loc 67
ccs 28
cts 31
cp 0.9032
crap 16.2322
rs 1.4

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM\Tools;
6
7
use Doctrine\DBAL\Platforms\AbstractPlatform;
8
use Doctrine\DBAL\Schema\Column;
9
use Doctrine\DBAL\Schema\Comparator;
10
use Doctrine\DBAL\Schema\Index;
11
use Doctrine\DBAL\Schema\Schema;
12
use Doctrine\DBAL\Schema\Table;
13
use Doctrine\DBAL\Schema\Visitor\DropSchemaSqlCollector;
14
use Doctrine\DBAL\Schema\Visitor\RemoveNamespacedAssets;
15
use Doctrine\ORM\EntityManagerInterface;
16
use Doctrine\ORM\Mapping\AssociationMetadata;
17
use Doctrine\ORM\Mapping\ClassMetadata;
18
use Doctrine\ORM\Mapping\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