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

SchemaTool::createSchema()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

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

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

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

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

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

628
            /** @scrutinizer ignore-call */ 
629
            $joinColumns = $property->getJoinColumns();
Loading history...
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

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

639
                return $this->getDefiningClass(/** @scrutinizer ignore-type */ $targetEntity, $joinColumn->getReferencedColumnName());
Loading history...
640
            }
641
        }
642
643
        return null;
644
    }
645
646
    /**
647
     * Gathers columns and fk constraints that are required for one part of relationship.
648
     *
649
     * @param JoinColumnMetadata[] $joinColumns
650
     * @param Table                $theJoinTable
651
     * @param ClassMetadata        $class
652
     * @param AssociationMetadata  $mapping
653
     * @param string[]             $primaryKeyColumns
654
     * @param mixed[][]            $addedFks
655
     * @param bool[]               $blacklistedFks
656
     *
657
     * @throws ORMException
658
     */
659 186
    private function gatherRelationJoinColumns(
660
        $joinColumns,
661
        $theJoinTable,
662
        $class,
663
        $mapping,
664
        &$primaryKeyColumns,
665
        &$addedFks,
666
        &$blacklistedFks
667
    ) {
668 186
        $localColumns      = [];
669 186
        $foreignColumns    = [];
670 186
        $fkOptions         = [];
671 186
        $foreignTableName  = $class->table->getQuotedQualifiedName($this->platform);
672 186
        $uniqueConstraints = [];
673
674 186
        foreach ($joinColumns as $joinColumn) {
675 186
            list($definingClass, $referencedFieldName) = $this->getDefiningClass(
676 186
                $class,
677 186
                $joinColumn->getReferencedColumnName()
678
            );
679
680 186
            if (! $definingClass) {
681
                throw new MissingColumnException(
682
                    $joinColumn->getReferencedColumnName(),
683
                    $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\Excep...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

683
                    /** @scrutinizer ignore-type */ $mapping->getSourceEntity(),
Loading history...
684
                    $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\Excep...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

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