Failed Conditions
Push — master ( fa7802...d60694 )
by Guilherme
09:27
created

SchemaTool::gatherRelationJoinColumns()   F

Complexity

Conditions 21
Paths 531

Size

Total Lines 114
Code Lines 66

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 58
CRAP Score 21.7851

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 21
eloc 66
c 1
b 0
f 0
nc 531
nop 7
dl 0
loc 114
ccs 58
cts 66
cp 0.8788
crap 21.7851
rs 0.6513

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\FieldMetadata;
19
use Doctrine\ORM\Mapping\GeneratorType;
20
use Doctrine\ORM\Mapping\InheritanceType;
21
use Doctrine\ORM\Mapping\JoinColumnMetadata;
22
use Doctrine\ORM\Mapping\ManyToManyAssociationMetadata;
23
use Doctrine\ORM\Mapping\MappingException;
24
use Doctrine\ORM\Mapping\OneToManyAssociationMetadata;
25
use Doctrine\ORM\Mapping\ToOneAssociationMetadata;
26
use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs;
27
use Doctrine\ORM\Tools\Event\GenerateSchemaTableEventArgs;
28
use Doctrine\ORM\Tools\Exception\MissingColumnException;
29
use Doctrine\ORM\Tools\Exception\NotSupported;
30
use Throwable;
31
use function array_diff;
32
use function array_diff_key;
33
use function array_flip;
34
use function array_intersect_key;
35
use function array_keys;
36
use function count;
37
use function implode;
38
use function in_array;
39
use function is_int;
40
use function is_numeric;
41
use function reset;
42
use function strtolower;
43
44
/**
45
 * The SchemaTool is a tool to create/drop/update database schemas based on
46
 * <tt>ClassMetadata</tt> class descriptors.
47
 */
