Failed Conditions
Push — master ( ddb3cd...4476ec )
by Marco
11:47
created

SchemaTool::gatherColumns()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 6

Importance

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