Failed Conditions
Push — master ( a3e53b...559253 )
by Guilherme
14:58
created

SchemaTool::gatherRelationsSql()   B

Complexity

Conditions 9
Paths 8

Size

Total Lines 70
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 9.0041

Importance

Changes 0
Metric Value
cc 9
eloc 45
c 0
b 0
f 0
nc 8
nop 5
dl 0
loc 70
ccs 26
cts 27
cp 0.963
crap 9.0041
rs 7.6444

How to fix   Long Method   

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
    {
0 ignored issues
show
Coding Style introduced by
The closing parenthesis and the opening brace of a multi-line function declaration must be on the same line
Loading history...
443 267
        $fieldName  = $fieldMetadata->getName();
444 267
        $columnName = sprintf('%s%s', $columnPrefix, $fieldMetadata->getColumnName());
445 267
        $columnType = $fieldMetadata->getTypeName();
446
447
        $options = [
448 267
            'length'          => $fieldMetadata->getLength(),
449 267
            'notnull'         => ! $fieldMetadata->isNullable(),
450
            'platformOptions' => [
451 267
                'version' => ($classMetadata->isVersioned() && $classMetadata->versionProperty->getName() === $fieldName),
452
            ],
453
        ];
454
455 267
        if ($classMetadata->inheritanceType === InheritanceType::SINGLE_TABLE && $classMetadata->getParent()) {
456 7
            $options['notnull'] = false;
457
        }
458
459 267
        if (strtolower($columnType) === 'string' && $options['length'] === null) {
460
            $options['length'] = 255;
461
        }
462
463 267
        if (is_int($fieldMetadata->getPrecision())) {
464 267
            $options['precision'] = $fieldMetadata->getPrecision();
465
        }
466
467 267
        if (is_int($fieldMetadata->getScale())) {
468 267
            $options['scale'] = $fieldMetadata->getScale();
469
        }
470
471 267
        if ($fieldMetadata->getColumnDefinition()) {
472 1
            $options['columnDefinition'] = $fieldMetadata->getColumnDefinition();
473
        }
474
475 267
        $fieldOptions = $fieldMetadata->getOptions();
476
477
        // the 'default' option can be overwritten here
478 267
        $options = $this->gatherColumnOptions($fieldOptions) + $options;
479
480 267
        if ($fieldMetadata->hasValueGenerator() && $fieldMetadata->getValueGenerator()->getType() === GeneratorType::IDENTITY && $classMetadata->getIdentifierFieldNames() === [$fieldName]) {
481 233
            $options['autoincrement'] = true;
482
        }
483
484 267
        if ($classMetadata->inheritanceType === InheritanceType::JOINED && ! $classMetadata->isRootEntity()) {
485 59
            $options['autoincrement'] = false;
486
        }
487
488 267
        $quotedColumnName = $this->platform->quoteIdentifier($columnName);
489
490 267
        if ($table->hasColumn($quotedColumnName)) {
491
            // required in some inheritance scenarios
492
            $table->changeColumn($quotedColumnName, $options);
493
494
            $column = $table->getColumn($quotedColumnName);
495
        } else {
496 267
            $column = $table->addColumn($quotedColumnName, $columnType, $options);
497
        }
498
499 267
        if ($fieldMetadata->isUnique()) {
500 17
            $table->addUniqueIndex([$columnName]);
501
        }
502
503 267
        return $column;
504
    }
505
506
    /**
507
     * Gathers the SQL for properly setting up the relations of the given class.
508
     * This includes the SQL for foreign key constraints and join tables.
509
     *
510
     * @param ClassMetadata $class
511
     * @param Table         $table
512
     * @param Schema        $schema
513
     * @param mixed[][]     $addedFks
514
     * @param bool[]        $blacklistedFks
515
     *
516
     * @throws ORMException
517
     */
518 267
    private function gatherRelationsSql($class, $table, $schema, &$addedFks, &$blacklistedFks)