48
class SchemaTool
49
{
50
    private const KNOWN_COLUMN_OPTIONS = ['comment', 'unsigned', 'fixed', 'default'];
51
52
    /** @var EntityManagerInterface */
53
    private $em;
54
55
    /** @var AbstractPlatform */
56
    private $platform;
57
58
    /**
59
     * Initializes a new SchemaTool instance that uses the connection of the
60
     * provided EntityManager.
61
     */
62 1230
    public function __construct(EntityManagerInterface $em)
63
    {
64 1230
        $this->em       = $em;
65 1230
        $this->platform = $em->getConnection()->getDatabasePlatform();
66 1230
    }
67
68
    /**
69
     * Creates the database schema for the given array of ClassMetadata instances.
70
     *
71
     * @param ClassMetadata[] $classes
72
     *
73
     * @throws ToolsException
74
     */
75 256
    public function createSchema(array $classes)
76
    {
77 256
        $createSchemaSql = $this->getCreateSchemaSql($classes);
78 256
        $conn            = $this->em->getConnection();
79
80 256
        foreach ($createSchemaSql as $sql) {
81
            try {
82 256
                $conn->executeQuery($sql);
83 69
            } catch (Throwable $e) {
84 69
                throw ToolsException::schemaToolFailure($sql, $e);
85
            }
86
        }
87 187
    }
88
89
    /**
90
     * Gets the list of DDL statements that are required to create the database schema for
91
     * the given list of ClassMetadata instances.
92
     *
93
     * @param ClassMetadata[] $classes
94
     *
95
     * @return string[] The SQL statements needed to create the schema for the classes.
96
     */
97 256
    public function getCreateSchemaSql(array $classes)
98
    {
99 256
        $schema = $this->getSchemaFromMetadata($classes);
100
101 256
        return $schema->toSql($this->platform);
102
    }
103
104
    /**
105
     * Detects instances of ClassMetadata that don't need to be processed in the SchemaTool context.
106
     *
107
     * @param ClassMetadata   $class
108
     * @param ClassMetadata[] $processedClasses
109
     *
110
     * @return bool
111
     */
112 266
    private function processingNotRequired($class, array $processedClasses)
113
    {
114 266
        return isset($processedClasses[$class->getClassName()]) ||
115 266
            $class->isMappedSuperclass ||
116 266
            $class->isEmbeddedClass ||
117 266
            ($class->inheritanceType === InheritanceType::SINGLE_TABLE && ! $class->isRootEntity());
118
    }
119
120
    /**
121
     * Creates a Schema instance from a given set of metadata classes.
122
     *
123
     * @param ClassMetadata[] $classes
124
     *
125
     * @return Schema
126
     *
127
     * @throws ORMException
128
     */
129 266
    public function getSchemaFromMetadata(array $classes)
130
    {
131
        // Reminder for processed classes, used for hierarchies
132 266
        $processedClasses     = [];
133 266
        $eventManager         = $this->em->getEventManager();
134 266
        $schemaManager        = $this->em->getConnection()->getSchemaManager();
135 266
        $metadataSchemaConfig = $schemaManager->createSchemaConfig();
136
137 266
        $metadataSchemaConfig->setExplicitForeignKeyIndexes(false);
138 266
        $schema = new Schema([], [], $metadataSchemaConfig);
139
140 266
        $addedFks       = [];
141 266
        $blacklistedFks = [];
142
143 266
        foreach ($classes as $class) {
144
            /** @var ClassMetadata $class */
145 266
            if ($this->processingNotRequired($class, $processedClasses)) {
146 19
                continue;
147
            }
148
149 266
            $table = $schema->createTable($class->table->getQuotedQualifiedName($this->platform));
150
151 266
            switch ($class->inheritanceType) {
152
                case InheritanceType::SINGLE_TABLE:
153 21
                    $this->gatherColumns($class, $table);
154 21
                    $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
155
156
                    // Add the discriminator column
157 21
                    $this->addDiscriminatorColumnDefinition($class, $table);
158
159
                    // Aggregate all the information from all classes in the hierarchy
160 21
                    $parentClass = $class;
161
162 21
                    while (($parentClass = $parentClass->getParent()) !== null) {
163
                        // Parent class information is already contained in this class
164
                        $processedClasses[$parentClass->getClassName()] = true;
165
                    }
166
167 21
                    foreach ($class->getSubClasses() as $subClassName) {
168 19
                        $subClass = $this->em->getClassMetadata($subClassName);
169
170 19
                        $this->gatherColumns($subClass, $table);
0 ignored issues
show
Bug introduced by
$subClass of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Tools\SchemaTool::gatherColumns(). ( Ignorable by Annotation )

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

170
                        $this->gatherColumns(/** @scrutinizer ignore-type */ $subClass, $table);
Loading history...
171 19
                        $this->gatherRelationsSql($subClass, $table, $schema, $addedFks, $blacklistedFks);
0 ignored issues
show
Bug introduced by
$subClass of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Tools\Schem...l::gatherRelationsSql(). ( Ignorable by Annotation )

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

171
                        $this->gatherRelationsSql(/** @scrutinizer ignore-type */ $subClass, $table, $schema, $addedFks, $blacklistedFks);
Loading history...
172
173 19
                        $processedClasses[$subClassName] = true;
174
                    }
175
176 21
                    break;
177
178
                case InheritanceType::JOINED:
179
                    // Add all non-inherited fields as columns
180 59
                    $pkColumns = [];
181
182 59
                    foreach ($class->getPropertiesIterator() as $fieldName => $property) {
183 59
                        if (! ($property instanceof FieldMetadata)) {
184 16
                            continue;
185
                        }
186
187 59
                        if (! $class->isInheritedProperty($fieldName)) {
188 59
                            $columnName = $this->platform->quoteIdentifier($property->getColumnName());
189
190 59
                            $this->gatherColumn($class, $property, $table);
191
192 59
                            if ($class->isIdentifier($fieldName)) {
193 59
                                $pkColumns[] = $columnName;
194
                            }
195
                        }
196
                    }
197
198 59
                    $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
199
200
                    // Add the discriminator column only to the root table
201 59
                    if ($class->isRootEntity()) {
202 59
                        $this->addDiscriminatorColumnDefinition($class, $table);
203
                    } else {
204
                        // Add an ID FK column to child tables
205 58
                        $inheritedKeyColumns = [];
206
207 58
                        foreach ($class->identifier as $identifierField) {
208 58
                            $idProperty = $class->getProperty($identifierField);
209
210 58
                            if ($class->isInheritedProperty($identifierField)) {
211 58
                                $column     = $this->gatherColumn($class, $idProperty, $table);
0 ignored issues
show
Bug introduced by
It seems like $idProperty can also be of type null; however, parameter $fieldMetadata of Doctrine\ORM\Tools\SchemaTool::gatherColumn() does only seem to accept Doctrine\ORM\Mapping\FieldMetadata, maybe add an additional type check? ( Ignorable by Annotation )

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

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

335
                /** @scrutinizer ignore-call */ 
336
                $quotedName = $generator->getSequenceName();
Loading history...
336
337
                if (! $schema->hasSequence($quotedName)) {
338
                    $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

338
                    $schema->createSequence($quotedName, $generator->/** @scrutinizer ignore-call */ getAllocationSize());
Loading history...
339
                }
340
            }
341
342 266
            if ($eventManager->hasListeners(ToolEvents::postGenerateSchemaTable)) {
343 1
                $eventManager->dispatchEvent(
344 1
                    ToolEvents::postGenerateSchemaTable,
345 1
                    new GenerateSchemaTableEventArgs($class, $schema, $table)
346
                );
347
            }
348
        }
349
350 266
        if (! $this->platform->supportsSchemas() && ! $this->platform->canEmulateSchemas()) {
351 9
            $schema->visit(new RemoveNamespacedAssets());
352
        }
353
354 266
        if ($eventManager->hasListeners(ToolEvents::postGenerateSchema)) {
355 1
            $eventManager->dispatchEvent(
356 1
                ToolEvents::postGenerateSchema,
357 1
                new GenerateSchemaEventArgs($this->em, $schema)
358
            );
359
        }
360
361 266
        return $schema;
362
    }
363
364
    /**
365
     * Gets a portable column definition as required by the DBAL for the discriminator
366
     * column of a class.
367
     *
368
     * @param ClassMetadata $class
369
     */
370 75
    private function addDiscriminatorColumnDefinition($class, Table $table)
371
    {
372 75
        $discrColumn     = $class->discriminatorColumn;
373 75
        $discrColumnType = $discrColumn->getTypeName();
374
        $options         = [
375 75
            'notnull' => ! $discrColumn->isNullable(),
376
        ];
377
378 75
        switch ($discrColumnType) {
379 75
            case 'string':
380 68
                $options['length'] = $discrColumn->getLength() ?? 255;
381 68
                break;
382
383 7
            case 'decimal':
384
                $options['scale']     = $discrColumn->getScale();
385
                $options['precision'] = $discrColumn->getPrecision();
386
                break;
387
        }
388
389 75
        if (! empty($discrColumn->getColumnDefinition())) {
390
            $options['columnDefinition'] = $discrColumn->getColumnDefinition();
391
        }
392
393 75
        $table->addColumn($discrColumn->getColumnName(), $discrColumnType, $options);
394 75
    }
395
396
    /**
397
     * Gathers the column definitions as required by the DBAL of all field mappings
398
     * found in the given class.
399
     *
400
     * @param ClassMetadata $class
401
     */
402 248
    private function gatherColumns($class, Table $table)
403
    {
404 248
        $pkColumns = [];
405
406 248
        foreach ($class->getPropertiesIterator() as $fieldName => $property) {
407 248
            if (! ($property instanceof FieldMetadata)) {
408 183
                continue;
409
            }
410
411 248
            if ($class->inheritanceType === InheritanceType::SINGLE_TABLE && $class->isInheritedProperty($fieldName)) {
412 19
                continue;
413
            }
414
415 248
            $this->gatherColumn($class, $property, $table);
416
417 248
            if ($property->isPrimaryKey()) {
418 247
                $pkColumns[] = $this->platform->quoteIdentifier($property->getColumnName());
419
            }
420
        }
421 248
    }
422
423
    /**
424
     * Creates a column definition as required by the DBAL from an ORM field mapping definition.
425
     *
426
     * @param ClassMetadata $classMetadata The class that owns the field mapping.
427
     * @param FieldMetadata $fieldMetadata The field mapping.
428
     *
429
     * @return Column The portable column definition as required by the DBAL.
430
     */
431 266
    private function gatherColumn($classMetadata, FieldMetadata $fieldMetadata, Table $table)
432
    {
433 266
        $fieldName  = $fieldMetadata->getName();
434 266
        $columnName = $fieldMetadata->getColumnName();
435 266
        $columnType = $fieldMetadata->getTypeName();
436
437
        $options = [
438 266
            'length'          => $fieldMetadata->getLength(),
439 266
            'notnull'         => ! $fieldMetadata->isNullable(),
440
            'platformOptions' => [
441 266
                'version' => ($classMetadata->isVersioned() && $classMetadata->versionProperty->getName() === $fieldName),
442
            ],
443
        ];
444
445 266
        if ($classMetadata->inheritanceType === InheritanceType::SINGLE_TABLE && $classMetadata->getParent()) {
446 7
            $options['notnull'] = false;
447
        }
448
449 266
        if (strtolower($columnType) === 'string' && $options['length'] === null) {
450
            $options['length'] = 255;
451
        }
452
453 266
        if (is_int($fieldMetadata->getPrecision())) {
454 266
            $options['precision'] = $fieldMetadata->getPrecision();
455
        }
456
457 266
        if (is_int($fieldMetadata->getScale())) {
458 266
            $options['scale'] = $fieldMetadata->getScale();
459
        }
460
461 266
        if ($fieldMetadata->getColumnDefinition()) {
462 1
            $options['columnDefinition'] = $fieldMetadata->getColumnDefinition();
463
        }
464
465 266
        $fieldOptions = $fieldMetadata->getOptions();
466
467
        // the 'default' option can be overwritten here
468 266
        $options = $this->gatherColumnOptions($fieldOptions) + $options;
469
470 266
        if ($fieldMetadata->hasValueGenerator() && $fieldMetadata->getValueGenerator()->getType() === GeneratorType::IDENTITY && $classMetadata->getIdentifierFieldNames() === [$fieldName]) {
471 232
            $options['autoincrement'] = true;
472
        }
473
474 266
        if ($classMetadata->inheritanceType === InheritanceType::JOINED && ! $classMetadata->isRootEntity()) {
475 58
            $options['autoincrement'] = false;
476
        }
477
478 266
        $quotedColumnName = $this->platform->quoteIdentifier($fieldMetadata->getColumnName());
479
480 266
        if ($table->hasColumn($quotedColumnName)) {
481
            // required in some inheritance scenarios
482
            $table->changeColumn($quotedColumnName, $options);
483
484
            $column = $table->getColumn($quotedColumnName);
485
        } else {
486 266
            $column = $table->addColumn($quotedColumnName, $columnType, $options);
487
        }
488
489 266
        if ($fieldMetadata->isUnique()) {
490 17
            $table->addUniqueIndex([$columnName]);
491
        }
492
493 266
        return $column;
494
    }
495
496
    /**
497
     * Gathers the SQL for properly setting up the relations of the given class.
498
     * This includes the SQL for foreign key constraints and join tables.
499
     *
500
     * @param ClassMetadata $class
501
     * @param Table         $table
502
     * @param Schema        $schema
503
     * @param mixed[][]     $addedFks
504
     * @param bool[]        $blacklistedFks
505
     *
506
     * @throws ORMException
507
     */
508 266
    private function gatherRelationsSql($class, $table, $schema, &$addedFks, &$blacklistedFks)
509
    {
510 266
        foreach ($class->getPropertiesIterator() as $fieldName => $property) {
511 266
            if (! ($property instanceof AssociationMetadata)) {
512 266
                continue;
513
            }
514
515 188
            if ($class->isInheritedProperty($fieldName) && ! $property->getDeclaringClass()->isMappedSuperclass) {
516 21
                continue;
517
            }
518
519 188
            if (! $property->isOwningSide()) {
520 134
                continue;
521
            }
522
523 188
            $foreignClass = $this->em->getClassMetadata($property->getTargetEntity());
524
525
            switch (true) {
526 188
                case $property instanceof ToOneAssociationMetadata:
527 171
                    $primaryKeyColumns = []; // PK is unnecessary for this relation-type
528
529 171
                    $this->gatherRelationJoinColumns(
530 171
                        $property->getJoinColumns(),
531 171
                        $table,
532 171
                        $foreignClass,
0 ignored issues
show
Bug introduced by
$foreignClass of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Tools\Schem...erRelationJoinColumns(). ( Ignorable by Annotation )

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

532
                        /** @scrutinizer ignore-type */ $foreignClass,
Loading history...
533 171
                        $property,
534 171
                        $primaryKeyColumns,
535 171
                        $addedFks,
536 171
                        $blacklistedFks
537
                    );
538
539 171
                    break;
540
541 42
                case $property instanceof OneToManyAssociationMetadata:
542
                    //... create join table, one-many through join table supported later
543
                    throw NotSupported::create();
544
545 42
                case $property instanceof ManyToManyAssociationMetadata:
546
                    // create join table
547 42
                    $joinTable     = $property->getJoinTable();
548 42
                    $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
549 42
                    $theJoinTable  = $schema->createTable($joinTableName);
550
551 42
                    $primaryKeyColumns = [];
552
553
                    // Build first FK constraint (relation table => source table)
554 42
                    $this->gatherRelationJoinColumns(
555 42
                        $joinTable->getJoinColumns(),
556 42
                        $theJoinTable,
557 42
                        $class,
558 42
                        $property,
559 42
                        $primaryKeyColumns,
560 42
                        $addedFks,
561 42
                        $blacklistedFks
562
                    );
563
564
                    // Build second FK constraint (relation table => target table)
565 42
                    $this->gatherRelationJoinColumns(
566 42
                        $joinTable->getInverseJoinColumns(),
567 42
                        $theJoinTable,
568 42
                        $foreignClass,
569 42
                        $property,
570 42
                        $primaryKeyColumns,
571 42
                        $addedFks,
572 42
                        $blacklistedFks
573
                    );
574
575 42
                    $theJoinTable->setPrimaryKey($primaryKeyColumns);
576
577 42
                    break;
578
            }
579
        }
580 266
    }
581
582
    /**
583
     * Gets the class metadata that is responsible for the definition of the referenced column name.
584
     *
585
     * Previously this was a simple task, but with DDC-117 this problem is actually recursive. If its
586
     * not a simple field, go through all identifier field names that are associations recursively and
587
     * find that referenced column name.
588
     *
589
     * TODO: Is there any way to make this code more pleasing?
590
     *
591
     * @param ClassMetadata $class
592
     * @param string        $referencedColumnName
593
     *
594
     * @return mixed[] (ClassMetadata, referencedFieldName)
595
     */
596 188
    private function getDefiningClass($class, $referencedColumnName)
597
    {
598 188
        if (isset($class->fieldNames[$referencedColumnName])) {
599 188
            $propertyName = $class->fieldNames[$referencedColumnName];
600
601 188
            if ($class->hasField($propertyName)) {
602 188
                return [$class, $propertyName];
603
            }
604
        }
605
606 10
        $idColumns        = $class->getIdentifierColumns($this->em);
607 10
        $idColumnNameList = array_keys($idColumns);
608
609 10
        if (! in_array($referencedColumnName, $idColumnNameList, true)) {
610
            return null;
611
        }
612
613
        // it seems to be an entity as foreign key
614 10
        foreach ($class->getIdentifierFieldNames() as $fieldName) {
615 10
            $property = $class->getProperty($fieldName);
616
617 10
            if (! ($property instanceof AssociationMetadata)) {
618 5
                continue;
619
            }
620
621 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

621
            /** @scrutinizer ignore-call */ 
622
            $joinColumns = $property->getJoinColumns();
Loading history...
622
623 10
            if (count($joinColumns) > 1) {
624
                throw MappingException::noSingleAssociationJoinColumnFound($class->getClassName(), $fieldName);
625
            }
626
627 10
            $joinColumn = reset($joinColumns);
628
629 10
            if ($joinColumn->getColumnName() === $referencedColumnName) {
630 10
                $targetEntity = $this->em->getClassMetadata($property->getTargetEntity());
631
632 10
                return $this->getDefiningClass($targetEntity, $joinColumn->getReferencedColumnName());
0 ignored issues
show
Bug introduced by
$targetEntity of type Doctrine\Common\Persistence\Mapping\ClassMetadata is incompatible with the type Doctrine\ORM\Mapping\ClassMetadata expected by parameter $class of Doctrine\ORM\Tools\SchemaTool::getDefiningClass(). ( Ignorable by Annotation )

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

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