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

SchemaTool::getDropSchemaSQL()   C

Complexity

Conditions 12
Paths 12

Size

Total Lines 44
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 22.4191

Importance

Changes 0
Metric Value
cc 12
eloc 24
nc 12
nop 1
dl 0
loc 44
ccs 14
cts 24
cp 0.5833
crap 22.4191
rs 5.1612
c 0
b 0
f 0

How to fix   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 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 strtolower;
40
41
/**
42
 * The SchemaTool is a tool to create/drop/update database schemas based on
43
 * <tt>ClassMetadata</tt> class descriptors.
44
 */
45
class SchemaTool
46
{
47
    /** @var EntityManagerInterface */
48
    private $em;
49
50
    /** @var AbstractPlatform */
51
    private $platform;
52
53
    /**
54
     * Initializes a new SchemaTool instance that uses the connection of the
55
     * provided EntityManager.
56
     */
57 1213
    public function __construct(EntityManagerInterface $em)
58
    {
59 1213
        $this->em       = $em;
60 1213
        $this->platform = $em->getConnection()->getDatabasePlatform();
61 1213
    }
62
63
    /**
64
     * Creates the database schema for the given array of ClassMetadata instances.
65
     *
66
     * @param ClassMetadata[] $classes
67
     *
68
     * @throws ToolsException
69
     */
70 251
    public function createSchema(array $classes)
71
    {
72 251
        $createSchemaSql = $this->getCreateSchemaSql($classes);
73 251
        $conn            = $this->em->getConnection();
74
75 251
        foreach ($createSchemaSql as $sql) {
76
            try {
77 251
                $conn->executeQuery($sql);
78 68
            } catch (\Throwable $e) {
79 251
                throw ToolsException::schemaToolFailure($sql, $e);
80
            }
81
        }
82 183
    }
83
84
    /**
85
     * Gets the list of DDL statements that are required to create the database schema for
86
     * the given list of ClassMetadata instances.
87
     *
88
     * @param ClassMetadata[] $classes
89
     *
90
     * @return string[] The SQL statements needed to create the schema for the classes.
91
     */
92 251
    public function getCreateSchemaSql(array $classes)
93
    {
94 251
        $schema = $this->getSchemaFromMetadata($classes);
95
96 251
        return $schema->toSql($this->platform);
97
    }
98
99
    /**
100
     * Detects instances of ClassMetadata that don't need to be processed in the SchemaTool context.
101
     *
102
     * @param ClassMetadata   $class
103
     * @param ClassMetadata[] $processedClasses
104
     *
105
     * @return bool
106
     */
107 260
    private function processingNotRequired($class, array $processedClasses)
108
    {
109 260
        return isset($processedClasses[$class->getClassName()]) ||
110 260
            $class->isMappedSuperclass ||
111 260
            $class->isEmbeddedClass ||
112 260
            ($class->inheritanceType === InheritanceType::SINGLE_TABLE && ! $class->isRootEntity())
113
        ;
114
    }
115
116
    /**
117
     * Creates a Schema instance from a given set of metadata classes.
118
     *
119
     * @param ClassMetadata[] $classes
120
     *
121
     * @return Schema
122
     *
123
     * @throws ORMException
124
     */
125 260
    public function getSchemaFromMetadata(array $classes)
126
    {
127
        // Reminder for processed classes, used for hierarchies
128 260
        $processedClasses     = [];
129 260
        $eventManager         = $this->em->getEventManager();
130 260
        $schemaManager        = $this->em->getConnection()->getSchemaManager();
131 260
        $metadataSchemaConfig = $schemaManager->createSchemaConfig();
132
133 260
        $metadataSchemaConfig->setExplicitForeignKeyIndexes(false);
134 260
        $schema = new Schema([], [], $metadataSchemaConfig);
135
136 260
        $addedFks       = [];
137 260
        $blacklistedFks = [];
138
139 260
        foreach ($classes as $class) {
140
            /** @var ClassMetadata $class */
141 260
            if ($this->processingNotRequired($class, $processedClasses)) {
142 19
                continue;
143
            }
144
145 260
            $table = $schema->createTable($class->table->getQuotedQualifiedName($this->platform));
146
147 260
            switch ($class->inheritanceType) {
148
                case InheritanceType::SINGLE_TABLE:
149 21
                    $this->gatherColumns($class, $table);
150 21
                    $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
151
152
                    // Add the discriminator column
153 21
                    $this->addDiscriminatorColumnDefinition($class, $table);
154
155
                    // Aggregate all the information from all classes in the hierarchy
156 21
                    $parentClass = $class;
157
158 21
                    while (($parentClass = $parentClass->getParent()) !== null) {
159
                        // Parent class information is already contained in this class
160
                        $processedClasses[$parentClass->getClassName()] = true;
161
                    }
162
163 21
                    foreach ($class->getSubClasses() as $subClassName) {
164 19
                        $subClass = $this->em->getClassMetadata($subClassName);
165
166 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

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

167
                        $this->gatherRelationsSql(/** @scrutinizer ignore-type */ $subClass, $table, $schema, $addedFks, $blacklistedFks);
Loading history...
168
169 19
                        $processedClasses[$subClassName] = true;
170
                    }
171
172 21
                    break;
173
174
                case InheritanceType::JOINED:
175
                    // Add all non-inherited fields as columns
176 59
                    $pkColumns = [];
177
178 59
                    foreach ($class->getDeclaredPropertiesIterator() as $fieldName => $property) {
179 59
                        if (! ($property instanceof FieldMetadata)) {
180 16
                            continue;
181
                        }
182
183 59
                        if (! $class->isInheritedProperty($fieldName)) {
184 59
                            $columnName = $this->platform->quoteIdentifier($property->getColumnName());
185
186 59
                            $this->gatherColumn($class, $property, $table);
187
188 59
                            if ($class->isIdentifier($fieldName)) {
189 59
                                $pkColumns[] = $columnName;
190
                            }
191
                        }
192
                    }
193
194 59
                    $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
195
196
                    // Add the discriminator column only to the root table
197 59
                    if ($class->isRootEntity()) {
198 59
                        $this->addDiscriminatorColumnDefinition($class, $table);
199
                    } else {
200
                        // Add an ID FK column to child tables
201 58
                        $inheritedKeyColumns = [];
202
203 58
                        foreach ($class->identifier as $identifierField) {
204 58
                            $idProperty = $class->getProperty($identifierField);
205
206 58
                            if ($class->isInheritedProperty($identifierField)) {
207 58
                                $column     = $this->gatherColumn($class, $idProperty, $table);
208 58
                                $columnName = $column->getQuotedName($this->platform);
209
210
                                // TODO: This seems rather hackish, can we optimize it?
211 58
                                $column->setAutoincrement(false);
212
213 58
                                $pkColumns[]           = $columnName;
214 58
                                $inheritedKeyColumns[] = $columnName;
215
                            }
216
                        }
217
218 58
                        if (! empty($inheritedKeyColumns)) {
219
                            // Add a FK constraint on the ID column
220 58
                            $rootClass = $this->em->getClassMetadata($class->getRootClassName());
221
222 58
                            $table->addForeignKeyConstraint(
223 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...
224 58
                                $inheritedKeyColumns,
225 58
                                $inheritedKeyColumns,
226 58
                                ['onDelete' => 'CASCADE']
227
                            );
228
                        }
229
                    }
230
231 59
                    $table->setPrimaryKey($pkColumns);
232
233 59
                    break;
234
235
                case InheritanceType::TABLE_PER_CLASS:
236
                    throw NotSupported::create();
237
238
                default:
239 234
                    $this->gatherColumns($class, $table);
240 234
                    $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
241
242 234
                    break;
243
            }
244
245 260
            $pkColumns = [];
246
247 260
            foreach ($class->identifier as $identifierField) {
248 260
                $property = $class->getProperty($identifierField);
249
250 260
                if ($property instanceof FieldMetadata) {
251 259
                    $pkColumns[] = $this->platform->quoteIdentifier($property->getColumnName());
252
253 259
                    continue;
254
                }
255
256 32
                if ($property instanceof ToOneAssociationMetadata) {
257 32
                    foreach ($property->getJoinColumns() as $joinColumn) {
258 32
                        $pkColumns[] = $this->platform->quoteIdentifier($joinColumn->getColumnName());
259
                    }
260
                }
261
            }
262
263 260
            if (! $table->hasIndex('primary')) {
264 241
                $table->setPrimaryKey($pkColumns);
265
            }
266
267
            // there can be unique indexes automatically created for join column
268
            // if join column is also primary key we should keep only primary key on this column
269
            // so, remove indexes overruled by primary key
270 260
            $primaryKey = $table->getIndex('primary');
271
272 260
            foreach ($table->getIndexes() as $idxKey => $existingIndex) {
273 260
                if ($primaryKey->overrules($existingIndex)) {
274 260
                    $table->dropIndex($idxKey);
275
                }
276
            }
277
278 260
            if ($class->table->getIndexes()) {
279 1
                foreach ($class->table->getIndexes() as $indexName => $indexData) {
280 1
                    $indexName = is_numeric($indexName) ? null : $indexName;
281 1
                    $index     = new Index($indexName, $indexData['columns'], $indexData['unique'], $indexData['flags'], $indexData['options']);
282
283 1
                    foreach ($table->getIndexes() as $tableIndexName => $tableIndex) {
284 1
                        if ($tableIndex->isFullfilledBy($index)) {
285
                            $table->dropIndex($tableIndexName);
286 1
                            break;
287
                        }
288
                    }
289
290 1
                    if ($indexData['unique']) {
291
                        $table->addUniqueIndex($indexData['columns'], $indexName, $indexData['options']);
292
                    } else {
293 1
                        $table->addIndex($indexData['columns'], $indexName, $indexData['flags'], $indexData['options']);
294
                    }
295
                }
296
            }
297
298 260
            if ($class->table->getUniqueConstraints()) {
299 4
                foreach ($class->table->getUniqueConstraints() as $indexName => $indexData) {
300 4
                    $indexName = is_numeric($indexName) ? null : $indexName;
301 4
                    $uniqIndex = new Index($indexName, $indexData['columns'], true, false, $indexData['flags'], $indexData['options']);
302
303 4
                    foreach ($table->getIndexes() as $tableIndexName => $tableIndex) {
304 4
                        if ($tableIndex->isFullfilledBy($uniqIndex)) {
305 3
                            $table->dropIndex($tableIndexName);
306 4
                            break;
307
                        }
308
                    }
309
310 4
                    $table->addUniqueConstraint($indexData['columns'], $indexName, $indexData['flags'], $indexData['options']);
311
                }
312
            }
313
314 260
            if ($class->table->getOptions()) {
315 1
                foreach ($class->table->getOptions() as $key => $val) {
316 1
                    $table->addOption($key, $val);
317
                }
318
            }
319
320 260
            $processedClasses[$class->getClassName()] = true;
321
322 260
            foreach ($class->getDeclaredPropertiesIterator() as $property) {
323 260
                if (! $property instanceof FieldMetadata
324 260
                    || ! $property->hasValueGenerator()
325 228
                    || $property->getValueGenerator()->getType() !== GeneratorType::SEQUENCE
326 260
                    || $class->getClassName() !== $class->getRootClassName()) {
327 260
                    continue;
328
                }
329
330
                $quotedName = $this->platform->quoteIdentifier($property->getValueGenerator()->getDefinition()['sequenceName']);
331
332
                if (! $schema->hasSequence($quotedName)) {
333
                    $schema->createSequence($quotedName, $property->getValueGenerator()->getDefinition()['allocationSize']);
334
                }
335
            }
336
337 260
            if ($eventManager->hasListeners(ToolEvents::postGenerateSchemaTable)) {
338 1
                $eventManager->dispatchEvent(
339 1
                    ToolEvents::postGenerateSchemaTable,
340 260
                    new GenerateSchemaTableEventArgs($class, $schema, $table)
341
                );
342
            }
343
        }
344
345 260
        if (! $this->platform->supportsSchemas() && ! $this->platform->canEmulateSchemas()) {
346 8
            $schema->visit(new RemoveNamespacedAssets());
347
        }
348
349 260
        if ($eventManager->hasListeners(ToolEvents::postGenerateSchema)) {
350 1
            $eventManager->dispatchEvent(
351 1
                ToolEvents::postGenerateSchema,
352 1
                new GenerateSchemaEventArgs($this->em, $schema)
353
            );
354
        }
355
356 260
        return $schema;
357
    }
358
359
    /**
360
     * Gets a portable column definition as required by the DBAL for the discriminator
361
     * column of a class.
362
     *
363
     * @param ClassMetadata $class
364
     */
365 75
    private function addDiscriminatorColumnDefinition($class, Table $table)
366
    {
367 75
        $discrColumn     = $class->discriminatorColumn;
368 75
        $discrColumnType = $discrColumn->getTypeName();
369
        $options         = [
370 75
            'notnull' => ! $discrColumn->isNullable(),
371
        ];
372
373 75
        switch ($discrColumnType) {
374 75
            case 'string':
375 68
                $options['length'] = $discrColumn->getLength() ?? 255;
376 68
                break;
377
378 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...
379
                $options['scale']     = $discrColumn->getScale();
380
                $options['precision'] = $discrColumn->getPrecision();
381
                break;
382
        }
383
384 75
        if (! empty($discrColumn->getColumnDefinition())) {
385
            $options['columnDefinition'] = $discrColumn->getColumnDefinition();
386
        }
387
388 75
        $table->addColumn($discrColumn->getColumnName(), $discrColumnType, $options);
389 75
    }
390
391
    /**
392
     * Gathers the column definitions as required by the DBAL of all field mappings
393
     * found in the given class.
394
     *
395
     * @param ClassMetadata $class
396
     */
397 241
    private function gatherColumns($class, Table $table)
398
    {
399 241
        $pkColumns = [];
400
401 241
        foreach ($class->getDeclaredPropertiesIterator() as $fieldName => $property) {
402 241
            if (! ($property instanceof FieldMetadata)) {
403 181
                continue;
404
            }
405
406 241
            if ($class->inheritanceType === InheritanceType::SINGLE_TABLE && $class->isInheritedProperty($fieldName)) {
407 19
                continue;
408
            }
409
410 241
            $this->gatherColumn($class, $property, $table);
411
412 241
            if ($property->isPrimaryKey()) {
413 241
                $pkColumns[] = $this->platform->quoteIdentifier($property->getColumnName());
414
            }
415
        }
416 241
    }
417
418
    /**
419
     * Creates a column definition as required by the DBAL from an ORM field mapping definition.
420
     *
421
     * @param ClassMetadata $classMetadata The class that owns the field mapping.
422
     * @param FieldMetadata $fieldMetadata The field mapping.
423
     *
424
     * @return Column The portable column definition as required by the DBAL.
425
     */
426 260
    private function gatherColumn($classMetadata, FieldMetadata $fieldMetadata, Table $table)
427
    {
428 260
        $fieldName  = $fieldMetadata->getName();
429 260
        $columnName = $fieldMetadata->getColumnName();
430 260
        $columnType = $fieldMetadata->getTypeName();
431
432
        $options = [
433 260
            'length'          => $fieldMetadata->getLength(),
434 260
            'notnull'         => ! $fieldMetadata->isNullable(),
435
            'platformOptions' => [
436 260
                'version' => ($classMetadata->isVersioned() && $classMetadata->versionProperty->getName() === $fieldName),
437
            ],
438
        ];
439
440 260
        if ($classMetadata->inheritanceType === InheritanceType::SINGLE_TABLE && $classMetadata->getParent()) {
441 7
            $options['notnull'] = false;
442
        }
443
444 260
        if (strtolower($columnType) === 'string' && $options['length'] === null) {
445
            $options['length'] = 255;
446
        }
447
448 260
        if (is_int($fieldMetadata->getPrecision())) {
449 260
            $options['precision'] = $fieldMetadata->getPrecision();
450
        }
451
452 260
        if (is_int($fieldMetadata->getScale())) {
453 260
            $options['scale'] = $fieldMetadata->getScale();
454
        }
455
456 260
        if ($fieldMetadata->getColumnDefinition()) {
457 1
            $options['columnDefinition'] = $fieldMetadata->getColumnDefinition();
458
        }
459
460 260
        $fieldOptions = $fieldMetadata->getOptions();
461
462 260
        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...
463 33
            $knownOptions = ['comment', 'unsigned', 'fixed', 'default'];
464
465 33
            foreach ($knownOptions as $knownOption) {
466 33
                if (array_key_exists($knownOption, $fieldOptions)) {
467 32
                    $options[$knownOption] = $fieldOptions[$knownOption];
468
469 33
                    unset($fieldOptions[$knownOption]);
470
                }
471
            }
472
473 33
            $options['customSchemaOptions'] = $fieldOptions;
474
        }
475
476 260
        if ($fieldMetadata->hasValueGenerator() && $fieldMetadata->getValueGenerator()->getType() === GeneratorType::IDENTITY && $classMetadata->getIdentifierFieldNames() === [$fieldName]) {
477 227
            $options['autoincrement'] = true;
478
        }
479
480 260
        if ($classMetadata->inheritanceType === InheritanceType::JOINED && ! $classMetadata->isRootEntity()) {
481 58
            $options['autoincrement'] = false;
482
        }
483
484 260
        $quotedColumnName = $this->platform->quoteIdentifier($fieldMetadata->getColumnName());
485
486 260
        if ($table->hasColumn($quotedColumnName)) {
487
            // required in some inheritance scenarios
488
            $table->changeColumn($quotedColumnName, $options);
489
490
            $column = $table->getColumn($quotedColumnName);
491
        } else {
492 260
            $column = $table->addColumn($quotedColumnName, $columnType, $options);
493
        }
494
495 260
        if ($fieldMetadata->isUnique()) {
496 17
            $table->addUniqueIndex([$columnName]);
497
        }
498
499 260
        return $column;
500
    }