519
    {
520 267
        foreach ($class->getPropertiesIterator() as $fieldName => $property) {
521 267
            if (! ($property instanceof AssociationMetadata)) {
522 267
                continue;
523
            }
524
525 188
            if ($class->isInheritedProperty($fieldName) && ! $property->getDeclaringClass()->isMappedSuperclass) {
526 21
                continue;
527
            }
528
529 188
            if (! $property->isOwningSide()) {
530 134
                continue;
531
            }
532
533 188
            $foreignClass = $this->em->getClassMetadata($property->getTargetEntity());
534
535
            switch (true) {
536 188
                case $property instanceof ToOneAssociationMetadata:
537 171
                    $primaryKeyColumns = []; // PK is unnecessary for this relation-type
538
539 171
                    $this->gatherRelationJoinColumns(
540 171
                        $property->getJoinColumns(),
541
                        $table,
542
                        $foreignClass,
543
                        $property,
544
                        $primaryKeyColumns,
545
                        $addedFks,
546
                        $blacklistedFks
547
                    );
548
549 171
                    break;
550
551 42
                case $property instanceof OneToManyAssociationMetadata:
552
                    //... create join table, one-many through join table supported later
553
                    throw NotSupported::create();
554
555 42
                case $property instanceof ManyToManyAssociationMetadata:
556
                    // create join table
557 42
                    $joinTable     = $property->getJoinTable();
558 42
                    $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
559 42
                    $theJoinTable  = $schema->createTable($joinTableName);
560
561 42
                    $primaryKeyColumns = [];
562
563
                    // Build first FK constraint (relation table => source table)
564 42
                    $this->gatherRelationJoinColumns(
565 42
                        $joinTable->getJoinColumns(),
566
                        $theJoinTable,
567
                        $class,
568
                        $property,
569
                        $primaryKeyColumns,
570
                        $addedFks,
571
                        $blacklistedFks
572
                    );
573
574
                    // Build second FK constraint (relation table => target table)
575 42
                    $this->gatherRelationJoinColumns(
576 42
                        $joinTable->getInverseJoinColumns(),
577
                        $theJoinTable,
578
                        $foreignClass,
579
                        $property,
580
                        $primaryKeyColumns,
581
                        $addedFks,
582
                        $blacklistedFks
583
                    );
584
585 42
                    $theJoinTable->setPrimaryKey($primaryKeyColumns);
586
587 42
                    break;
588
            }
589
        }
590 267
    }
591
592
    /**
593
     * Gets the class metadata that is responsible for the definition of the referenced column name.
594
     *
595
     * Previously this was a simple task, but with DDC-117 this problem is actually recursive. If its
596
     * not a simple field, go through all identifier field names that are associations recursively and
597
     * find that referenced column name.
598
     *
599
     * TODO: Is there any way to make this code more pleasing?
600
     *
601
     * @param ClassMetadata $class
602
     * @param string        $referencedColumnName
603
     *
604
     * @return mixed[] (ClassMetadata, referencedFieldName)
605
     */
606 188
    private function getDefiningClass($class, $referencedColumnName)
607
    {
608 188
        if (isset($class->fieldNames[$referencedColumnName])) {
609 188
            $propertyName = $class->fieldNames[$referencedColumnName];
610
611 188
            if ($class->hasField($propertyName)) {
612 188
                return [$class, $propertyName];
613
            }
614
        }
615
616 10
        $idColumns        = $class->getIdentifierColumns($this->em);
617 10
        $idColumnNameList = array_keys($idColumns);
618
619 10
        if (! in_array($referencedColumnName, $idColumnNameList, true)) {
620
            return null;
621
        }
622
623
        // it seems to be an entity as foreign key
624 10
        foreach ($class->getIdentifierFieldNames() as $fieldName) {
625 10
            $property = $class->getProperty($fieldName);
626
627 10
            if (! ($property instanceof AssociationMetadata)) {
628 5
                continue;
629
            }
630
631 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

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