Failed Conditions
Pull Request — master (#6959)
by Matthew
12:08
created

SchemaTool::getSchemaFromMetadata()   F

Complexity

Conditions 46
Paths > 20000

Size

Total Lines 232
Code Lines 124

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 112
CRAP Score 46.4301

Importance

Changes 0
Metric Value
cc 46
eloc 124
nc 69129
nop 1
dl 0
loc 232
ccs 112
cts 119
cp 0.9412
crap 46.4301
rs 2
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ORM\Tools;
6
7
use Doctrine\DBAL\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
29
/**
30
 * The SchemaTool is a tool to create/drop/update database schemas based on
31
 * <tt>ClassMetadata</tt> class descriptors.
32
 */
33
class SchemaTool
34
{
35
    /**
36
     * @var \Doctrine\ORM\EntityManagerInterface
37
     */
38
    private $em;
39
40
    /**
41
     * @var \Doctrine\DBAL\Platforms\AbstractPlatform
42
     */
43
    private $platform;
44
45
    /**
46
     * Initializes a new SchemaTool instance that uses the connection of the
47
     * provided EntityManager.
48
     */
49 1211
    public function __construct(EntityManagerInterface $em)
50
    {
51 1211
        $this->em       = $em;
52 1211
        $this->platform = $em->getConnection()->getDatabasePlatform();
53 1211
    }
54
55
    /**
56
     * Creates the database schema for the given array of ClassMetadata instances.
57
     *
58
     * @param ClassMetadata[] $classes
59
     *
60
     * @throws ToolsException
61
     */
62 246
    public function createSchema(array $classes)
63
    {
64 246
        $createSchemaSql = $this->getCreateSchemaSql($classes);
65 246
        $conn            = $this->em->getConnection();
66
67 246
        foreach ($createSchemaSql as $sql) {
68
            try {
69 246
                $conn->executeQuery($sql);
70 68
            } catch (\Throwable $e) {
71 246
                throw ToolsException::schemaToolFailure($sql, $e);
72
            }
73
        }
74 178
    }
75
76
    /**
77
     * Gets the list of DDL statements that are required to create the database schema for
78
     * the given list of ClassMetadata instances.
79
     *
80
     * @param ClassMetadata[] $classes
81
     *
82
     * @return string[] The SQL statements needed to create the schema for the classes.
83
     */
84 246
    public function getCreateSchemaSql(array $classes)
85
    {
86 246
        $schema = $this->getSchemaFromMetadata($classes);
87
88 246
        return $schema->toSql($this->platform);
89
    }
90
91
    /**
92
     * Detects instances of ClassMetadata that don't need to be processed in the SchemaTool context.
93
     *
94
     * @param ClassMetadata   $class
95
     * @param ClassMetadata[] $processedClasses
96
     *
97
     * @return bool
98
     */
99 255
    private function processingNotRequired($class, array $processedClasses)
100
    {
101 255
        return isset($processedClasses[$class->getClassName()]) ||
102 255
            $class->isMappedSuperclass ||
103 255
            $class->isEmbeddedClass ||
104 255
            ($class->inheritanceType === InheritanceType::SINGLE_TABLE && ! $class->isRootEntity())
105
        ;
106
    }
107
108
    /**
109
     * Creates a Schema instance from a given set of metadata classes.
110
     *
111
     * @param ClassMetadata[] $classes
112
     *
113
     * @return Schema
114
     *
115
     * @throws \Doctrine\ORM\ORMException
116
     */
117 255
    public function getSchemaFromMetadata(array $classes)
118
    {
119
        // Reminder for processed classes, used for hierarchies
120 255
        $processedClasses     = [];
121 255
        $eventManager         = $this->em->getEventManager();
122 255
        $schemaManager        = $this->em->getConnection()->getSchemaManager();
123 255
        $metadataSchemaConfig = $schemaManager->createSchemaConfig();
124
125 255
        $metadataSchemaConfig->setExplicitForeignKeyIndexes(false);
126 255
        $schema = new Schema([], [], $metadataSchemaConfig);
127
128 255
        $addedFks       = [];
129 255
        $blacklistedFks = [];
130
131 255
        foreach ($classes as $class) {
132
            /** @var \Doctrine\ORM\Mapping\ClassMetadata $class */
133 255
            if ($this->processingNotRequired($class, $processedClasses)) {
134 19
                continue;
135
            }
136
137 255
            $table = $schema->createTable($class->table->getQuotedQualifiedName($this->platform));
138
139 255
            switch ($class->inheritanceType) {
140
                case InheritanceType::SINGLE_TABLE:
141 21
                    $this->gatherColumns($class, $table);
142 21
                    $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
143
144
                    // Add the discriminator column
145 21
                    $this->addDiscriminatorColumnDefinition($class, $table);
146
147
                    // Aggregate all the information from all classes in the hierarchy
148 21
                    $parentClass = $class;
149
150 21
                    while (($parentClass = $parentClass->getParent()) !== null) {
151
                        // Parent class information is already contained in this class
152
                        $processedClasses[$parentClass->getClassName()] = true;
153
                    }
154
155 21
                    foreach ($class->getSubClasses() as $subClassName) {
156 19
                        $subClass = $this->em->getClassMetadata($subClassName);
157
158 19
                        $this->gatherColumns($subClass, $table);
159 19
                        $this->gatherRelationsSql($subClass, $table, $schema, $addedFks, $blacklistedFks);
160
161 19
                        $processedClasses[$subClassName] = true;
162
                    }
163
164 21
                    break;
165
166
                case InheritanceType::JOINED:
167
                    // Add all non-inherited fields as columns
168 58
                    $pkColumns = [];
169
170 58
                    foreach ($class->getDeclaredPropertiesIterator() as $fieldName => $property) {
171 58
                        if (! ($property instanceof FieldMetadata)) {
172 16
                            continue;
173
                        }
174
175 58
                        if (! $class->isInheritedProperty($fieldName)) {
176 58
                            $columnName = $this->platform->quoteIdentifier($property->getColumnName());
177
178 58
                            $this->gatherColumn($class, $property, $table);
179
180 58
                            if ($class->isIdentifier($fieldName)) {
181 58
                                $pkColumns[] = $columnName;
182
                            }
183
                        }
184
                    }
185
186 58
                    $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
187
188
                    // Add the discriminator column only to the root table
189 58
                    if ($class->isRootEntity()) {
190 58
                        $this->addDiscriminatorColumnDefinition($class, $table);
191
                    } else {
192
                        // Add an ID FK column to child tables
193 57
                        $inheritedKeyColumns = [];
194
195 57
                        foreach ($class->identifier as $identifierField) {
196 57
                            $idProperty = $class->getProperty($identifierField);
197
198 57
                            if ($class->isInheritedProperty($identifierField)) {
199 57
                                $column     = $this->gatherColumn($class, $idProperty, $table);
200 57
                                $columnName = $column->getQuotedName($this->platform);
201
202
                                // TODO: This seems rather hackish, can we optimize it?
203 57
                                $column->setAutoincrement(false);
204
205 57
                                $pkColumns[]           = $columnName;
206 57
                                $inheritedKeyColumns[] = $columnName;
207
                            }
208
                        }
209
210 57
                        if (! empty($inheritedKeyColumns)) {
211
                            // Add a FK constraint on the ID column
212 57
                            $rootClass = $this->em->getClassMetadata($class->getRootClassName());
213
214 57
                            $table->addForeignKeyConstraint(
215 57
                                $rootClass->table->getQuotedQualifiedName($this->platform),
216 57
                                $inheritedKeyColumns,
217 57
                                $inheritedKeyColumns,
218 57
                                ['onDelete' => 'CASCADE']
219
                            );
220
                        }
221
                    }
222
223 58
                    $table->setPrimaryKey($pkColumns);
224
225 58
                    break;
226
227
                case InheritanceType::TABLE_PER_CLASS:
228
                    throw ORMException::notSupported();
229
230
                default:
231 230
                    $this->gatherColumns($class, $table);
232 230
                    $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
233
234 230
                    break;
235
            }
236
237 255
            $pkColumns = [];
238
239 255
            foreach ($class->identifier as $identifierField) {
240 255
                $property = $class->getProperty($identifierField);
241
242 255
                if ($property instanceof FieldMetadata) {
243 255
                    $pkColumns[] = $this->platform->quoteIdentifier($property->getColumnName());
244
245 255
                    continue;
246
                }
247
248 29
                if ($property instanceof ToOneAssociationMetadata) {
249 29
                    foreach ($property->getJoinColumns() as $joinColumn) {
250 29
                        $pkColumns[] = $this->platform->quoteIdentifier($joinColumn->getColumnName());
251
                    }
252
                }
253
            }
254
255 255
            if (! $table->hasIndex('primary')) {
256 237
                $table->setPrimaryKey($pkColumns);
257
            }
258
259
            // there can be unique indexes automatically created for join column
260
            // if join column is also primary key we should keep only primary key on this column
261
            // so, remove indexes overruled by primary key
262 255
            $primaryKey = $table->getIndex('primary');
263
264 255
            foreach ($table->getIndexes() as $idxKey => $existingIndex) {
265 255
                if ($primaryKey->overrules($existingIndex)) {
266 255
                    $table->dropIndex($idxKey);
267
                }
268
            }
269
270 255
            if ($class->table->getIndexes()) {
271 1
                foreach ($class->table->getIndexes() as $indexName => $indexData) {
272 1
                    $indexName = is_numeric($indexName) ? null : $indexName;
273 1
                    $index     = new Index($indexName, $indexData['columns'], $indexData['unique'], $indexData['flags'], $indexData['options']);
274
275 1
                    foreach ($table->getIndexes() as $tableIndexName => $tableIndex) {
276 1
                        if ($tableIndex->isFullfilledBy($index)) {
277
                            $table->dropIndex($tableIndexName);
278 1
                            break;
279
                        }
280
                    }
281
282 1
                    if ($indexData['unique']) {
283
                        $table->addUniqueIndex($indexData['columns'], $indexName, $indexData['options']);
284
                    } else {
285 1
                        $table->addIndex($indexData['columns'], $indexName, $indexData['flags'], $indexData['options']);
286
                    }
287
                }
288
            }
289
290 255
            if ($class->table->getUniqueConstraints()) {
291 4
                foreach ($class->table->getUniqueConstraints() as $indexName => $indexData) {
292 4
                    $indexName = is_numeric($indexName) ? null : $indexName;
293 4
                    $uniqIndex = new Index($indexName, $indexData['columns'], true, false, $indexData['flags'], $indexData['options']);
294
295 4
                    foreach ($table->getIndexes() as $tableIndexName => $tableIndex) {
296 4
                        if ($tableIndex->isFullfilledBy($uniqIndex)) {
297 3
                            $table->dropIndex($tableIndexName);
298 4
                            break;
299
                        }
300
                    }
301
302 4
                    $table->addUniqueConstraint($indexData['columns'], $indexName, $indexData['flags'], $indexData['options']);
303
                }
304
            }
305
306 255
            if ($class->table->getOptions()) {
307 1
                foreach ($class->table->getOptions() as $key => $val) {
308 1
                    $table->addOption($key, $val);
309
                }
310
            }
311
312 255
            $processedClasses[$class->getClassName()] = true;
313
314 255
            foreach ($class->getDeclaredPropertiesIterator() as $property) {
315 255
                if (! $property instanceof FieldMetadata
316 255
                    || ! $property->hasValueGenerator()
317 225
                    || $property->getValueGenerator()->getType() !== GeneratorType::SEQUENCE
318 255
                    || $class->getClassName() !== $class->getRootClassName()) {
319 255
                    continue;
320
                }
321
322
                $quotedName = $this->platform->quoteIdentifier($property->getValueGenerator()->getDefinition()['sequenceName']);
323
324
                if (! $schema->hasSequence($quotedName)) {
325
                    $schema->createSequence($quotedName, $property->getValueGenerator()->getDefinition()['allocationSize']);
326
                }
327
            }
328
329 255
            if ($eventManager->hasListeners(ToolEvents::postGenerateSchemaTable)) {
330 1
                $eventManager->dispatchEvent(
331 1
                    ToolEvents::postGenerateSchemaTable,
332 255
                    new GenerateSchemaTableEventArgs($class, $schema, $table)
333
                );
334
            }
335
        }
336
337 255
        if (! $this->platform->supportsSchemas() && ! $this->platform->canEmulateSchemas()) {
338 8
            $schema->visit(new RemoveNamespacedAssets());
339
        }
340
341 255
        if ($eventManager->hasListeners(ToolEvents::postGenerateSchema)) {
342 1
            $eventManager->dispatchEvent(
343 1
                ToolEvents::postGenerateSchema,
344 1
                new GenerateSchemaEventArgs($this->em, $schema)
345
            );
346
        }
347
348 255
        return $schema;
349
    }
350
351
    /**
352
     * Gets a portable column definition as required by the DBAL for the discriminator
353
     * column of a class.
354
     *
355
     * @param ClassMetadata $class
356
     */
357 74
    private function addDiscriminatorColumnDefinition($class, Table $table)
358
    {
359 74
        $discrColumn     = $class->discriminatorColumn;
360 74
        $discrColumnType = $discrColumn->getTypeName();
361
        $options         = [
362 74
            'notnull' => ! $discrColumn->isNullable(),
363
        ];
364
365 74
        switch ($discrColumnType) {
366
            case 'string':
367 67
                $options['length'] = $discrColumn->getLength() ?? 255;
368 67
                break;
369
370
            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...
371
                $options['scale']     = $discrColumn->getScale();
372
                $options['precision'] = $discrColumn->getPrecision();
373
                break;
374
        }
375
376 74
        if (! empty($discrColumn->getColumnDefinition())) {
377
            $options['columnDefinition'] = $discrColumn->getColumnDefinition();
378
        }
379
380 74
        $table->addColumn($discrColumn->getColumnName(), $discrColumnType, $options);
381 74
    }
382
383
    /**
384
     * Gathers the column definitions as required by the DBAL of all field mappings
385
     * found in the given class.
386
     *
387
     * @param ClassMetadata $class
388
     */
389 237
    private function gatherColumns($class, Table $table)
390
    {
391 237
        $pkColumns = [];
392
393 237
        foreach ($class->getDeclaredPropertiesIterator() as $fieldName => $property) {
394 237
            if (! ($property instanceof FieldMetadata)) {
395 178
                continue;
396
            }
397
398 237
            if ($class->inheritanceType === InheritanceType::SINGLE_TABLE && $class->isInheritedProperty($fieldName)) {
399 19
                continue;
400
            }
401
402 237
            $this->gatherColumn($class, $property, $table);
403
404 237
            if ($property->isPrimaryKey()) {
405 237
                $pkColumns[] = $this->platform->quoteIdentifier($property->getColumnName());
406
            }
407
        }
408 237
    }
409
410
    /**
411
     * Creates a column definition as required by the DBAL from an ORM field mapping definition.
412
     *
413
     * @param ClassMetadata $classMetadata The class that owns the field mapping.
414
     * @param FieldMetadata $fieldMetadata The field mapping.
415
     *
416
     * @return Column The portable column definition as required by the DBAL.
417
     */
418 255
    private function gatherColumn($classMetadata, FieldMetadata $fieldMetadata, Table $table)
419
    {
420 255
        $fieldName  = $fieldMetadata->getName();
421 255
        $columnName = $fieldMetadata->getColumnName();
422 255
        $columnType = $fieldMetadata->getTypeName();
423
424
        $options = [
425 255
            'length'          => $fieldMetadata->getLength(),
426 255
            'notnull'         => ! $fieldMetadata->isNullable(),
427
            'platformOptions' => [
428 255
                'version' => ($classMetadata->isVersioned() && $classMetadata->versionProperty->getName() === $fieldName),
429
            ],
430
        ];
431
432 255
        if ($classMetadata->inheritanceType === InheritanceType::SINGLE_TABLE && $classMetadata->getParent()) {
433 7
            $options['notnull'] = false;
434
        }
435
436 255
        if (strtolower($columnType) === 'string' && $options['length'] === null) {
437
            $options['length'] = 255;
438
        }
439
440 255
        if (is_int($fieldMetadata->getPrecision())) {
441 255
            $options['precision'] = $fieldMetadata->getPrecision();
442
        }
443
444 255
        if (is_int($fieldMetadata->getScale())) {
445 255
            $options['scale'] = $fieldMetadata->getScale();
446
        }
447
448 255
        if ($fieldMetadata->getColumnDefinition()) {
449 1
            $options['columnDefinition'] = $fieldMetadata->getColumnDefinition();
450
        }
451
452 255
        $fieldOptions = $fieldMetadata->getOptions();
453
454 255
        if ($fieldOptions) {
455 32
            $knownOptions = ['comment', 'unsigned', 'fixed', 'default'];
456
457 32
            foreach ($knownOptions as $knownOption) {
458 32
                if (array_key_exists($knownOption, $fieldOptions)) {
459 31
                    $options[$knownOption] = $fieldOptions[$knownOption];
460
461 32
                    unset($fieldOptions[$knownOption]);
462
                }
463
            }
464
465 32
            $options['customSchemaOptions'] = $fieldOptions;
466
        }
467
468 255
        if ($fieldMetadata->hasValueGenerator() && $fieldMetadata->getValueGenerator()->getType() === GeneratorType::IDENTITY && $classMetadata->getIdentifierFieldNames() === [$fieldName]) {
469 224
            $options['autoincrement'] = true;
470
        }
471
472 255
        if ($classMetadata->inheritanceType === InheritanceType::JOINED && ! $classMetadata->isRootEntity()) {
473 57
            $options['autoincrement'] = false;
474
        }
475
476 255
        $quotedColumnName = $this->platform->quoteIdentifier($fieldMetadata->getColumnName());
477
478 255
        if ($table->hasColumn($quotedColumnName)) {
479
            // required in some inheritance scenarios
480
            $table->changeColumn($quotedColumnName, $options);
481
482
            $column = $table->getColumn($quotedColumnName);
483
        } else {
484 255
            $column = $table->addColumn($quotedColumnName, $columnType, $options);
485
        }
486
487 255
        if ($fieldMetadata->isUnique()) {
488 17
            $table->addUniqueIndex([$columnName]);
489
        }
490
491 255
        return $column;
492
    }
493
494
    /**
495
     * Gathers the SQL for properly setting up the relations of the given class.
496
     * This includes the SQL for foreign key constraints and join tables.
497
     *
498
     * @param ClassMetadata $class
499
     * @param Table         $table
500
     * @param Schema        $schema
501
     * @param mixed[][]     $addedFks
502
     * @param bool[]        $blacklistedFks
503
     *
504
     * @throws \Doctrine\ORM\ORMException
505
     */
506 255
    private function gatherRelationsSql($class, $table, $schema, &$addedFks, &$blacklistedFks)
507
    {
508 255
        foreach ($class->getDeclaredPropertiesIterator() as $fieldName => $property) {
509 255
            if (! ($property instanceof AssociationMetadata)) {
510 255
                continue;
511
            }
512
513 183
            $declaringClass = $property->getDeclaringClass();
514
515 183
            assert($declaringClass instanceof ClassMetadata);
516
517 183
            if ($class->isInheritedProperty($fieldName) && ! $declaringClass->isMappedSuperclass) {
518 21
                continue;
519
            }
520
521 183
            if (! $property->isOwningSide()) {
522 131
                continue;
523
            }
524
525 183
            $foreignClass = $this->em->getClassMetadata($property->getTargetEntity());
526
527
            switch (true) {
528 183
                case ($property instanceof ToOneAssociationMetadata):
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

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

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

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

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

Loading history...
529 166
                    $primaryKeyColumns = []; // PK is unnecessary for this relation-type
530
531 166
                    $this->gatherRelationJoinColumns(
532 166
                        $property->getJoinColumns(),
533 166
                        $table,
534 166
                        $foreignClass,
535 166
                        $property,
536 166
                        $primaryKeyColumns,
537 166
                        $addedFks,
538 166
                        $blacklistedFks
539
                    );
540
541 166
                    break;
542
543 42
                case ($property instanceof OneToManyAssociationMetadata):
544
                    //... create join table, one-many through join table supported later
545
                    throw ORMException::notSupported();
546
547 42
                case ($property instanceof ManyToManyAssociationMetadata):
548
                    // create join table
549 42
                    $joinTable     = $property->getJoinTable();
550 42
                    $joinTableName = $joinTable->getQuotedQualifiedName($this->platform);
551 42
                    $theJoinTable  = $schema->createTable($joinTableName);
552
553 42
                    $primaryKeyColumns = [];
554
555
                    // Build first FK constraint (relation table => source table)
556 42
                    $this->gatherRelationJoinColumns(
557 42
                        $joinTable->getJoinColumns(),
558 42
                        $theJoinTable,
559 42
                        $class,
560 42
                        $property,
561 42
                        $primaryKeyColumns,
562 42
                        $addedFks,
563 42
                        $blacklistedFks
564
                    );
565
566
                    // Build second FK constraint (relation table => target table)
567 42
                    $this->gatherRelationJoinColumns(
568 42
                        $joinTable->getInverseJoinColumns(),
569 42
                        $theJoinTable,
570 42
                        $foreignClass,
571 42
                        $property,
572 42
                        $primaryKeyColumns,
573 42
                        $addedFks,
574 42
                        $blacklistedFks
575
                    );
576
577 42
                    $theJoinTable->setPrimaryKey($primaryKeyColumns);
578
579 183
                    break;
580
            }
581
        }
582 255
    }
583
584
    /**
585
     * Gets the class metadata that is responsible for the definition of the referenced column name.
586
     *
587
     * Previously this was a simple task, but with DDC-117 this problem is actually recursive. If its
588
     * not a simple field, go through all identifier field names that are associations recursively and
589
     * find that referenced column name.
590
     *
591
     * TODO: Is there any way to make this code more pleasing?
592
     *
593
     * @param ClassMetadata $class
594
     * @param string        $referencedColumnName
595
     *
596
     * @return mixed[] (ClassMetadata, referencedFieldName)
597
     */
598 183
    private function getDefiningClass($class, $referencedColumnName)
599
    {
600 183
        if (isset($class->fieldNames[$referencedColumnName])) {
601 183
            $propertyName = $class->fieldNames[$referencedColumnName];
602
603 183
            if ($class->hasField($propertyName)) {
604 183
                return [$class, $propertyName];
605
            }
606
        }
607
608 9
        $idColumns        = $class->getIdentifierColumns($this->em);
609 9
        $idColumnNameList = array_keys($idColumns);
610
611 9
        if (! in_array($referencedColumnName, $idColumnNameList, true)) {
612
            return null;
613
        }
614
615
        // it seems to be an entity as foreign key
616 9
        foreach ($class->getIdentifierFieldNames() as $fieldName) {
617 9
            $property = $class->getProperty($fieldName);
618
619 9
            if (! ($property instanceof AssociationMetadata)) {
620 5
                continue;
621
            }
622
623 9
            $joinColumns = $property->getJoinColumns();
624
625 9
            if (count($joinColumns) > 1) {
626
                throw MappingException::noSingleAssociationJoinColumnFound($class->getClassName(), $fieldName);
627
            }
628
629 9
            $joinColumn = reset($joinColumns);
630
631 9
            if ($joinColumn->getColumnName() === $referencedColumnName) {
632 9
                $targetEntity = $this->em->getClassMetadata($property->getTargetEntity());
633
634 9
                return $this->getDefiningClass($targetEntity, $joinColumn->getReferencedColumnName());
635
            }
636
        }
637
638
        return null;
639
    }
640
641
    /**
642
     * Gathers columns and fk constraints that are required for one part of relationship.
643
     *
644
     * @param JoinColumnMetadata[] $joinColumns
645
     * @param Table                $theJoinTable
646
     * @param ClassMetadata        $class
647
     * @param AssociationMetadata  $mapping
648
     * @param string[]             $primaryKeyColumns
649
     * @param mixed[][]            $addedFks
650
     * @param bool[]               $blacklistedFks
651
     *
652
     * @throws \Doctrine\ORM\ORMException
653
     */
654 183
    private function gatherRelationJoinColumns(
655
        $joinColumns,
656
        $theJoinTable,
657
        $class,
658
        $mapping,
659
        &$primaryKeyColumns,
660
        &$addedFks,
661
        &$blacklistedFks
662
    ) {
663 183
        $localColumns      = [];
664 183
        $foreignColumns    = [];
665 183
        $fkOptions         = [];
666 183
        $foreignTableName  = $class->table->getQuotedQualifiedName($this->platform);
667 183
        $uniqueConstraints = [];
668
669 183
        foreach ($joinColumns as $joinColumn) {
670 183
            list($definingClass, $referencedFieldName) = $this->getDefiningClass(
671 183
                $class,
672 183
                $joinColumn->getReferencedColumnName()
673
            );
674
675 183
            if (! $definingClass) {
676
                throw new \Doctrine\ORM\ORMException(sprintf(
677
                    'Column name "%s" referenced for relation from %s towards %s does not exist.',
678
                    $joinColumn->getReferencedColumnName(),
679
                    $mapping->getSourceEntity(),
680
                    $mapping->getTargetEntity()
681
                ));
682
            }
683
684 183
            $quotedColumnName           = $this->platform->quoteIdentifier($joinColumn->getColumnName());
685 183
            $quotedReferencedColumnName = $this->platform->quoteIdentifier($joinColumn->getReferencedColumnName());
686
687 183
            $primaryKeyColumns[] = $quotedColumnName;
688 183
            $localColumns[]      = $quotedColumnName;
689 183
            $foreignColumns[]    = $quotedReferencedColumnName;
690
691 183
            if (! $theJoinTable->hasColumn($quotedColumnName)) {
692
                // Only add the column to the table if it does not exist already.
693
                // It might exist already if the foreign key is mapped into a regular
694
                // property as well.
695 181
                $property  = $definingClass->getProperty($referencedFieldName);
696 181
                $columnDef = null;
697
698 181
                if (! empty($joinColumn->getColumnDefinition())) {
699
                    $columnDef = $joinColumn->getColumnDefinition();
700 181
                } elseif ($property->getColumnDefinition()) {
701 1
                    $columnDef = $property->getColumnDefinition();
702
                }
703
704 181
                $columnType    = $property->getTypeName();
705
                $columnOptions = [
706 181
                    'notnull'          => ! $joinColumn->isNullable(),
707 181
                    'columnDefinition' => $columnDef,
708
                ];
709
710 181
                if ($property->getOptions()) {
711
                    $columnOptions['options'] = $property->getOptions();
712
                }
713
714 181
                switch ($columnType) {
715
                    case 'string':
716 7
                        $columnOptions['length'] = is_int($property->getLength()) ? $property->getLength() : 255;
717 7
                        break;
718
719
                    case 'decimal':
720
                        $columnOptions['scale']     = $property->getScale();
721
                        $columnOptions['precision'] = $property->getPrecision();
722
                        break;
723
                }
724
725 181
                $theJoinTable->addColumn($quotedColumnName, $columnType, $columnOptions);
726
            }
727
728 183
            if ($joinColumn->isUnique()) {
729 59
                $uniqueConstraints[] = ['columns' => [$quotedColumnName]];
730
            }
731
732 183
            if (! empty($joinColumn->getOnDelete())) {
733 183
                $fkOptions['onDelete'] = $joinColumn->getOnDelete();
734
            }
735
        }
736
737
        // Prefer unique constraints over implicit simple indexes created for foreign keys.
738
        // Also avoids index duplication.
739 183
        foreach ($uniqueConstraints as $indexName => $unique) {
740 59
            $theJoinTable->addUniqueIndex($unique['columns'], is_numeric($indexName) ? null : $indexName);
741
        }
742
743 183
        $compositeName = $theJoinTable->getName() . '.' . implode('', $localColumns);
744
745 183
        if (isset($addedFks[$compositeName])
746 1
            && ($foreignTableName !== $addedFks[$compositeName]['foreignTableName']
747 183
            || 0 < count(array_diff($foreignColumns, $addedFks[$compositeName]['foreignColumns'])))
748
        ) {
749 1
            foreach ($theJoinTable->getForeignKeys() as $fkName => $key) {
750 1
                if (count(array_diff($key->getLocalColumns(), $localColumns)) === 0
751 1
                    && (($key->getForeignTableName() !== $foreignTableName)
752 1
                    || 0 < count(array_diff($key->getForeignColumns(), $foreignColumns)))
753
                ) {
754 1
                    $theJoinTable->removeForeignKey($fkName);
755 1
                    break;
756
                }
757
            }
758
759 1
            $blacklistedFks[$compositeName] = true;
760 183
        } elseif (! isset($blacklistedFks[$compositeName])) {
761 183
            $addedFks[$compositeName] = [
762 183
                'foreignTableName' => $foreignTableName,
763 183
                'foreignColumns'   => $foreignColumns,
764
            ];
765
766 183
            $theJoinTable->addForeignKeyConstraint(
767 183
                $foreignTableName,
768 183
                $localColumns,
769 183
                $foreignColumns,
770 183
                $fkOptions
771
            );
772
        }
773 183
    }
774
775
    /**
776
     * Drops the database schema for the given classes.
777
     *
778
     * In any way when an exception is thrown it is suppressed since drop was
779
     * issued for all classes of the schema and some probably just don't exist.
780
     *
781
     * @param ClassMetadata[] $classes
782
     */
783 8
    public function dropSchema(array $classes)
784
    {
785 8
        $dropSchemaSql = $this->getDropSchemaSQL($classes);
786 8
        $conn          = $this->em->getConnection();
787
788 8
        foreach ($dropSchemaSql as $sql) {
789
            try {
790 8
                $conn->executeQuery($sql);
791 8
            } catch (\Throwable $e) {
792
                // ignored
793
            }
794
        }
795 8
    }
796
797
    /**
798
     * Drops all elements in the database of the current connection.
799
     */
800
    public function dropDatabase()
801
    {
802
        $dropSchemaSql = $this->getDropDatabaseSQL();
803
        $conn          = $this->em->getConnection();
804
805
        foreach ($dropSchemaSql as $sql) {
806
            $conn->executeQuery($sql);
807
        }
808
    }
809
810
    /**
811
     * Gets the SQL needed to drop the database schema for the connections database.
812
     *
813
     * @return string[]
814
     */
815
    public function getDropDatabaseSQL()
816
    {
817
        $sm     = $this->em->getConnection()->getSchemaManager();
818
        $schema = $sm->createSchema();
819
820
        $visitor = new DropSchemaSqlCollector($this->platform);
821
        $schema->visit($visitor);
822
823
        return $visitor->getQueries();
824
    }
825
826
    /**
827
     * Gets SQL to drop the tables defined by the passed classes.
828
     *
829
     * @param ClassMetadata[] $classes
830
     *
831
     * @return string[]
832
     */
833 8
    public function getDropSchemaSQL(array $classes)
834
    {
835 8
        $visitor = new DropSchemaSqlCollector($this->platform);
836 8
        $schema  = $this->getSchemaFromMetadata($classes);
837
838 8
        $sm         = $this->em->getConnection()->getSchemaManager();
839 8
        $fullSchema = $sm->createSchema();
840
841 8
        foreach ($fullSchema->getTables() as $table) {
842 8
            if (! $schema->hasTable($table->getName())) {
843 6
                foreach ($table->getForeignKeys() as $foreignKey) {
844
                    /* @var $foreignKey \Doctrine\DBAL\Schema\ForeignKeyConstraint */
845
                    if ($schema->hasTable($foreignKey->getForeignTableName())) {
846 6
                        $visitor->acceptForeignKey($table, $foreignKey);
847
                    }
848
                }
849
            } else {
850 8
                $visitor->acceptTable($table);
851 8
                foreach ($table->getForeignKeys() as $foreignKey) {
852 8
                    $visitor->acceptForeignKey($table, $foreignKey);
853
                }
854
            }
855
        }
856
857 8
        if ($this->platform->supportsSequences()) {
858
            foreach ($schema->getSequences() as $sequence) {
859
                $visitor->acceptSequence($sequence);
860
            }
861
862
            foreach ($schema->getTables() as $table) {
863
                /* @var $sequence Table */
864
                if ($table->hasPrimaryKey()) {
865
                    $columns = $table->getPrimaryKey()->getColumns();
866
                    if (count($columns) === 1) {
867
                        $checkSequence = $table->getName() . '_' . $columns[0] . '_seq';
868
                        if ($fullSchema->hasSequence($checkSequence)) {
869
                            $visitor->acceptSequence($fullSchema->getSequence($checkSequence));
870
                        }
871
                    }
872
                }
873
            }
874
        }
875
876 8
        return $visitor->getQueries();
877
    }
878
879
    /**
880
     * Updates the database schema of the given classes by comparing the ClassMetadata
881
     * instances to the current database schema that is inspected.
882
     *
883
     * @param ClassMetadata[] $classes
884
     * @param bool            $saveMode If TRUE, only performs a partial update
885
     *                                  without dropping assets which are scheduled for deletion.
886
     */
887
    public function updateSchema(array $classes, $saveMode = false)
888
    {
889
        $updateSchemaSql = $this->getUpdateSchemaSql($classes, $saveMode);
890
        $conn            = $this->em->getConnection();
891
892
        foreach ($updateSchemaSql as $sql) {
893
            $conn->executeQuery($sql);
894
        }
895
    }
896
897
    /**
898
     * Gets the sequence of SQL statements that need to be performed in order
899
     * to bring the given class mappings in-synch with the relational schema.
900
     *
901
     * @param ClassMetadata[] $classes  The classes to consider.
902
     * @param bool            $saveMode If TRUE, only generates SQL for a partial update
903
     *                                  that does not include SQL for dropping assets which are scheduled for deletion.
904
     *
905
     * @return string[] The sequence of SQL statements.
906
     */
907 1
    public function getUpdateSchemaSql(array $classes, $saveMode = false)
908
    {
909 1
        $sm = $this->em->getConnection()->getSchemaManager();
910
911 1
        $fromSchema = $sm->createSchema();
912 1
        $toSchema   = $this->getSchemaFromMetadata($classes);
913
914 1
        $comparator = new Comparator();
915 1
        $schemaDiff = $comparator->compare($fromSchema, $toSchema);
916
917 1
        if ($saveMode) {
918
            return $schemaDiff->toSaveSql($this->platform);
919
        }
920
921 1
        return $schemaDiff->toSql($this->platform);
922
    }
923
}
924