Passed
Pull Request — 2.7 (#7875)
by Luís
07:27
created

SchemaTool   F

Complexity

Total Complexity 148

Size/Duplication

Total Lines 889
Duplicated Lines 0 %

Test Coverage

Coverage 87.63%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 148
eloc 375
c 2
b 0
f 0
dl 0
loc 889
ccs 340
cts 388
cp 0.8763
rs 2

19 Methods

Rating   Name   Duplication   Size   Complexity  
A dropSchema() 0 9 3
A createSchemaForComparison() 0 25 5
A gatherColumns() 0 13 5
A __construct() 0 5 1
A gatherColumnOptions() 0 10 2
A getUpdateSchemaSql() 0 13 2
C getDropSchemaSQL() 0 44 12
F gatherColumn() 0 55 17
A getDropDatabaseSQL() 0 9 1
A addDiscriminatorColumnDefinition() 0 21 5
A updateSchema() 0 7 2
A getDefiningClass() 0 22 6
F getSchemaFromMetadata() 0 223 43
B gatherRelationsSql() 0 57 10
A dropDatabase() 0 7 2
A getCreateSchemaSql() 0 5 1
F gatherRelationJoinColumns() 0 110 23
A createSchema() 0 10 3
A processingNotRequired() 0 7 5

How to fix   Complexity   

Complex Class

Complex classes like SchemaTool often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SchemaTool, and based on these observations, apply Extract Interface, too.

1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ORM\Tools;
21
22
use Doctrine\DBAL\Schema\AbstractAsset;
23
use Doctrine\DBAL\Schema\Comparator;
24
use Doctrine\DBAL\Schema\Index;
25
use Doctrine\DBAL\Schema\Schema;
26
use Doctrine\DBAL\Schema\Table;
27
use Doctrine\DBAL\Schema\Visitor\DropSchemaSqlCollector;
28
use Doctrine\DBAL\Schema\Visitor\RemoveNamespacedAssets;
29
use Doctrine\ORM\EntityManagerInterface;
30
use Doctrine\ORM\Mapping\ClassMetadata;
31
use Doctrine\ORM\ORMException;
32
use Doctrine\ORM\Tools\Event\GenerateSchemaTableEventArgs;
33
use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs;
34
35
/**
36
 * The SchemaTool is a tool to create/drop/update database schemas based on
37
 * <tt>ClassMetadata</tt> class descriptors.
38
 *
39
 * @link    www.doctrine-project.org
40
 * @since   2.0
41
 * @author  Guilherme Blanco <[email protected]>
42
 * @author  Jonathan Wage <[email protected]>
43
 * @author  Roman Borschel <[email protected]>
44
 * @author  Benjamin Eberlei <[email protected]>
45
 * @author  Stefano Rodriguez <[email protected]>
46
 */
47
class SchemaTool
48
{
49
    private const KNOWN_COLUMN_OPTIONS = ['comment', 'unsigned', 'fixed', 'default'];
50
51
    /**
52
     * @var \Doctrine\ORM\EntityManagerInterface
53
     */
54
    private $em;
55
56
    /**
57
     * @var \Doctrine\DBAL\Platforms\AbstractPlatform
58
     */
59
    private $platform;
60
61
    /**
62
     * The quote strategy.
63
     *
64
     * @var \Doctrine\ORM\Mapping\QuoteStrategy
65
     */
66
    private $quoteStrategy;
67
68
    /**
69
     * Initializes a new SchemaTool instance that uses the connection of the
70
     * provided EntityManager.
71
     *
72
     * @param \Doctrine\ORM\EntityManagerInterface $em
73
     */
74 1344
    public function __construct(EntityManagerInterface $em)
75
    {
76 1344
        $this->em               = $em;
77 1344
        $this->platform         = $em->getConnection()->getDatabasePlatform();
78 1344
        $this->quoteStrategy    = $em->getConfiguration()->getQuoteStrategy();
79 1344
    }
80
81
    /**
82
     * Creates the database schema for the given array of ClassMetadata instances.
83
     *
84
     * @param array $classes
85
     *
86
     * @return void
87
     *
88
     * @throws ToolsException
89
     */
90 307
    public function createSchema(array $classes)
91
    {
92 307
        $createSchemaSql = $this->getCreateSchemaSql($classes);
93 307
        $conn = $this->em->getConnection();
94
95 307
        foreach ($createSchemaSql as $sql) {
96
            try {
97 307
                $conn->executeQuery($sql);
98 90
            } catch (\Throwable $e) {
99 307
                throw ToolsException::schemaToolFailure($sql, $e);
100
            }
101
        }
102 217
    }
103
104
    /**
105
     * Gets the list of DDL statements that are required to create the database schema for
106
     * the given list of ClassMetadata instances.
107
     *
108
     * @param array $classes
109
     *
110
     * @return array The SQL statements needed to create the schema for the classes.
111
     */
112 307
    public function getCreateSchemaSql(array $classes)
113
    {
114 307
        $schema = $this->getSchemaFromMetadata($classes);
115
116 307
        return $schema->toSql($this->platform);
117
    }
118
119
    /**
120
     * Detects instances of ClassMetadata that don't need to be processed in the SchemaTool context.
121
     *
122
     * @param ClassMetadata $class
123
     * @param array         $processedClasses
124
     *
125
     * @return bool
126
     */
127 319
    private function processingNotRequired($class, array $processedClasses)
128
    {
129
        return (
130 319
            isset($processedClasses[$class->name]) ||
131 319
            $class->isMappedSuperclass ||
132 319
            $class->isEmbeddedClass ||
133 319
            ($class->isInheritanceTypeSingleTable() && $class->name != $class->rootEntityName)
134
        );
135
    }
136
137
    /**
138
     * Creates a Schema instance from a given set of metadata classes.
139
     *
140
     * @param array $classes
141
     *
142
     * @return Schema
143
     *
144
     * @throws \Doctrine\ORM\ORMException
145
     */
146 319
    public function getSchemaFromMetadata(array $classes)
147
    {
148
        // Reminder for processed classes, used for hierarchies
149 319
        $processedClasses       = [];
150 319
        $eventManager           = $this->em->getEventManager();
151 319
        $schemaManager          = $this->em->getConnection()->getSchemaManager();
152 319
        $metadataSchemaConfig   = $schemaManager->createSchemaConfig();
153
154 319
        $metadataSchemaConfig->setExplicitForeignKeyIndexes(false);
155 319
        $schema = new Schema([], [], $metadataSchemaConfig);
156
157 319
        $addedFks = [];
158 319
        $blacklistedFks = [];
159
160 319
        foreach ($classes as $class) {
161
            /** @var \Doctrine\ORM\Mapping\ClassMetadata $class */
162 319
            if ($this->processingNotRequired($class, $processedClasses)) {
163 39
                continue;
164
            }
165
166 319
            $table = $schema->createTable($this->quoteStrategy->getTableName($class, $this->platform));
167
168 319
            if ($class->isInheritanceTypeSingleTable()) {
169 36
                $this->gatherColumns($class, $table);
170 36
                $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
171
172
                // Add the discriminator column
173 36
                $this->addDiscriminatorColumnDefinition($class, $table);
174
175
                // Aggregate all the information from all classes in the hierarchy
176 36
                foreach ($class->parentClasses as $parentClassName) {
177
                    // Parent class information is already contained in this class
178
                    $processedClasses[$parentClassName] = true;
179
                }
180
181 36
                foreach ($class->subClasses as $subClassName) {
182 34
                    $subClass = $this->em->getClassMetadata($subClassName);
183 34
                    $this->gatherColumns($subClass, $table);
184 34
                    $this->gatherRelationsSql($subClass, $table, $schema, $addedFks, $blacklistedFks);
185 36
                    $processedClasses[$subClassName] = true;
186
                }
187 315
            } elseif ($class->isInheritanceTypeJoined()) {
188
                // Add all non-inherited fields as columns
189 62
                foreach ($class->fieldMappings as $fieldName => $mapping) {
190 62
                    if ( ! isset($mapping['inherited'])) {
191 62
                        $this->gatherColumn($class, $mapping, $table);
192
                    }
193
                }
194
195 62
                $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
196
197
                // Add the discriminator column only to the root table
198 62
                if ($class->name == $class->rootEntityName) {
199 62
                    $this->addDiscriminatorColumnDefinition($class, $table);
200
                } else {
201
                    // Add an ID FK column to child tables
202 61
                    $pkColumns           = [];
203 61
                    $inheritedKeyColumns = [];
204
205 61
                    foreach ($class->identifier as $identifierField) {
206 61
                        if (isset($class->fieldMappings[$identifierField]['inherited'])) {
207 61
                            $idMapping = $class->fieldMappings[$identifierField];
208 61
                            $this->gatherColumn($class, $idMapping, $table);
209 61
                            $columnName = $this->quoteStrategy->getColumnName(
210 61
                                $identifierField,
211 61
                                $class,
212 61
                                $this->platform
213
                            );
214
                            // TODO: This seems rather hackish, can we optimize it?
215 61
                            $table->getColumn($columnName)->setAutoincrement(false);
216
217 61
                            $pkColumns[] = $columnName;
218 61
                            $inheritedKeyColumns[] = $columnName;
219
220 61
                            continue;
221
                        }
222
223 2
                        if (isset($class->associationMappings[$identifierField]['inherited'])) {
224 1
                            $idMapping = $class->associationMappings[$identifierField];
225
226 1
                            $targetEntity = current(
227 1
                                array_filter(
228 1
                                    $classes,
229
                                    function (ClassMetadata $class) use ($idMapping) : bool {
230 1
                                        return $class->name === $idMapping['targetEntity'];
231 1
                                    }
232
                                )
233
                            );
234
235 1
                            foreach ($idMapping['joinColumns'] as $joinColumn) {
236 1
                                if (isset($targetEntity->fieldMappings[$joinColumn['referencedColumnName']])) {
237 1
                                    $columnName = $this->quoteStrategy->getJoinColumnName(
238 1
                                        $joinColumn,
239 1
                                        $class,
240 1
                                        $this->platform
241
                                    );
242
243 1
                                    $pkColumns[]           = $columnName;
244 2
                                    $inheritedKeyColumns[] = $columnName;
245
                                }
246
                            }
247
                        }
248
                    }
249
250 61
                    if ( ! empty($inheritedKeyColumns)) {
251
                        // Add a FK constraint on the ID column
252 61
                        $table->addForeignKeyConstraint(
253 61
                            $this->quoteStrategy->getTableName(
254 61
                                $this->em->getClassMetadata($class->rootEntityName),
255 61
                                $this->platform
256
                            ),
257 61
                            $inheritedKeyColumns,
258 61
                            $inheritedKeyColumns,
259 61
                            ['onDelete' => 'CASCADE']
260
                        );
261
                    }
262
263 61
                    if ( ! empty($pkColumns)) {
264 62
                        $table->setPrimaryKey($pkColumns);
265
                    }
266
                }
267 291
            } elseif ($class->isInheritanceTypeTablePerClass()) {
268
                throw ORMException::notSupported();
269
            } else {
270 291
                $this->gatherColumns($class, $table);
271 291
                $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
272
            }
273
274 319
            $pkColumns = [];
275
276 319
            foreach ($class->identifier as $identifierField) {
277 319
                if (isset($class->fieldMappings[$identifierField])) {
278 318
                    $pkColumns[] = $this->quoteStrategy->getColumnName($identifierField, $class, $this->platform);
279 34
                } elseif (isset($class->associationMappings[$identifierField])) {
280
                    /* @var $assoc \Doctrine\ORM\Mapping\OneToOne */
281 34
                    $assoc = $class->associationMappings[$identifierField];
282
283 34
                    foreach ($assoc['joinColumns'] as $joinColumn) {
284 319
                        $pkColumns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
285
                    }
286
                }
287
            }
288
289 319
            if ( ! $table->hasIndex('primary')) {
290 319
                $table->setPrimaryKey($pkColumns);
291
            }
292
293
            // there can be unique indexes automatically created for join column
294
            // if join column is also primary key we should keep only primary key on this column
295
            // so, remove indexes overruled by primary key
296 319
            $primaryKey = $table->getIndex('primary');
297
298 319
            foreach ($table->getIndexes() as $idxKey => $existingIndex) {
299 319
                if ($primaryKey->overrules($existingIndex)) {
300 319
                    $table->dropIndex($idxKey);
301
                }
302
            }
303
304 319
            if (isset($class->table['indexes'])) {
305 1
                foreach ($class->table['indexes'] as $indexName => $indexData) {
306 1
                    if ( ! isset($indexData['flags'])) {
307 1
                        $indexData['flags'] = [];
308
                    }
309
310 1
                    $table->addIndex($indexData['columns'], is_numeric($indexName) ? null : $indexName, (array) $indexData['flags'], $indexData['options'] ?? []);
311
                }
312
            }
313
314 319
            if (isset($class->table['uniqueConstraints'])) {
315 4
                foreach ($class->table['uniqueConstraints'] as $indexName => $indexData) {
316 4
                    $uniqIndex = new Index($indexName, $indexData['columns'], true, false, [], $indexData['options'] ?? []);
317
318 4
                    foreach ($table->getIndexes() as $tableIndexName => $tableIndex) {
319 4
                        if ($tableIndex->isFullfilledBy($uniqIndex)) {
320 3
                            $table->dropIndex($tableIndexName);
321 4
                            break;
322
                        }
323
                    }
324
325 4
                    $table->addUniqueIndex($indexData['columns'], is_numeric($indexName) ? null : $indexName, $indexData['options'] ?? []);
326
                }
327
            }
328
329 319
            if (isset($class->table['options'])) {
330 1
                foreach ($class->table['options'] as $key => $val) {
331 1
                    $table->addOption($key, $val);
332
                }
333
            }
334
335 319
            $processedClasses[$class->name] = true;
336
337 319
            if ($class->isIdGeneratorSequence() && $class->name == $class->rootEntityName) {
338
                $seqDef     = $class->sequenceGeneratorDefinition;
339
                $quotedName = $this->quoteStrategy->getSequenceName($seqDef, $class, $this->platform);
340
                if ( ! $schema->hasSequence($quotedName)) {
341
                    $schema->createSequence(
342
                        $quotedName,
343
                        $seqDef['allocationSize'],
344
                        $seqDef['initialValue']
345
                    );
346
                }
347
            }
348
349 319
            if ($eventManager->hasListeners(ToolEvents::postGenerateSchemaTable)) {
350 1
                $eventManager->dispatchEvent(
351 1
                    ToolEvents::postGenerateSchemaTable,
352 319
                    new GenerateSchemaTableEventArgs($class, $schema, $table)
353
                );
354
            }
355
        }
356
357 319
        if ( ! $this->platform->supportsSchemas() && ! $this->platform->canEmulateSchemas()) {
358 10
            $schema->visit(new RemoveNamespacedAssets());
359
        }
360
361 319
        if ($eventManager->hasListeners(ToolEvents::postGenerateSchema)) {
362 1
            $eventManager->dispatchEvent(
363 1
                ToolEvents::postGenerateSchema,
364 1
                new GenerateSchemaEventArgs($this->em, $schema)
365
            );
366
        }
367
368 319
        return $schema;
369
    }
370
371
    /**
372
     * Gets a portable column definition as required by the DBAL for the discriminator
373
     * column of a class.
374
     *
375
     * @param ClassMetadata $class
376
     * @param Table         $table
377
     *
378
     * @return void
379
     */
380 93
    private function addDiscriminatorColumnDefinition($class, Table $table)
381
    {
382 93
        $discrColumn = $class->discriminatorColumn;
383
384 93
        if ( ! isset($discrColumn['type']) ||
385 93
            (strtolower($discrColumn['type']) == 'string' && ! isset($discrColumn['length']))
386
        ) {
387 1
            $discrColumn['type'] = 'string';
388 1
            $discrColumn['length'] = 255;
389
        }
390
391
        $options = [
392 93
            'length'    => $discrColumn['length'] ?? null,
393
            'notnull'   => true
394
        ];
395
396 93
        if (isset($discrColumn['columnDefinition'])) {
397
            $options['columnDefinition'] = $discrColumn['columnDefinition'];
398
        }
399
400 93
        $table->addColumn($discrColumn['name'], $discrColumn['type'], $options);
401 93
    }
402
403
    /**
404
     * Gathers the column definitions as required by the DBAL of all field mappings
405
     * found in the given class.
406
     *
407
     * @param ClassMetadata $class
408
     * @param Table         $table
409
     *
410
     * @return void
411
     */
412 298
    private function gatherColumns($class, Table $table)
413
    {
414 298
        $pkColumns = [];
415
416 298
        foreach ($class->fieldMappings as $mapping) {
417 298
            if ($class->isInheritanceTypeSingleTable() && isset($mapping['inherited'])) {
418 34
                continue;
419
            }
420
421 298
            $this->gatherColumn($class, $mapping, $table);
422
423 298
            if ($class->isIdentifier($mapping['fieldName'])) {
424 298
                $pkColumns[] = $this->quoteStrategy->getColumnName($mapping['fieldName'], $class, $this->platform);
425
            }
426
        }
427 298
    }
428
429
    /**
430
     * Creates a column definition as required by the DBAL from an ORM field mapping definition.
431
     *
432
     * @param ClassMetadata $class   The class that owns the field mapping.
433
     * @param array         $mapping The field mapping.
434
     * @param Table         $table
435
     *
436
     * @return void
437
     */
438 319
    private function gatherColumn($class, array $mapping, Table $table)
439
    {
440 319
        $columnName = $this->quoteStrategy->getColumnName($mapping['fieldName'], $class, $this->platform);
441 319
        $columnType = $mapping['type'];
442
443 319
        $options = [];
444 319
        $options['length'] = $mapping['length'] ?? null;
445 319
        $options['notnull'] = isset($mapping['nullable']) ? ! $mapping['nullable'] : true;
446 319
        if ($class->isInheritanceTypeSingleTable() && $class->parentClasses) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $class->parentClasses of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
447 7
            $options['notnull'] = false;
448
        }
449
450 319
        $options['platformOptions'] = [];
451 319
        $options['platformOptions']['version'] = $class->isVersioned && $class->versionField === $mapping['fieldName'];
452
453 319
        if (strtolower($columnType) === 'string' && null === $options['length']) {
454 163
            $options['length'] = 255;
455
        }
456
457 319
        if (isset($mapping['precision'])) {
458 317
            $options['precision'] = $mapping['precision'];
459
        }
460
461 319
        if (isset($mapping['scale'])) {
462 317
            $options['scale'] = $mapping['scale'];
463
        }
464
465 319
        if (isset($mapping['default'])) {
466 32
            $options['default'] = $mapping['default'];
467
        }
468
469 319
        if (isset($mapping['columnDefinition'])) {
470 1
            $options['columnDefinition'] = $mapping['columnDefinition'];
471
        }
472
473
        // the 'default' option can be overwritten here
474 319
        $options = $this->gatherColumnOptions($mapping) + $options;
475
476 319
        if ($class->isIdGeneratorIdentity() && $class->getIdentifierFieldNames() == [$mapping['fieldName']]) {
477 272
            $options['autoincrement'] = true;
478
        }
479 319
        if ($class->isInheritanceTypeJoined() && $class->name !== $class->rootEntityName) {
480 61
            $options['autoincrement'] = false;
481
        }
482
483 319
        if ($table->hasColumn($columnName)) {
484
            // required in some inheritance scenarios
485
            $table->changeColumn($columnName, $options);
486
        } else {
487 319
            $table->addColumn($columnName, $columnType, $options);
488
        }
489
490 319
        $isUnique = $mapping['unique'] ?? false;
491 319
        if ($isUnique) {
492 18
            $table->addUniqueIndex([$columnName]);
493
        }
494 319
    }
495
496
    /**
497
     * Gathers the SQL for properly setting up the relations of the given class.
498
     * This includes the SQL for foreign key constraints and join tables.
499
     *
500
     * @param ClassMetadata $class
501
     * @param Table         $table
502
     * @param Schema        $schema
503
     * @param array         $addedFks
504
     * @param array         $blacklistedFks
505
     *
506
     * @return void
507
     *
508
     * @throws \Doctrine\ORM\ORMException
509
     */
510 319
    private function gatherRelationsSql($class, $table, $schema, &$addedFks, &$blacklistedFks)
511
    {
512 319
        foreach ($class->associationMappings as $id => $mapping) {
513 217
            if (isset($mapping['inherited']) && ! \in_array($id, $class->identifier, true)) {
514 21
                continue;
515
            }
516
517 217
            $foreignClass = $this->em->getClassMetadata($mapping['targetEntity']);
518
519 217
            if ($mapping['type'] & ClassMetadata::TO_ONE && $mapping['isOwningSide']) {
520 193
                $primaryKeyColumns = []; // PK is unnecessary for this relation-type
521
522 193
                $this->gatherRelationJoinColumns(
523 193
                    $mapping['joinColumns'],
524 193
                    $table,
525 193
                    $foreignClass,
526 193
                    $mapping,
527 193
                    $primaryKeyColumns,
528 193
                    $addedFks,
529 193
                    $blacklistedFks
530
                );
531 159
            } elseif ($mapping['type'] == ClassMetadata::ONE_TO_MANY && $mapping['isOwningSide']) {
532
                //... create join table, one-many through join table supported later
533
                throw ORMException::notSupported();
534 159
            } elseif ($mapping['type'] == ClassMetadata::MANY_TO_MANY && $mapping['isOwningSide']) {
535
                // create join table
536 49
                $joinTable = $mapping['joinTable'];
537
538 49
                $theJoinTable = $schema->createTable(
539 49
                    $this->quoteStrategy->getJoinTableName($mapping, $foreignClass, $this->platform)
540
                );
541
542 49
                $primaryKeyColumns = [];
543
544
                // Build first FK constraint (relation table => source table)
545 49
                $this->gatherRelationJoinColumns(
546 49
                    $joinTable['joinColumns'],
547 49
                    $theJoinTable,
548 49
                    $class,
549 49
                    $mapping,
550 49
                    $primaryKeyColumns,
551 49
                    $addedFks,
552 49
                    $blacklistedFks
553
                );
554
555
                // Build second FK constraint (relation table => target table)
556 49
                $this->gatherRelationJoinColumns(
557 49
                    $joinTable['inverseJoinColumns'],
558 49
                    $theJoinTable,
559 49
                    $foreignClass,
560 49
                    $mapping,
561 49
                    $primaryKeyColumns,
562 49
                    $addedFks,
563 49
                    $blacklistedFks
564
                );
565
566 217
                $theJoinTable->setPrimaryKey($primaryKeyColumns);
567
            }
568
        }
569 319
    }
570
571
    /**
572
     * Gets the class metadata that is responsible for the definition of the referenced column name.
573
     *
574
     * Previously this was a simple task, but with DDC-117 this problem is actually recursive. If its
575
     * not a simple field, go through all identifier field names that are associations recursively and
576
     * find that referenced column name.
577
     *
578
     * TODO: Is there any way to make this code more pleasing?
579
     *
580
     * @param ClassMetadata $class
581
     * @param string        $referencedColumnName
582
     *
583
     * @return array (ClassMetadata, referencedFieldName)
584
     */
585 217
    private function getDefiningClass($class, $referencedColumnName)
586
    {
587 217
        $referencedFieldName = $class->getFieldName($referencedColumnName);
588
589 217
        if ($class->hasField($referencedFieldName)) {
590 217
            return [$class, $referencedFieldName];
591
        }
592
593 10
        if (in_array($referencedColumnName, $class->getIdentifierColumnNames())) {
594
            // it seems to be an entity as foreign key
595 10
            foreach ($class->getIdentifierFieldNames() as $fieldName) {
596 10
                if ($class->hasAssociation($fieldName)
597 10
                    && $class->getSingleAssociationJoinColumnName($fieldName) == $referencedColumnName) {
598 10
                    return $this->getDefiningClass(
599 10
                        $this->em->getClassMetadata($class->associationMappings[$fieldName]['targetEntity']),
600 10
                        $class->getSingleAssociationReferencedJoinColumnName($fieldName)
601
                    );
602
                }
603
            }
604
        }
605
606
        return null;
607
    }
608
609
    /**
610
     * Gathers columns and fk constraints that are required for one part of relationship.
611
     *
612
     * @param array         $joinColumns
613
     * @param Table         $theJoinTable
614
     * @param ClassMetadata $class
615
     * @param array         $mapping
616
     * @param array         $primaryKeyColumns
617
     * @param array         $addedFks
618
     * @param array         $blacklistedFks
619
     *
620
     * @return void
621
     *
622
     * @throws \Doctrine\ORM\ORMException
623
     */
624 217
    private function gatherRelationJoinColumns(
625
        $joinColumns,
626
        $theJoinTable,
627
        $class,
628
        $mapping,
629
        &$primaryKeyColumns,
630
        &$addedFks,
631
        &$blacklistedFks
632
    )
633
    {
634 217
        $localColumns       = [];
635 217
        $foreignColumns     = [];
636 217
        $fkOptions          = [];
637 217
        $foreignTableName   = $this->quoteStrategy->getTableName($class, $this->platform);
638 217
        $uniqueConstraints  = [];
639
640 217
        foreach ($joinColumns as $joinColumn) {
641
642 217
            list($definingClass, $referencedFieldName) = $this->getDefiningClass(
643 217
                $class,
644 217
                $joinColumn['referencedColumnName']
645
            );
646
647 217
            if ( ! $definingClass) {
648
                throw new \Doctrine\ORM\ORMException(
649
                    'Column name `' . $joinColumn['referencedColumnName'] . '` referenced for relation from '
650
                    . $mapping['sourceEntity'] . ' towards ' . $mapping['targetEntity'] . ' does not exist.'
651
                );
652
            }
653
654 217
            $quotedColumnName    = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
655 217
            $quotedRefColumnName = $this->quoteStrategy->getReferencedJoinColumnName(
656 217
                $joinColumn,
657 217
                $class,
658 217
                $this->platform
659
            );
660
661 217
            $primaryKeyColumns[] = $quotedColumnName;
662 217
            $localColumns[]      = $quotedColumnName;
663 217
            $foreignColumns[]    = $quotedRefColumnName;
664
665 217
            if ( ! $theJoinTable->hasColumn($quotedColumnName)) {
666
                // Only add the column to the table if it does not exist already.
667
                // It might exist already if the foreign key is mapped into a regular
668
                // property as well.
669
670 214
                $fieldMapping = $definingClass->getFieldMapping($referencedFieldName);
671
672 214
                $columnDef = null;
673 214
                if (isset($joinColumn['columnDefinition'])) {
674
                    $columnDef = $joinColumn['columnDefinition'];
675 214
                } elseif (isset($fieldMapping['columnDefinition'])) {
676 1
                    $columnDef = $fieldMapping['columnDefinition'];
677
                }
678
679 214
                $columnOptions = ['notnull' => false, 'columnDefinition' => $columnDef];
680
681 214
                if (isset($joinColumn['nullable'])) {
682 134
                    $columnOptions['notnull'] = ! $joinColumn['nullable'];
683
                }
684
685 214
                $columnOptions = $columnOptions + $this->gatherColumnOptions($fieldMapping);
686
687 214
                if ($fieldMapping['type'] == "string" && isset($fieldMapping['length'])) {
688 4
                    $columnOptions['length'] = $fieldMapping['length'];
689 213
                } elseif ($fieldMapping['type'] == "decimal") {
690
                    $columnOptions['scale'] = $fieldMapping['scale'];
691
                    $columnOptions['precision'] = $fieldMapping['precision'];
692
                }
693
694 214
                $theJoinTable->addColumn($quotedColumnName, $fieldMapping['type'], $columnOptions);
695
            }
696
697 217
            if (isset($joinColumn['unique']) && $joinColumn['unique'] == true) {
698 65
                $uniqueConstraints[] = ['columns' => [$quotedColumnName]];
699
            }
700
701 217
            if (isset($joinColumn['onDelete'])) {
702 217
                $fkOptions['onDelete'] = $joinColumn['onDelete'];
703
            }
704
        }
705
706
        // Prefer unique constraints over implicit simple indexes created for foreign keys.
707
        // Also avoids index duplication.
708 217
        foreach ($uniqueConstraints as $indexName => $unique) {
709 65
            $theJoinTable->addUniqueIndex($unique['columns'], is_numeric($indexName) ? null : $indexName);
710
        }
711
712 217
        $compositeName = $theJoinTable->getName().'.'.implode('', $localColumns);
713 217
        if (isset($addedFks[$compositeName])
714 1
            && ($foreignTableName != $addedFks[$compositeName]['foreignTableName']
715 217
            || 0 < count(array_diff($foreignColumns, $addedFks[$compositeName]['foreignColumns'])))
716
        ) {
717 1
            foreach ($theJoinTable->getForeignKeys() as $fkName => $key) {
718 1
                if (0 === count(array_diff($key->getLocalColumns(), $localColumns))
719 1
                    && (($key->getForeignTableName() != $foreignTableName)
720 1
                    || 0 < count(array_diff($key->getForeignColumns(), $foreignColumns)))
721
                ) {
722 1
                    $theJoinTable->removeForeignKey($fkName);
723 1
                    break;
724
                }
725
            }
726 1
            $blacklistedFks[$compositeName] = true;
727 217
        } elseif ( ! isset($blacklistedFks[$compositeName])) {
728 217
            $addedFks[$compositeName] = ['foreignTableName' => $foreignTableName, 'foreignColumns' => $foreignColumns];
729 217
            $theJoinTable->addUnnamedForeignKeyConstraint(
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\DBAL\Schema\Tab...dForeignKeyConstraint() has been deprecated: Use {@link addForeignKeyConstraint} ( Ignorable by Annotation )

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

729
            /** @scrutinizer ignore-deprecated */ $theJoinTable->addUnnamedForeignKeyConstraint(

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
730 217
                $foreignTableName,
731 217
                $localColumns,
732 217
                $foreignColumns,
733 217
                $fkOptions
734
            );
735
        }
736 217
    }
737
738
    /**
739
     * @param mixed[] $mapping
740
     *
741
     * @return mixed[]
742
     */
743 319
    private function gatherColumnOptions(array $mapping) : array
744
    {
745 319
        if (! isset($mapping['options'])) {
746 319
            return [];
747
        }
748
749 4
        $options = array_intersect_key($mapping['options'], array_flip(self::KNOWN_COLUMN_OPTIONS));
750 4
        $options['customSchemaOptions'] = array_diff_key($mapping['options'], $options);
751
752 4
        return $options;
753
    }
754
755
    /**
756
     * Drops the database schema for the given classes.
757
     *
758
     * In any way when an exception is thrown it is suppressed since drop was
759
     * issued for all classes of the schema and some probably just don't exist.
760
     *
761
     * @param array $classes
762
     *
763
     * @return void
764
     */
765 6
    public function dropSchema(array $classes)
766
    {
767 6
        $dropSchemaSql = $this->getDropSchemaSQL($classes);
768 6
        $conn = $this->em->getConnection();
769
770 6
        foreach ($dropSchemaSql as $sql) {
771
            try {
772 6
                $conn->executeQuery($sql);
773 6
            } catch (\Throwable $e) {
774
                // ignored
775
            }
776
        }
777 6
    }
778
779
    /**
780
     * Drops all elements in the database of the current connection.
781
     *
782
     * @return void
783
     */
784
    public function dropDatabase()
785
    {
786
        $dropSchemaSql = $this->getDropDatabaseSQL();
787
        $conn = $this->em->getConnection();
788
789
        foreach ($dropSchemaSql as $sql) {
790
            $conn->executeQuery($sql);
791
        }
792
    }
793
794
    /**
795
     * Gets the SQL needed to drop the database schema for the connections database.
796
     *
797
     * @return array
798
     */
799
    public function getDropDatabaseSQL()
800
    {
801
        $sm = $this->em->getConnection()->getSchemaManager();
802
        $schema = $sm->createSchema();
803
804
        $visitor = new DropSchemaSqlCollector($this->platform);
805
        $schema->visit($visitor);
806
807
        return $visitor->getQueries();
808
    }
809
810
    /**
811
     * Gets SQL to drop the tables defined by the passed classes.
812
     *
813
     * @param array $classes
814
     *
815
     * @return array
816
     */
817 6
    public function getDropSchemaSQL(array $classes)
818
    {
819 6
        $visitor = new DropSchemaSqlCollector($this->platform);
820 6
        $schema = $this->getSchemaFromMetadata($classes);
821
822 6
        $sm = $this->em->getConnection()->getSchemaManager();
823 6
        $fullSchema = $sm->createSchema();
824
825 6
        foreach ($fullSchema->getTables() as $table) {
826 6
            if ( ! $schema->hasTable($table->getName())) {
827 6
                foreach ($table->getForeignKeys() as $foreignKey) {
828
                    /* @var $foreignKey \Doctrine\DBAL\Schema\ForeignKeyConstraint */
829
                    if ($schema->hasTable($foreignKey->getForeignTableName())) {
830 6
                        $visitor->acceptForeignKey($table, $foreignKey);
831
                    }
832
                }
833
            } else {
834 6
                $visitor->acceptTable($table);
835 6
                foreach ($table->getForeignKeys() as $foreignKey) {
836 6
                    $visitor->acceptForeignKey($table, $foreignKey);
837
                }
838
            }
839
        }
840
841 6
        if ($this->platform->supportsSequences()) {
842
            foreach ($schema->getSequences() as $sequence) {
843
                $visitor->acceptSequence($sequence);
844
            }
845
846
            foreach ($schema->getTables() as $table) {
847
                /* @var $sequence Table */
848
                if ($table->hasPrimaryKey()) {
849
                    $columns = $table->getPrimaryKey()->getColumns();
850
                    if (count($columns) == 1) {
851
                        $checkSequence = $table->getName() . '_' . $columns[0] . '_seq';
852
                        if ($fullSchema->hasSequence($checkSequence)) {
853
                            $visitor->acceptSequence($fullSchema->getSequence($checkSequence));
854
                        }
855
                    }
856
                }
857
            }
858
        }
859
860 6
        return $visitor->getQueries();
861
    }
862
863
    /**
864
     * Updates the database schema of the given classes by comparing the ClassMetadata
865
     * instances to the current database schema that is inspected.
866
     *
867
     * @param array   $classes
868
     * @param boolean $saveMode If TRUE, only performs a partial update
869
     *                          without dropping assets which are scheduled for deletion.
870
     *
871
     * @return void
872
     */
873
    public function updateSchema(array $classes, $saveMode = false)
874
    {
875
        $updateSchemaSql = $this->getUpdateSchemaSql($classes, $saveMode);
876
        $conn = $this->em->getConnection();
877
878
        foreach ($updateSchemaSql as $sql) {
879
            $conn->executeQuery($sql);
880
        }
881
    }
882
883
    /**
884
     * Gets the sequence of SQL statements that need to be performed in order
885
     * to bring the given class mappings in-synch with the relational schema.
886
     *
887
     * @param array   $classes  The classes to consider.
888
     * @param boolean $saveMode If TRUE, only generates SQL for a partial update
889
     *                          that does not include SQL for dropping assets which are scheduled for deletion.
890
     *
891
     * @return array The sequence of SQL statements.
892
     */
893 4
    public function getUpdateSchemaSql(array $classes, $saveMode = false)
894
    {
895 4
        $toSchema = $this->getSchemaFromMetadata($classes);
896 4
        $fromSchema = $this->createSchemaForComparison($toSchema);
897
898 4
        $comparator = new Comparator();
899 4
        $schemaDiff = $comparator->compare($fromSchema, $toSchema);
900
901 4
        if ($saveMode) {
902
            return $schemaDiff->toSaveSql($this->platform);
903
        }
904
905 4
        return $schemaDiff->toSql($this->platform);
906
    }
907
908
    /**
909
     * Creates the schema from the database, ensuring tables from the target schema are whitelisted for comparison.
910
     */
911 4
    private function createSchemaForComparison(Schema $toSchema) : Schema
912
    {
913 4
        $connection    = $this->em->getConnection();
914 4
        $schemaManager = $connection->getSchemaManager();
915
916
        // backup schema assets filter
917 4
        $config         = $connection->getConfiguration();
918 4
        $previousFilter = $config->getSchemaAssetsFilter();
919
920 4
        if ($previousFilter === null) {
921 2
            return $schemaManager->createSchema();
922
        }
923
924
        // whitelist assets we already know about in $toSchema, use the existing filter otherwise
925
        $config->setSchemaAssetsFilter(static function ($asset) use ($previousFilter, $toSchema) : bool {
926 2
            $assetName = $asset instanceof AbstractAsset ? $asset->getName() : $asset;
927
928 2
            return $toSchema->hasTable($assetName) || $toSchema->hasSequence($assetName) || $previousFilter($asset);
929 2
        });
930
931
        try {
932 2
            return $schemaManager->createSchema();
933
        } finally {
934
            // restore schema assets filter
935 2
            $config->setSchemaAssetsFilter($previousFilter);
936
        }
937
    }
938
}
939