501
502
    /**
503
     * Gathers the SQL for properly setting up the relations of the given class.
504
     * This includes the SQL for foreign key constraints and join tables.
505
     *
506
     * @param ClassMetadata $class
507
     * @param Table         $table
508
     * @param Schema        $schema
509
     * @param mixed[][]     $addedFks
510
     * @param bool[]        $blacklistedFks
511
     *
512
     * @throws ORMException
513
     */
514 260
    private function gatherRelationsSql($class, $table, $schema, &$addedFks, &$blacklistedFks)
515
    {
516 260
        foreach ($class->getDeclaredPropertiesIterator() as $fieldName => $property) {
517 260
            if (! ($property instanceof AssociationMetadata)) {
518 260
                continue;
519
            }
520
521 186
            if ($class->isInheritedProperty($fieldName) && ! $property->getDeclaringClass()->isMappedSuperclass) {
522 21
                continue;
523
            }
524
525 186
            if (! $property->isOwningSide()) {
526 133
                continue;
527
            }
528
529 186
            $foreignClass = $this->em->getClassMetadata($property->getTargetEntity());
530
531
            switch (true) {
532 186
                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...
533 169
                    $primaryKeyColumns = []; // PK is unnecessary for this relation-type
534
535 169
                    $this->gatherRelationJoinColumns(
536 169
                        $property->getJoinColumns(),
537 169
                        $table,
538 169
                        $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

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

627
            /** @scrutinizer ignore-call */ 
628
            $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

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

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