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

SchemaTool::getUpdateSchemaSql()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2.0054

Importance

Changes 0
Metric Value
cc 2
eloc 8
nc 2
nop 2
dl 0
loc 15
ccs 8
cts 9
cp 0.8889
crap 2.0054
rs 9.4285
c 0
b 0
f 0
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 1210
    public function __construct(EntityManagerInterface $em)
58
    {
59 1210
        $this->em       = $em;
60 1210
        $this->platform = $em->getConnection()->getDatabasePlatform();
61 1210
    }
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':
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):
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