Failed Conditions
Pull Request — master (#6743)
by Grégoire
14:09
created

SchemaTool::gatherRelationJoinColumns()   F

Complexity

Conditions 22
Paths 1011

Size

Total Lines 116
Code Lines 67

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 58
CRAP Score 23.1723

Importance

Changes 0
Metric Value
cc 22
eloc 67
nc 1011
nop 7
dl 0
loc 116
ccs 58
cts 67
cp 0.8657
crap 23.1723
rs 2
c 0
b 0
f 0

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\MissingColumnException;
29
use Doctrine\ORM\Tools\NotSupported;
30
use function array_diff;
31
use function array_key_exists;
32
use function array_keys;
33
use function count;
34
use function implode;
35
use function in_array;
36
use function is_int;
37
use function is_numeric;
38
use function reset;
39
use function sprintf;
40
use function strtolower;
41
42
/**
43
 * The SchemaTool is a tool to create/drop/update database schemas based on
44
 * <tt>ClassMetadata</tt> class descriptors.
45
 */
46
class SchemaTool
47
{
48
    /**
49
     * @var EntityManagerInterface
50
     */
51
    private $em;
52
53
    /**
54
     * @var AbstractPlatform
55
     */
56
    private $platform;
57
58
    /**
59
     * Initializes a new SchemaTool instance that uses the connection of the
60
     * provided EntityManager.
61
     */
62 1214
    public function __construct(EntityManagerInterface $em)
63
    {
64 1214
        $this->em       = $em;
65 1214
        $this->platform = $em->getConnection()->getDatabasePlatform();
66 1214
    }
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 247
    public function createSchema(array $classes)
76
    {
77 247
        $createSchemaSql = $this->getCreateSchemaSql($classes);
78 247
        $conn            = $this->em->getConnection();
79
80 247
        foreach ($createSchemaSql as $sql) {
81
            try {
82 247
                $conn->executeQuery($sql);
83 68
            } catch (\Throwable $e) {
84 247
                throw ToolsException::schemaToolFailure($sql, $e);
85
            }
86
        }
87 179
    }
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 247
    public function getCreateSchemaSql(array $classes)
98
    {
99 247
        $schema = $this->getSchemaFromMetadata($classes);
100
101 247
        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 256
    private function processingNotRequired($class, array $processedClasses)
113
    {
114 256
        return isset($processedClasses[$class->getClassName()]) ||
115 256
            $class->isMappedSuperclass ||
116 256
            $class->isEmbeddedClass ||
117 256
            ($class->inheritanceType === InheritanceType::SINGLE_TABLE && ! $class->isRootEntity())
118
        ;
119
    }
120
121
    /**
122
     * Creates a Schema instance from a given set of metadata classes.
123
     *
124
     * @param ClassMetadata[] $classes
125
     *
126
     * @return Schema
127
     *
128
     * @throws ORMException
129
     */
130 256
    public function getSchemaFromMetadata(array $classes)
131
    {
132
        // Reminder for processed classes, used for hierarchies
133 256
        $processedClasses     = [];
134 256
        $eventManager         = $this->em->getEventManager();
135 256
        $schemaManager        = $this->em->getConnection()->getSchemaManager();
136 256
        $metadataSchemaConfig = $schemaManager->createSchemaConfig();
137
138 256
        $metadataSchemaConfig->setExplicitForeignKeyIndexes(false);
139 256
        $schema = new Schema([], [], $metadataSchemaConfig);
140
141 256
        $addedFks       = [];
142 256
        $blacklistedFks = [];
143
144 256
        foreach ($classes as $class) {
145
            /** @var ClassMetadata $class */
146 256
            if ($this->processingNotRequired($class, $processedClasses)) {
147 19
                continue;
148
            }
149
150 256
            $table = $schema->createTable($class->table->getQuotedQualifiedName($this->platform));
151
152 256
            switch ($class->inheritanceType) {
153
                case InheritanceType::SINGLE_TABLE:
154 21
                    $this->gatherColumns($class, $table);
155 21
                    $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
156
157
                    // Add the discriminator column
158 21
                    $this->addDiscriminatorColumnDefinition($class, $table);
159
160
                    // Aggregate all the information from all classes in the hierarchy
161 21
                    $parentClass = $class;
162
163 21
                    while (($parentClass = $parentClass->getParent()) !== null) {
164
                        // Parent class information is already contained in this class
165
                        $processedClasses[$parentClass->getClassName()] = true;
166
                    }
167
168 21
                    foreach ($class->getSubClasses() as $subClassName) {
169 19
                        $subClass = $this->em->getClassMetadata($subClassName);
170
171 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

171
                        $this->gatherColumns(/** @scrutinizer ignore-type */ $subClass, $table);
Loading history...
172 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

172
                        $this->gatherRelationsSql(/** @scrutinizer ignore-type */ $subClass, $table, $schema, $addedFks, $blacklistedFks);
Loading history...
173
174 19
                        $processedClasses[$subClassName] = true;
175
                    }
176
177 21
                    break;
178
179
                case InheritanceType::JOINED:
180
                    // Add all non-inherited fields as columns
181 58
                    $pkColumns = [];
182
183 58
                    foreach ($class->getDeclaredPropertiesIterator() as $fieldName => $property) {
184 58
                        if (! ($property instanceof FieldMetadata)) {
185 16
                            continue;
186
                        }
187
188 58
                        if (! $class->isInheritedProperty($fieldName)) {
189 58
                            $columnName = $this->platform->quoteIdentifier($property->getColumnName());
190
191 58
                            $this->gatherColumn($class, $property, $table);
192
193 58
                            if ($class->isIdentifier($fieldName)) {
194 58
                                $pkColumns[] = $columnName;
195
                            }
196
                        }
197
                    }
198
199 58
                    $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
200
201
                    // Add the discriminator column only to the root table
202 58
                    if ($class->isRootEntity()) {
203 58
                        $this->addDiscriminatorColumnDefinition($class, $table);
204
                    } else {
205
                        // Add an ID FK column to child tables
206 57
                        $inheritedKeyColumns = [];
207
208 57
                        foreach ($class->identifier as $identifierField) {
209 57
                            $idProperty = $class->getProperty($identifierField);
210
211 57
                            if ($class->isInheritedProperty($identifierField)) {
212 57
                                $column     = $this->gatherColumn($class, $idProperty, $table);
213 57
                                $columnName = $column->getQuotedName($this->platform);
214
215
                                // TODO: This seems rather hackish, can we optimize it?
216 57
                                $column->setAutoincrement(false);
217
218 57
                                $pkColumns[]           = $columnName;
219 57
                                $inheritedKeyColumns[] = $columnName;
220
                            }
221
                        }
222
223 57
                        if (! empty($inheritedKeyColumns)) {
224
                            // Add a FK constraint on the ID column
225 57
                            $rootClass = $this->em->getClassMetadata($class->getRootClassName());
226
227 57
                            $table->addForeignKeyConstraint(
228 57
                                $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...
229 57
                                $inheritedKeyColumns,
230 57
                                $inheritedKeyColumns,
231 57
                                ['onDelete' => 'CASCADE']
232
                            );
233
                        }
234
                    }
235
236 58
                    $table->setPrimaryKey($pkColumns);
237
238 58
                    break;
239
240
                case InheritanceType::TABLE_PER_CLASS:
241
                    throw NotSupported::create();
242
243
                default:
244 231
                    $this->gatherColumns($class, $table);
245 231
                    $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
246
247 231
                    break;
248
            }
249
250 256
            $pkColumns = [];
251
252 256
            foreach ($class->identifier as $identifierField) {
253 256
                $property = $class->getProperty($identifierField);
254
255 256
                if ($property instanceof FieldMetadata) {
256 256
                    $pkColumns[] = $this->platform->quoteIdentifier($property->getColumnName());
257
258 256
                    continue;
259
                }
260
261 29
                if ($property instanceof ToOneAssociationMetadata) {
262 29
                    foreach ($property->getJoinColumns() as $joinColumn) {
263 29
                        $pkColumns[] = $this->platform->quoteIdentifier($joinColumn->getColumnName());
264
                    }
265
                }
266
            }
267
268 256
            if (! $table->hasIndex('primary')) {
269 238
                $table->setPrimaryKey($pkColumns);
270
            }
271
272
            // there can be unique indexes automatically created for join column
273
            // if join column is also primary key we should keep only primary key on this column
274
            // so, remove indexes overruled by primary key
275 256
            $primaryKey = $table->getIndex('primary');
276
277 256
            foreach ($table->getIndexes() as $idxKey => $existingIndex) {
278 256
                if ($primaryKey->overrules($existingIndex)) {
279 256
                    $table->dropIndex($idxKey);
280
                }
281
            }
282
283 256
            if ($class->table->getIndexes()) {
284 1
                foreach ($class->table->getIndexes() as $indexName => $indexData) {
285 1
                    $indexName = is_numeric($indexName) ? null : $indexName;
286 1
                    $index     = new Index($indexName, $indexData['columns'], $indexData['unique'], $indexData['flags'], $indexData['options']);
287
288 1
                    foreach ($table->getIndexes() as $tableIndexName => $tableIndex) {
289 1
                        if ($tableIndex->isFullfilledBy($index)) {
290
                            $table->dropIndex($tableIndexName);
291 1
                            break;
292
                        }
293
                    }
294
295 1
                    if ($indexData['unique']) {
296
                        $table->addUniqueIndex($indexData['columns'], $indexName, $indexData['options']);
297
                    } else {
298 1
                        $table->addIndex($indexData['columns'], $indexName, $indexData['flags'], $indexData['options']);
299
                    }
300
                }
301
            }
302
303 256
            if ($class->table->getUniqueConstraints()) {
304 4
                foreach ($class->table->getUniqueConstraints() as $indexName => $indexData) {
305 4
                    $indexName = is_numeric($indexName) ? null : $indexName;
306 4
                    $uniqIndex = new Index($indexName, $indexData['columns'], true, false, $indexData['flags'], $indexData['options']);
307
308 4
                    foreach ($table->getIndexes() as $tableIndexName => $tableIndex) {
309 4
                        if ($tableIndex->isFullfilledBy($uniqIndex)) {
310 3
                            $table->dropIndex($tableIndexName);
311 4
                            break;
312
                        }
313
                    }
314
315 4
                    $table->addUniqueConstraint($indexData['columns'], $indexName, $indexData['flags'], $indexData['options']);
316
                }
317
            }
318
319 256
            if ($class->table->getOptions()) {
320 1
                foreach ($class->table->getOptions() as $key => $val) {
321 1
                    $table->addOption($key, $val);
322
                }
323
            }
324
325 256
            $processedClasses[$class->getClassName()] = true;
326
327 256
            foreach ($class->getDeclaredPropertiesIterator() as $property) {
328 256
                if (! $property instanceof FieldMetadata
329 256
                    || ! $property->hasValueGenerator()
330 226
                    || $property->getValueGenerator()->getType() !== GeneratorType::SEQUENCE
331 256
                    || $class->getClassName() !== $class->getRootClassName()) {
332 256
                    continue;
333
                }
334
335
                $quotedName = $this->platform->quoteIdentifier($property->getValueGenerator()->getDefinition()['sequenceName']);
336
337
                if (! $schema->hasSequence($quotedName)) {
338
                    $schema->createSequence($quotedName, $property->getValueGenerator()->getDefinition()['allocationSize']);
339
                }
340
            }
341
342 256
            if ($eventManager->hasListeners(ToolEvents::postGenerateSchemaTable)) {
343 1
                $eventManager->dispatchEvent(
344 1
                    ToolEvents::postGenerateSchemaTable,
345 256
                    new GenerateSchemaTableEventArgs($class, $schema, $table)
346
                );
347
            }
348
        }
349
350 256
        if (! $this->platform->supportsSchemas() && ! $this->platform->canEmulateSchemas()) {
351 8
            $schema->visit(new RemoveNamespacedAssets());
352
        }
353
354 256
        if ($eventManager->hasListeners(ToolEvents::postGenerateSchema)) {
355 1
            $eventManager->dispatchEvent(
356 1
                ToolEvents::postGenerateSchema,
357 1
                new GenerateSchemaEventArgs($this->em, $schema)
358
            );
359
        }
360
361 256
        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 74
    private function addDiscriminatorColumnDefinition($class, Table $table)
371
    {
372 74
        $discrColumn     = $class->discriminatorColumn;
373 74
        $discrColumnType = $discrColumn->getTypeName();
374
        $options         = [
375 74
            'notnull' => ! $discrColumn->isNullable(),
376
        ];
377
378 74
        switch ($discrColumnType) {
379 74
            case 'string':
380 67
                $options['length'] = $discrColumn->getLength() ?? 255;
381 67
                break;
382
383 7
            case 'decimal':
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
384
                $options['scale']     = $discrColumn->getScale();
385
                $options['precision'] = $discrColumn->getPrecision();
386
                break;
387
        }
388
389 74
        if (! empty($discrColumn->getColumnDefinition())) {
390
            $options['columnDefinition'] = $discrColumn->getColumnDefinition();
391
        }
392
393 74
        $table->addColumn($discrColumn->getColumnName(), $discrColumnType, $options);
394 74
    }
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 238
    private function gatherColumns($class, Table $table)
403
    {
404 238
        $pkColumns = [];
405
406 238
        foreach ($class->getDeclaredPropertiesIterator() as $fieldName => $property) {
407 238
            if (! ($property instanceof FieldMetadata)) {
408 178
                continue;
409
            }
410
411 238
            if ($class->inheritanceType === InheritanceType::SINGLE_TABLE && $class->isInheritedProperty($fieldName)) {
412 19
                continue;
413
            }
414
415 238
            $this->gatherColumn($class, $property, $table);
416
417 238
            if ($property->isPrimaryKey()) {
418 238
                $pkColumns[] = $this->platform->quoteIdentifier($property->getColumnName());
419
            }
420
        }
421 238
    }
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 256
    private function gatherColumn($classMetadata, FieldMetadata $fieldMetadata, Table $table)
432
    {
433 256
        $fieldName  = $fieldMetadata->getName();
434 256
        $columnName = $fieldMetadata->getColumnName();
435 256
        $columnType = $fieldMetadata->getTypeName();
436
437
        $options = [
438 256
            'length'          => $fieldMetadata->getLength(),
439 256
            'notnull'         => ! $fieldMetadata->isNullable(),
440
            'platformOptions' => [
441 256
                'version' => ($classMetadata->isVersioned() && $classMetadata->versionProperty->getName() === $fieldName),
442
            ],
443
        ];
444
445 256
        if ($classMetadata->inheritanceType === InheritanceType::SINGLE_TABLE && $classMetadata->getParent()) {
446 7
            $options['notnull'] = false;
447
        }
448
449 256
        if (strtolower($columnType) === 'string' && $options['length'] === null) {
450
            $options['length'] = 255;
451
        }
452
453 256
        if (is_int($fieldMetadata->getPrecision())) {
454 256
            $options['precision'] = $fieldMetadata->getPrecision();
455
        }
456
457 256
        if (is_int($fieldMetadata->getScale())) {
458 256
            $options['scale'] = $fieldMetadata->getScale();
459
        }
460
461 256
        if ($fieldMetadata->getColumnDefinition()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fieldMetadata->getColumnDefinition() of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
462 1
            $options['columnDefinition'] = $fieldMetadata->getColumnDefinition();
463
        }
464
465 256
        $fieldOptions = $fieldMetadata->getOptions();
466
467 256
        if ($fieldOptions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fieldOptions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
468 33
            $knownOptions = ['comment', 'unsigned', 'fixed', 'default'];
469
470 33
            foreach ($knownOptions as $knownOption) {
471 33
                if (array_key_exists($knownOption, $fieldOptions)) {
472 32
                    $options[$knownOption] = $fieldOptions[$knownOption];
473
474 33
                    unset($fieldOptions[$knownOption]);
475
                }
476
            }
477
478 33
            $options['customSchemaOptions'] = $fieldOptions;
479
        }
480
481 256
        if ($fieldMetadata->hasValueGenerator() && $fieldMetadata->getValueGenerator()->getType() === GeneratorType::IDENTITY && $classMetadata->getIdentifierFieldNames() === [$fieldName]) {
482 225
            $options['autoincrement'] = true;
483
        }
484
485 256
        if ($classMetadata->inheritanceType === InheritanceType::JOINED && ! $classMetadata->isRootEntity()) {
486 57
            $options['autoincrement'] = false;
487
        }
488
489 256
        $quotedColumnName = $this->platform->quoteIdentifier($fieldMetadata->getColumnName());
490
491 256
        if ($table->hasColumn($quotedColumnName)) {
492
            // required in some inheritance scenarios
493
            $table->changeColumn($quotedColumnName, $options);
494
495
            $column = $table->getColumn($quotedColumnName);
496
        } else {
497 256
            $column = $table->addColumn($quotedColumnName, $columnType, $options);
498
        }
499
500 256
        if ($fieldMetadata->isUnique()) {
501 17
            $table->addUniqueIndex([$columnName]);
502
        }
503
504 256
        return $column;
505
    }
506
507
    /**
508
     * Gathers the SQL for properly setting up the relations of the given class.
509
     * This includes the SQL for foreign key constraints and join tables.
510
     *
511
     * @param ClassMetadata $class
512
     * @param Table         $table
513
     * @param Schema        $schema
514
     * @param mixed[][]     $addedFks
515
     * @param bool[]        $blacklistedFks
516
     *
517
     * @throws ORMException
518
     */
519 256
    private function gatherRelationsSql($class, $table, $schema, &$addedFks, &$blacklistedFks)
520
    {
521 256
        foreach ($class->getDeclaredPropertiesIterator() as $fieldName => $property) {
522 256
            if (! ($property instanceof AssociationMetadata)) {
523 256
                continue;
524
            }
525
526 183
            if ($class->isInheritedProperty($fieldName) && ! $property->getDeclaringClass()->isMappedSuperclass) {
527 21
                continue;
528
            }
529
530 183
            if (! $property->isOwningSide()) {
531 131
                continue;
532
            }
533
534 183
            $foreignClass = $this->em->getClassMetadata($property->getTargetEntity());
535
536
            switch (true) {
537 183
                case ($property instanceof ToOneAssociationMetadata):
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
538 166
                    $primaryKeyColumns = []; // PK is unnecessary for this relation-type
539
540 166
                    $this->gatherRelationJoinColumns(
541 166
                        $property->getJoinColumns(),
542 166
                        $table,
543 166
                        $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

543
                        /** @scrutinizer ignore-type */ $foreignClass,
Loading history...
544 166
                        $property,
545 166
                        $primaryKeyColumns,
546 166
                        $addedFks,
547 166
                        $blacklistedFks
548
                    );
549
550 166
                    break;
551
552 42
                case ($property instanceof OneToManyAssociationMetadata):
553
                    //... create join table, one-many through join table supported later
554
                    throw NotSupported::create();
555
556 42
                case ($property instanceof ManyToManyAssociationMetadata):
557
                    // create join table
558 42
                    $joinTable     = $property->getJoinTable();
559 42
                    $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
560 42
                    $theJoinTable  = $schema->createTable($joinTableName);
561
562 42
                    $primaryKeyColumns = [];
563
564
                    // Build first FK constraint (relation table => source table)
565 42
                    $this->gatherRelationJoinColumns(
566 42
                        $joinTable->getJoinColumns(),
567 42
                        $theJoinTable,
568 42
                        $class,
569 42
                        $property,
570 42
                        $primaryKeyColumns,
571 42
                        $addedFks,
572 42
                        $blacklistedFks
573
                    );
574
575
                    // Build second FK constraint (relation table => target table)
576 42
                    $this->gatherRelationJoinColumns(
577 42
                        $joinTable->getInverseJoinColumns(),
578 42
                        $theJoinTable,
579 42
                        $foreignClass,
580 42
                        $property,
581 42
                        $primaryKeyColumns,
582 42
                        $addedFks,
583 42
                        $blacklistedFks
584
                    );
585
586 42
                    $theJoinTable->setPrimaryKey($primaryKeyColumns);
587
588 183
                    break;
589
            }
590
        }
591 256
    }
592
593
    /**
594
     * Gets the class metadata that is responsible for the definition of the referenced column name.
595
     *
596
     * Previously this was a simple task, but with DDC-117 this problem is actually recursive. If its
597
     * not a simple field, go through all identifier field names that are associations recursively and
598
     * find that referenced column name.
599
     *
600
     * TODO: Is there any way to make this code more pleasing?
601
     *
602
     * @param ClassMetadata $class
603
     * @param string        $referencedColumnName
604
     *
605
     * @return mixed[] (ClassMetadata, referencedFieldName)
606
     */
607 183
    private function getDefiningClass($class, $referencedColumnName)
608
    {
609 183
        if (isset($class->fieldNames[$referencedColumnName])) {
610 183
            $propertyName = $class->fieldNames[$referencedColumnName];
611
612 183
            if ($class->hasField($propertyName)) {
613 183
                return [$class, $propertyName];
614
            }
615
        }
616
617 9
        $idColumns        = $class->getIdentifierColumns($this->em);
618 9
        $idColumnNameList = array_keys($idColumns);
619
620 9
        if (! in_array($referencedColumnName, $idColumnNameList, true)) {
621
            return null;
622
        }
623
624
        // it seems to be an entity as foreign key
625 9
        foreach ($class->getIdentifierFieldNames() as $fieldName) {
626 9
            $property = $class->getProperty($fieldName);
627
628 9
            if (! ($property instanceof AssociationMetadata)) {
629 5
                continue;
630
            }
631
632 9
            $joinColumns = $property->getJoinColumns();
0 ignored issues
show
Bug introduced by
The method getJoinColumns() does not exist on Doctrine\ORM\Mapping\Property. It seems like you code against a sub-type of Doctrine\ORM\Mapping\Property such as Doctrine\ORM\Mapping\ToOneAssociationMetadata. ( Ignorable by Annotation )

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

632
            /** @scrutinizer ignore-call */ 
633
            $joinColumns = $property->getJoinColumns();
Loading history...
Bug introduced by
The method getJoinColumns() does not exist on Doctrine\ORM\Mapping\AssociationMetadata. It seems like you code against a sub-type of Doctrine\ORM\Mapping\AssociationMetadata such as Doctrine\ORM\Mapping\ToOneAssociationMetadata. ( Ignorable by Annotation )

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

632
            /** @scrutinizer ignore-call */ 
633
            $joinColumns = $property->getJoinColumns();
Loading history...
633
634 9
            if (count($joinColumns) > 1) {
635
                throw MappingException::noSingleAssociationJoinColumnFound($class->getClassName(), $fieldName);
636
            }
637
638 9
            $joinColumn = reset($joinColumns);
639
640 9
            if ($joinColumn->getColumnName() === $referencedColumnName) {
641 9
                $targetEntity = $this->em->getClassMetadata($property->getTargetEntity());
642
643 9
                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

643
                return $this->getDefiningClass(/** @scrutinizer ignore-type */ $targetEntity, $joinColumn->getReferencedColumnName());
Loading history...
644
            }
645
        }
646
647
        return null;
648
    }
649
650
    /**
651
     * Gathers columns and fk constraints that are required for one part of relationship.
652
     *
653
     * @param JoinColumnMetadata[] $joinColumns
654
     * @param Table                $theJoinTable
655
     * @param ClassMetadata        $class
656
     * @param AssociationMetadata  $mapping
657
     * @param string[]             $primaryKeyColumns
658
     * @param mixed[][]            $addedFks
659
     * @param bool[]               $blacklistedFks
660
     *
661
     * @throws ORMException
662
     */
663 183
    private function gatherRelationJoinColumns(
664
        $joinColumns,
665
        $theJoinTable,
666
        $class,
667
        $mapping,
668
        &$primaryKeyColumns,
669
        &$addedFks,
670
        &$blacklistedFks
671
    ) {
672 183
        $localColumns      = [];
673 183
        $foreignColumns    = [];
674 183
        $fkOptions         = [];
675 183
        $foreignTableName  = $class->table->getQuotedQualifiedName($this->platform);
676 183
        $uniqueConstraints = [];
677
678 183
        foreach ($joinColumns as $joinColumn) {
679 183
            list($definingClass, $referencedFieldName) = $this->getDefiningClass(
680 183
                $class,
681 183
                $joinColumn->getReferencedColumnName()
682
            );
683
684 183
            if (! $definingClass) {
685
                throw new MissingColumnException(
686
                    $joinColumn->getReferencedColumnName(),
687
                    $mapping->getSourceEntity(),
0 ignored issues
show
Bug introduced by
$mapping->getSourceEntity() of type string is incompatible with the type integer expected by parameter $code of Doctrine\ORM\Tools\Missi...xception::__construct(). ( Ignorable by Annotation )

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

687
                    /** @scrutinizer ignore-type */ $mapping->getSourceEntity(),
Loading history...
688
                    $mapping->getTargetEntity()
0 ignored issues
show
Bug introduced by
$mapping->getTargetEntity() of type string is incompatible with the type Throwable|null expected by parameter $previous of Doctrine\ORM\Tools\Missi...xception::__construct(). ( Ignorable by Annotation )

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

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