Failed Conditions
Pull Request — master (#6743)
by Grégoire
18:17 queued 12:33
created

SchemaTool::getDropSchemaSQL()   C

Complexity

Conditions 12
Paths 12

Size

Total Lines 44
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 23.8367

Importance

Changes 0
Metric Value
cc 12
eloc 24
nc 12
nop 1
dl 0
loc 44
rs 5.1612
c 0
b 0
f 0
ccs 13
cts 23
cp 0.5652
crap 23.8367

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\Schema\Column;
8
use Doctrine\DBAL\Schema\Comparator;
9
use Doctrine\DBAL\Schema\Index;
10
use Doctrine\DBAL\Schema\Schema;
11
use Doctrine\DBAL\Schema\Table;
12
use Doctrine\DBAL\Schema\Visitor\DropSchemaSqlCollector;
13
use Doctrine\DBAL\Schema\Visitor\RemoveNamespacedAssets;
14
use Doctrine\ORM\EntityManagerInterface;
15
use Doctrine\ORM\Mapping\AssociationMetadata;
16
use Doctrine\ORM\Mapping\ClassMetadata;
17
use Doctrine\ORM\Mapping\FieldMetadata;
18
use Doctrine\ORM\Mapping\GeneratorType;
19
use Doctrine\ORM\Mapping\InheritanceType;
20
use Doctrine\ORM\Mapping\JoinColumnMetadata;
21
use Doctrine\ORM\Mapping\ManyToManyAssociationMetadata;
22
use Doctrine\ORM\Mapping\MappingException;
23
use Doctrine\ORM\Mapping\OneToManyAssociationMetadata;
24
use Doctrine\ORM\Mapping\ToOneAssociationMetadata;
25
use Doctrine\ORM\ORMException;
26
use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs;
27
use Doctrine\ORM\Tools\Event\GenerateSchemaTableEventArgs;
28
use Doctrine\ORM\Tools\NotSupported;
29
30
/**
31
 * The SchemaTool is a tool to create/drop/update database schemas based on
32
 * <tt>ClassMetadata</tt> class descriptors.
33
 */
34
class SchemaTool
35
{
36
    /**
37
     * @var \Doctrine\ORM\EntityManagerInterface
38
     */
39
    private $em;
40
41
    /**
42
     * @var \Doctrine\DBAL\Platforms\AbstractPlatform
43
     */
44
    private $platform;
45
46
    /**
47
     * Initializes a new SchemaTool instance that uses the connection of the
48
     * provided EntityManager.
49 1107
     */
50
    public function __construct(EntityManagerInterface $em)
51 1107
    {
52 1107
        $this->em       = $em;
53 1107
        $this->platform = $em->getConnection()->getDatabasePlatform();
54
    }
55
56
    /**
57
     * Creates the database schema for the given array of ClassMetadata instances.
58
     *
59
     * @param ClassMetadata[] $classes
60
     *
61
     * @throws ToolsException
62 182
     */
63
    public function createSchema(array $classes)
64 182
    {
65 182
        $createSchemaSql = $this->getCreateSchemaSql($classes);
66
        $conn            = $this->em->getConnection();
67 182
68
        foreach ($createSchemaSql as $sql) {
69 182
            try {
70
                $conn->executeQuery($sql);
71 182
            } catch (\Throwable $e) {
72
                throw ToolsException::schemaToolFailure($sql, $e);
73
            }
74 182
        }
75
    }
76
77
    /**
78
     * Gets the list of DDL statements that are required to create the database schema for
79
     * the given list of ClassMetadata instances.
80
     *
81
     * @param ClassMetadata[] $classes
82
     *
83
     * @return string[] The SQL statements needed to create the schema for the classes.
84 182
     */
85
    public function getCreateSchemaSql(array $classes)
86 182
    {
87
        $schema = $this->getSchemaFromMetadata($classes);
88 182
89
        return $schema->toSql($this->platform);
90
    }
91
92
    /**
93
     * Detects instances of ClassMetadata that don't need to be processed in the SchemaTool context.
94
     *
95
     * @param ClassMetadata   $class
96
     * @param ClassMetadata[] $processedClasses
97
     *
98
     * @return bool
99 191
     */
100
    private function processingNotRequired($class, array $processedClasses)
101 191
    {
102 191
        return isset($processedClasses[$class->getClassName()]) ||
103 191
            $class->isMappedSuperclass ||
104 191
            $class->isEmbeddedClass ||
105
            ($class->inheritanceType === InheritanceType::SINGLE_TABLE && ! $class->isRootEntity())
106
        ;
107
    }
108
109
    /**
110
     * Creates a Schema instance from a given set of metadata classes.
111
     *
112
     * @param ClassMetadata[] $classes
113
     *
114
     * @return Schema
115
     *
116
     * @throws \Doctrine\ORM\ORMException
117 191
     */
118
    public function getSchemaFromMetadata(array $classes)
119
    {
120 191
        // Reminder for processed classes, used for hierarchies
121 191
        $processedClasses     = [];
122 191
        $eventManager         = $this->em->getEventManager();
123 191
        $schemaManager        = $this->em->getConnection()->getSchemaManager();
124
        $metadataSchemaConfig = $schemaManager->createSchemaConfig();
125 191
126 191
        $metadataSchemaConfig->setExplicitForeignKeyIndexes(false);
127
        $schema = new Schema([], [], $metadataSchemaConfig);
128 191
129 191
        $addedFks       = [];
130
        $blacklistedFks = [];
131 191
132
        foreach ($classes as $class) {
133 191
            /** @var \Doctrine\ORM\Mapping\ClassMetadata $class */
134 18
            if ($this->processingNotRequired($class, $processedClasses)) {
135
                continue;
136
            }
137 191
138
            $table = $schema->createTable($class->table->getQuotedQualifiedName($this->platform));
139 191
140
            switch ($class->inheritanceType) {
141 20
                case InheritanceType::SINGLE_TABLE:
142 20
                    $this->gatherColumns($class, $table);
143
                    $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
144
145 20
                    // Add the discriminator column
146
                    $this->addDiscriminatorColumnDefinition($class, $table);
147
148 20
                    // Aggregate all the information from all classes in the hierarchy
149
                    $parentClass = $class;
150 20
151
                    while (($parentClass = $parentClass->getParent()) !== null) {
152
                        // Parent class information is already contained in this class
153
                        $processedClasses[$parentClass->getClassName()] = true;
154
                    }
155 20
156 18
                    foreach ($class->getSubClasses() as $subClassName) {
157
                        $subClass = $this->em->getClassMetadata($subClassName);
158 18
159 18
                        $this->gatherColumns($subClass, $table);
160
                        $this->gatherRelationsSql($subClass, $table, $schema, $addedFks, $blacklistedFks);
161 18
162
                        $processedClasses[$subClassName] = true;
163
                    }
164 20
165
                    break;
166
167
                case InheritanceType::JOINED:
168 45
                    // Add all non-inherited fields as columns
169
                    $pkColumns = [];
170 45
171 45
                    foreach ($class->getDeclaredPropertiesIterator() as $fieldName => $property) {
172 15
                        if (! ($property instanceof FieldMetadata)) {
173
                            continue;
174
                        }
175 45
176 45
                        if (! $class->isInheritedProperty($fieldName)) {
177
                            $columnName = $this->platform->quoteIdentifier($property->getColumnName());
178 45
179
                            $this->gatherColumn($class, $property, $table);
180 45
181 45
                            if ($class->isIdentifier($fieldName)) {
182
                                $pkColumns[] = $columnName;
183
                            }
184
                        }
185
                    }
186 45
187
                    $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
188
189 45
                    // Add the discriminator column only to the root table
190 45
                    if ($class->isRootEntity()) {
191
                        $this->addDiscriminatorColumnDefinition($class, $table);
192
                    } else {
193 44
                        // Add an ID FK column to child tables
194
                        $inheritedKeyColumns = [];
195 44
196 44
                        foreach ($class->identifier as $identifierField) {
197
                            $idProperty = $class->getProperty($identifierField);
198 44
199 44
                            if ($class->isInheritedProperty($identifierField)) {
200 44
                                $column     = $this->gatherColumn($class, $idProperty, $table);
201
                                $columnName = $column->getQuotedName($this->platform);
202
203 44
                                // TODO: This seems rather hackish, can we optimize it?
204
                                $column->setAutoincrement(false);
205 44
206 44
                                $pkColumns[]           = $columnName;
207
                                $inheritedKeyColumns[] = $columnName;
208
                            }
209
                        }
210 44
211
                        if (! empty($inheritedKeyColumns)) {
212 44
                            // Add a FK constraint on the ID column
213
                            $rootClass = $this->em->getClassMetadata($class->getRootClassName());
214 44
215 44
                            $table->addForeignKeyConstraint(
216 44
                                $rootClass->table->getQuotedQualifiedName($this->platform),
217 44
                                $inheritedKeyColumns,
218 44
                                $inheritedKeyColumns,
219
                                ['onDelete' => 'CASCADE']
220
                            );
221
                        }
222
                    }
223 45
224
                    $table->setPrimaryKey($pkColumns);
225 45
226
                    break;
227
228
                case InheritanceType::TABLE_PER_CLASS:
229
                    throw NotSupported::create();
230
231 166
                default:
232 166
                    $this->gatherColumns($class, $table);
233
                    $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
234 166
235
                    break;
236
            }
237 191
238
            $pkColumns = [];
239 191
240 191
            foreach ($class->identifier as $identifierField) {
241
                $property = $class->getProperty($identifierField);
242 191
243 191
                if ($property instanceof FieldMetadata) {
244
                    $pkColumns[] = $this->platform->quoteIdentifier($property->getColumnName());
245 191
246
                    continue;
247
                }
248 26
249 26
                if ($property instanceof ToOneAssociationMetadata) {
250 26
                    foreach ($property->getJoinColumns() as $joinColumn) {
251
                        $pkColumns[] = $this->platform->quoteIdentifier($joinColumn->getColumnName());
252
                    }
253
                }
254
            }
255 191
256 173
            if (! $table->hasIndex('primary')) {
257
                $table->setPrimaryKey($pkColumns);
258
            }
259
260
            // there can be unique indexes automatically created for join column
261
            // if join column is also primary key we should keep only primary key on this column
262 191
            // so, remove indexes overruled by primary key
263
            $primaryKey = $table->getIndex('primary');
264 191
265 191
            foreach ($table->getIndexes() as $idxKey => $existingIndex) {
266 191
                if ($primaryKey->overrules($existingIndex)) {
267
                    $table->dropIndex($idxKey);
268
                }
269
            }
270 191
271 1
            if ($class->table->getIndexes()) {
272 1
                foreach ($class->table->getIndexes() as $indexName => $indexData) {
273 1
                    $indexName = is_numeric($indexName) ? null : $indexName;
274
                    $index     = new Index($indexName, $indexData['columns'], $indexData['unique'], $indexData['flags'], $indexData['options']);
275 1
276 1
                    foreach ($table->getIndexes() as $tableIndexName => $tableIndex) {
277
                        if ($tableIndex->isFullfilledBy($index)) {
278 1
                            $table->dropIndex($tableIndexName);
279
                            break;
280
                        }
281
                    }
282 1
283
                    if ($indexData['unique']) {
284
                        $table->addUniqueIndex($indexData['columns'], $indexName, $indexData['options']);
285 1
                    } else {
286
                        $table->addIndex($indexData['columns'], $indexName, $indexData['flags'], $indexData['options']);
287
                    }
288
                }
289
            }
290 191
291 4
            if ($class->table->getUniqueConstraints()) {
292 4
                foreach ($class->table->getUniqueConstraints() as $indexName => $indexData) {
293 4
                    $indexName = is_numeric($indexName) ? null : $indexName;
294
                    $uniqIndex = new Index($indexName, $indexData['columns'], true, false, $indexData['flags'], $indexData['options']);
295 4
296 4
                    foreach ($table->getIndexes() as $tableIndexName => $tableIndex) {
297 3
                        if ($tableIndex->isFullfilledBy($uniqIndex)) {
298 4
                            $table->dropIndex($tableIndexName);
299
                            break;
300
                        }
301
                    }
302 4
303
                    $table->addUniqueConstraint($indexData['columns'], $indexName, $indexData['flags'], $indexData['options']);
304
                }
305
            }
306 191
307 1
            if ($class->table->getOptions()) {
308 1
                foreach ($class->table->getOptions() as $key => $val) {
309
                    $table->addOption($key, $val);
310
                }
311
            }
312 191
313
            $processedClasses[$class->getClassName()] = true;
314 191
315 191
            foreach ($class->getDeclaredPropertiesIterator() as $property) {
316 191
                if (! $property instanceof FieldMetadata
317 159
                    || ! $property->hasValueGenerator()
318 191
                    || $property->getValueGenerator()->getType() !== GeneratorType::SEQUENCE
319 191
                    || $class->getClassName() !== $class->getRootClassName()) {
320
                    continue;
321
                }
322
323
                $quotedName = $this->platform->quoteIdentifier($property->getValueGenerator()->getDefinition()['sequenceName']);
324
325
                if (! $schema->hasSequence($quotedName)) {
326
                    $schema->createSequence($quotedName, $property->getValueGenerator()->getDefinition()['allocationSize']);
327
                }
328
            }
329 191
330 1
            if ($eventManager->hasListeners(ToolEvents::postGenerateSchemaTable)) {
331 1
                $eventManager->dispatchEvent(
332 191
                    ToolEvents::postGenerateSchemaTable,
333
                    new GenerateSchemaTableEventArgs($class, $schema, $table)
334
                );
335
            }
336
        }
337 191
338 8
        if (! $this->platform->supportsSchemas() && ! $this->platform->canEmulateSchemas()) {
339
            $schema->visit(new RemoveNamespacedAssets());
340
        }
341 191
342 1
        if ($eventManager->hasListeners(ToolEvents::postGenerateSchema)) {
343 1
            $eventManager->dispatchEvent(
344 1
                ToolEvents::postGenerateSchema,
345
                new GenerateSchemaEventArgs($this->em, $schema)
346
            );
347
        }
348 191
349
        return $schema;
350
    }
351
352
    /**
353
     * Gets a portable column definition as required by the DBAL for the discriminator
354
     * column of a class.
355
     *
356
     * @param ClassMetadata $class
357 60
     */
358
    private function addDiscriminatorColumnDefinition($class, Table $table)
359 60
    {
360 60
        $discrColumn     = $class->discriminatorColumn;
361
        $discrColumnType = $discrColumn->getTypeName();
362 60
        $options         = [
363
            'notnull' => ! $discrColumn->isNullable(),
364
        ];
365 60
366
        switch ($discrColumnType) {
367 54
            case 'string':
368 54
                $options['length'] = $discrColumn->getLength() ?? 255;
369
                break;
370
371
            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...
372
                $options['scale']     = $discrColumn->getScale();
373
                $options['precision'] = $discrColumn->getPrecision();
374
                break;
375
        }
376 60
377
        if (! empty($discrColumn->getColumnDefinition())) {
378
            $options['columnDefinition'] = $discrColumn->getColumnDefinition();
379
        }
380 60
381 60
        $table->addColumn($discrColumn->getColumnName(), $discrColumnType, $options);
382
    }
383
384
    /**
385
     * Gathers the column definitions as required by the DBAL of all field mappings
386
     * found in the given class.
387
     *
388
     * @param ClassMetadata $class
389 173
     */
390
    private function gatherColumns($class, Table $table)
391 173
    {
392
        $pkColumns = [];
393 173
394 173
        foreach ($class->getDeclaredPropertiesIterator() as $fieldName => $property) {
395 127
            if (! ($property instanceof FieldMetadata)) {
396
                continue;
397
            }
398 173
399 18
            if ($class->inheritanceType === InheritanceType::SINGLE_TABLE && $class->isInheritedProperty($fieldName)) {
400
                continue;
401
            }
402 173
403
            $this->gatherColumn($class, $property, $table);
404 173
405 173
            if ($property->isPrimaryKey()) {
406
                $pkColumns[] = $this->platform->quoteIdentifier($property->getColumnName());
407
            }
408 173
        }
409
    }
410
411
    /**
412
     * Creates a column definition as required by the DBAL from an ORM field mapping definition.
413
     *
414
     * @param ClassMetadata $classMetadata The class that owns the field mapping.
415
     * @param FieldMetadata $fieldMetadata The field mapping.
416
     *
417
     * @return Column The portable column definition as required by the DBAL.
418 191
     */
419
    private function gatherColumn($classMetadata, FieldMetadata $fieldMetadata, Table $table)
420 191
    {
421 191
        $fieldName  = $fieldMetadata->getName();
422 191
        $columnName = $fieldMetadata->getColumnName();
423
        $columnType = $fieldMetadata->getTypeName();
424
425 191
        $options = [
426 191
            'length'          => $fieldMetadata->getLength(),
427
            'notnull'         => ! $fieldMetadata->isNullable(),
428 191
            'platformOptions' => [
429
                'version' => ($classMetadata->isVersioned() && $classMetadata->versionProperty->getName() === $fieldName),
430
            ],
431
        ];
432 191
433 6
        if ($classMetadata->inheritanceType === InheritanceType::SINGLE_TABLE && $classMetadata->getParent()) {
434
            $options['notnull'] = false;
435
        }
436 191
437
        if (strtolower($columnType) === 'string' && $options['length'] === null) {
438
            $options['length'] = 255;
439
        }
440 191
441 191
        if (is_int($fieldMetadata->getPrecision())) {
442
            $options['precision'] = $fieldMetadata->getPrecision();
443
        }
444 191
445 191
        if (is_int($fieldMetadata->getScale())) {
446
            $options['scale'] = $fieldMetadata->getScale();
447
        }
448 191
449 1
        if ($fieldMetadata->getColumnDefinition()) {
450
            $options['columnDefinition'] = $fieldMetadata->getColumnDefinition();
451
        }
452 191
453
        $fieldOptions = $fieldMetadata->getOptions();
454 191
455 18
        if ($fieldOptions) {
456
            $knownOptions = ['comment', 'unsigned', 'fixed', 'default'];
457 18
458 18
            foreach ($knownOptions as $knownOption) {
459 17
                if (array_key_exists($knownOption, $fieldOptions)) {
460
                    $options[$knownOption] = $fieldOptions[$knownOption];
461 18
462
                    unset($fieldOptions[$knownOption]);
463
                }
464
            }
465 18
466
            $options['customSchemaOptions'] = $fieldOptions;
467
        }
468 191
469 158
        if ($fieldMetadata->hasValueGenerator() && $fieldMetadata->getValueGenerator()->getType() === GeneratorType::IDENTITY && $classMetadata->getIdentifierFieldNames() === [$fieldName]) {
470
            $options['autoincrement'] = true;
471
        }
472 191
473 44
        if ($classMetadata->inheritanceType === InheritanceType::JOINED && ! $classMetadata->isRootEntity()) {
474
            $options['autoincrement'] = false;
475
        }
476 191
477
        $quotedColumnName = $this->platform->quoteIdentifier($fieldMetadata->getColumnName());
478 191
479
        if ($table->hasColumn($quotedColumnName)) {
480
            // required in some inheritance scenarios
481
            $table->changeColumn($quotedColumnName, $options);
482
483
            $column = $table->getColumn($quotedColumnName);
484 191
        } else {
485
            $column = $table->addColumn($quotedColumnName, $columnType, $options);
486
        }
487 191
488 10
        if ($fieldMetadata->isUnique()) {
489
            $table->addUniqueIndex([$columnName]);
490
        }
491 191
492
        return $column;
493
    }
494
495
    /**
496
     * Gathers the SQL for properly setting up the relations of the given class.
497
     * This includes the SQL for foreign key constraints and join tables.
498
     *
499
     * @param ClassMetadata $class
500
     * @param Table         $table
501
     * @param Schema        $schema
502
     * @param mixed[][]     $addedFks
503
     * @param bool[]        $blacklistedFks
504
     *
505
     * @throws \Doctrine\ORM\ORMException
506 191
     */
507
    private function gatherRelationsSql($class, $table, $schema, &$addedFks, &$blacklistedFks)
508 191
    {
509 191
        foreach ($class->getDeclaredPropertiesIterator() as $fieldName => $property) {
510 191
            if (! ($property instanceof AssociationMetadata)) {
511
                continue;
512
            }
513 133
514 19
            if ($class->isInheritedProperty($fieldName) && ! $property->getDeclaringClass()->isMappedSuperclass) {
515
                continue;
516
            }
517 133
518 91
            if (! $property->isOwningSide()) {
519
                continue;
520
            }
521 133
522
            $foreignClass = $this->em->getClassMetadata($property->getTargetEntity());
523
524 133
            switch (true) {
525 118
                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...
526
                    $primaryKeyColumns = []; // PK is unnecessary for this relation-type
527 118
528 118
                    $this->gatherRelationJoinColumns(
529 118
                        $property->getJoinColumns(),
530 118
                        $table,
531 118
                        $foreignClass,
532 118
                        $property,
533 118
                        $primaryKeyColumns,
534 118
                        $addedFks,
535
                        $blacklistedFks
536
                    );
537 118
538
                    break;
539 35
540
                case ($property instanceof OneToManyAssociationMetadata):
541
                    //... create join table, one-many through join table supported later
542
                    throw NotSupported::create();
543 35
544
                case ($property instanceof ManyToManyAssociationMetadata):
545 35
                    // create join table
546 35
                    $joinTable     = $property->getJoinTable();
547 35
                    $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
548
                    $theJoinTable  = $schema->createTable($joinTableName);
549 35
550
                    $primaryKeyColumns = [];
551
552 35
                    // Build first FK constraint (relation table => source table)
553 35
                    $this->gatherRelationJoinColumns(
554 35
                        $joinTable->getJoinColumns(),
555 35
                        $theJoinTable,
556 35
                        $class,
557 35
                        $property,
558 35
                        $primaryKeyColumns,
559 35
                        $addedFks,
560
                        $blacklistedFks
561
                    );
562
563 35
                    // Build second FK constraint (relation table => target table)
564 35
                    $this->gatherRelationJoinColumns(
565 35
                        $joinTable->getInverseJoinColumns(),
566 35
                        $theJoinTable,
567 35
                        $foreignClass,
568 35
                        $property,
569 35
                        $primaryKeyColumns,
570 35
                        $addedFks,
571
                        $blacklistedFks
572
                    );
573 35
574
                    $theJoinTable->setPrimaryKey($primaryKeyColumns);
575 133
576
                    break;
577
            }
578 191
        }
579
    }
580
581
    /**
582
     * Gets the class metadata that is responsible for the definition of the referenced column name.
583
     *
584
     * Previously this was a simple task, but with DDC-117 this problem is actually recursive. If its
585
     * not a simple field, go through all identifier field names that are associations recursively and
586
     * find that referenced column name.
587
     *
588
     * TODO: Is there any way to make this code more pleasing?
589
     *
590
     * @param ClassMetadata $class
591
     * @param string        $referencedColumnName
592
     *
593
     * @return mixed[] (ClassMetadata, referencedFieldName)
594 133
     */
595
    private function getDefiningClass($class, $referencedColumnName)
596 133
    {
597 133
        if (isset($class->fieldNames[$referencedColumnName])) {
598
            $propertyName = $class->fieldNames[$referencedColumnName];
599 133
600 133
            if ($class->hasField($propertyName)) {
601
                return [$class, $propertyName];
602
            }
603
        }
604 9
605 9
        $idColumns        = $class->getIdentifierColumns($this->em);
606
        $idColumnNameList = array_keys($idColumns);
607 9
608
        if (! in_array($referencedColumnName, $idColumnNameList, true)) {
609
            return null;
610
        }
611
612 9
        // it seems to be an entity as foreign key
613 9
        foreach ($class->getIdentifierFieldNames() as $fieldName) {
614
            $property = $class->getProperty($fieldName);
615 9
616 5
            if (! ($property instanceof AssociationMetadata)) {
617
                continue;
618
            }
619 9
620
            $joinColumns = $property->getJoinColumns();
621 9
622
            if (count($joinColumns) > 1) {
623
                throw MappingException::noSingleAssociationJoinColumnFound($class->getClassName(), $fieldName);
624
            }
625 9
626
            $joinColumn = reset($joinColumns);
627 9
628 9
            if ($joinColumn->getColumnName() === $referencedColumnName) {
629
                $targetEntity = $this->em->getClassMetadata($property->getTargetEntity());
630 9
631
                return $this->getDefiningClass($targetEntity, $joinColumn->getReferencedColumnName());
632
            }
633
        }
634
635
        return null;
636
    }
637
638
    /**
639
     * Gathers columns and fk constraints that are required for one part of relationship.
640
     *
641
     * @param JoinColumnMetadata[] $joinColumns
642
     * @param Table                $theJoinTable
643
     * @param ClassMetadata        $class
644
     * @param AssociationMetadata  $mapping
645
     * @param string[]             $primaryKeyColumns
646
     * @param mixed[][]            $addedFks
647
     * @param bool[]               $blacklistedFks
648
     *
649
     * @throws \Doctrine\ORM\ORMException
650 133
     */
651
    private function gatherRelationJoinColumns(
652
        $joinColumns,
653
        $theJoinTable,
654
        $class,
655
        $mapping,
656
        &$primaryKeyColumns,
657
        &$addedFks,
658
        &$blacklistedFks
659 133
    ) {
660 133
        $localColumns      = [];
661 133
        $foreignColumns    = [];
662 133
        $fkOptions         = [];
663 133
        $foreignTableName  = $class->table->getQuotedQualifiedName($this->platform);
664
        $uniqueConstraints = [];
665 133
666 133
        foreach ($joinColumns as $joinColumn) {
667 133
            list($definingClass, $referencedFieldName) = $this->getDefiningClass(
668 133
                $class,
669
                $joinColumn->getReferencedColumnName()
670
            );
671 133
672
            if (! $definingClass) {
673
                throw new \Doctrine\ORM\ORMException(sprintf(
674
                    'Column name "%s" referenced for relation from %s towards %s does not exist.',
675
                    $joinColumn->getReferencedColumnName(),
676
                    $mapping->getSourceEntity(),
677
                    $mapping->getTargetEntity()
678
                ));
679
            }
680 133
681 133
            $quotedColumnName           = $this->platform->quoteIdentifier($joinColumn->getColumnName());
682
            $quotedReferencedColumnName = $this->platform->quoteIdentifier($joinColumn->getReferencedColumnName());
683 133
684 133
            $primaryKeyColumns[] = $quotedColumnName;
685 133
            $localColumns[]      = $quotedColumnName;
686
            $foreignColumns[]    = $quotedReferencedColumnName;
687 133
688
            if (! $theJoinTable->hasColumn($quotedColumnName)) {
689
                // Only add the column to the table if it does not exist already.
690
                // It might exist already if the foreign key is mapped into a regular
691 131
                // property as well.
692 131
                $property  = $definingClass->getProperty($referencedFieldName);
693
                $columnDef = null;
694 131
695
                if (! empty($joinColumn->getColumnDefinition())) {
696 131
                    $columnDef = $joinColumn->getColumnDefinition();
697 1
                } elseif ($property->getColumnDefinition()) {
698
                    $columnDef = $property->getColumnDefinition();
699
                }
700 131
701
                $columnType    = $property->getTypeName();
702 131
                $columnOptions = [
703 131
                    'notnull'          => ! $joinColumn->isNullable(),
704
                    'columnDefinition' => $columnDef,
705
                ];
706 131
707
                if ($property->getOptions()) {
708
                    $columnOptions['options'] = $property->getOptions();
709
                }
710 131
711
                switch ($columnType) {
712 7
                    case 'string':
713 7
                        $columnOptions['length'] = is_int($property->getLength()) ? $property->getLength() : 255;
714
                        break;
715
716
                    case 'decimal':
717
                        $columnOptions['scale']     = $property->getScale();
718
                        $columnOptions['precision'] = $property->getPrecision();
719
                        break;
720
                }
721 131
722
                $theJoinTable->addColumn($quotedColumnName, $columnType, $columnOptions);
723
            }
724 133
725 41
            if ($joinColumn->isUnique()) {
726
                $uniqueConstraints[] = ['columns' => [$quotedColumnName]];
727
            }
728 133
729 133
            if (! empty($joinColumn->getOnDelete())) {
730
                $fkOptions['onDelete'] = $joinColumn->getOnDelete();
731
            }
732
        }
733
734
        // Prefer unique constraints over implicit simple indexes created for foreign keys.
735 133
        // Also avoids index duplication.
736 41
        foreach ($uniqueConstraints as $indexName => $unique) {
737
            $theJoinTable->addUniqueIndex($unique['columns'], is_numeric($indexName) ? null : $indexName);
738
        }
739 133
740
        $compositeName = $theJoinTable->getName() . '.' . implode('', $localColumns);
741 133
742 1
        if (isset($addedFks[$compositeName])
743 133
            && ($foreignTableName !== $addedFks[$compositeName]['foreignTableName']
744
            || 0 < count(array_diff($foreignColumns, $addedFks[$compositeName]['foreignColumns'])))
745 1
        ) {
746 1
            foreach ($theJoinTable->getForeignKeys() as $fkName => $key) {
747 1
                if (count(array_diff($key->getLocalColumns(), $localColumns)) === 0
748 1
                    && (($key->getForeignTableName() !== $foreignTableName)
749
                    || 0 < count(array_diff($key->getForeignColumns(), $foreignColumns)))
750 1
                ) {
751 1
                    $theJoinTable->removeForeignKey($fkName);
752
                    break;
753
                }
754
            }
755 1
756 133
            $blacklistedFks[$compositeName] = true;
757 133
        } elseif (! isset($blacklistedFks[$compositeName])) {
758 133
            $addedFks[$compositeName] = [
759 133
                'foreignTableName' => $foreignTableName,
760
                'foreignColumns'   => $foreignColumns,
761
            ];
762 133
763 133
            $theJoinTable->addForeignKeyConstraint(
764 133
                $foreignTableName,
765 133
                $localColumns,
766 133
                $foreignColumns,
767
                $fkOptions
768
            );
769 133
        }
770
    }
771
772
    /**
773
     * Drops the database schema for the given classes.
774
     *
775
     * In any way when an exception is thrown it is suppressed since drop was
776
     * issued for all classes of the schema and some probably just don't exist.
777
     *
778
     * @param ClassMetadata[] $classes
779 8
     */
780
    public function dropSchema(array $classes)
781 8
    {
782 8
        $dropSchemaSql = $this->getDropSchemaSQL($classes);
783
        $conn          = $this->em->getConnection();
784 8
785
        foreach ($dropSchemaSql as $sql) {
786 8
            try {
787 8
                $conn->executeQuery($sql);
788
            } catch (\Throwable $e) {
789
                // ignored
790
            }
791 8
        }
792
    }
793
794
    /**
795
     * Drops all elements in the database of the current connection.
796
     */
797
    public function dropDatabase()
798
    {
799
        $dropSchemaSql = $this->getDropDatabaseSQL();
800
        $conn          = $this->em->getConnection();
801
802
        foreach ($dropSchemaSql as $sql) {
803
            $conn->executeQuery($sql);
804
        }
805
    }
806
807
    /**
808
     * Gets the SQL needed to drop the database schema for the connections database.
809
     *
810
     * @return string[]
811
     */
812
    public function getDropDatabaseSQL()
813
    {
814
        $sm     = $this->em->getConnection()->getSchemaManager();
815
        $schema = $sm->createSchema();
816
817
        $visitor = new DropSchemaSqlCollector($this->platform);
818
        $schema->visit($visitor);
819
820
        return $visitor->getQueries();
821
    }
822
823
    /**
824
     * Gets SQL to drop the tables defined by the passed classes.
825
     *
826
     * @param ClassMetadata[] $classes
827
     *
828
     * @return string[]
829 8
     */
830
    public function getDropSchemaSQL(array $classes)
831 8
    {
832 8
        $visitor = new DropSchemaSqlCollector($this->platform);
833
        $schema  = $this->getSchemaFromMetadata($classes);
834 8
835 8
        $sm         = $this->em->getConnection()->getSchemaManager();
836
        $fullSchema = $sm->createSchema();
837 8
838 8
        foreach ($fullSchema->getTables() as $table) {
839 6
            if (! $schema->hasTable($table->getName())) {
840
                foreach ($table->getForeignKeys() as $foreignKey) {
841
                    /* @var $foreignKey \Doctrine\DBAL\Schema\ForeignKeyConstraint */
842 6
                    if ($schema->hasTable($foreignKey->getForeignTableName())) {
843
                        $visitor->acceptForeignKey($table, $foreignKey);
844
                    }
845
                }
846 8
            } else {
847 8
                $visitor->acceptTable($table);
848 8
                foreach ($table->getForeignKeys() as $foreignKey) {
849
                    $visitor->acceptForeignKey($table, $foreignKey);
850
                }
851
            }
852
        }
853 8
854
        if ($this->platform->supportsSequences()) {
855
            foreach ($schema->getSequences() as $sequence) {
856
                $visitor->acceptSequence($sequence);
857
            }
858
859
            foreach ($schema->getTables() as $table) {
860
                /* @var $sequence Table */
861
                if ($table->hasPrimaryKey()) {
862
                    $columns = $table->getPrimaryKey()->getColumns();
863
                    if (count($columns) === 1) {
864
                        $checkSequence = $table->getName() . '_' . $columns[0] . '_seq';
865
                        if ($fullSchema->hasSequence($checkSequence)) {
866
                            $visitor->acceptSequence($fullSchema->getSequence($checkSequence));
867
                        }
868
                    }
869
                }
870
            }
871
        }
872 8
873
        return $visitor->getQueries();
874
    }
875
876
    /**
877
     * Updates the database schema of the given classes by comparing the ClassMetadata
878
     * instances to the current database schema that is inspected.
879
     *
880
     * @param ClassMetadata[] $classes
881
     * @param bool            $saveMode If TRUE, only performs a partial update
882
     *                                  without dropping assets which are scheduled for deletion.
883
     */
884
    public function updateSchema(array $classes, $saveMode = false)
885
    {
886
        $updateSchemaSql = $this->getUpdateSchemaSql($classes, $saveMode);
887
        $conn            = $this->em->getConnection();
888
889
        foreach ($updateSchemaSql as $sql) {
890
            $conn->executeQuery($sql);
891
        }
892
    }
893
894
    /**
895
     * Gets the sequence of SQL statements that need to be performed in order
896
     * to bring the given class mappings in-synch with the relational schema.
897
     *
898
     * @param ClassMetadata[] $classes  The classes to consider.
899
     * @param bool            $saveMode If TRUE, only generates SQL for a partial update
900
     *                                  that does not include SQL for dropping assets which are scheduled for deletion.
901
     *
902
     * @return string[] The sequence of SQL statements.
903 1
     */
904
    public function getUpdateSchemaSql(array $classes, $saveMode = false)
905 1
    {
906
        $sm = $this->em->getConnection()->getSchemaManager();
907 1
908 1
        $fromSchema = $sm->createSchema();
909
        $toSchema   = $this->getSchemaFromMetadata($classes);
910 1
911 1
        $comparator = new Comparator();
912
        $schemaDiff = $comparator->compare($fromSchema, $toSchema);
913 1
914
        if ($saveMode) {
915
            return $schemaDiff->toSaveSql($this->platform);
916
        }
917 1
918
        return $schemaDiff->toSql($this->platform);
919
    }
920
}
921