Failed Conditions
Pull Request — 2.6 (#7235)
by Aleksey
09:09
created

SchemaTool   F

Complexity

Total Complexity 147

Size/Duplication

Total Lines 870
Duplicated Lines 0 %

Test Coverage

Coverage 86.72%

Importance

Changes 0
Metric Value
dl 0
loc 870
ccs 333
cts 384
cp 0.8672
rs 1.263
c 0
b 0
f 0
wmc 147

18 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A getCreateSchemaSql() 0 5 1
B processingNotRequired() 0 7 5
A createSchema() 0 10 3
A dropSchema() 0 9 3
B gatherColumns() 0 13 5
A getUpdateSchemaSql() 0 15 2
C getDropSchemaSQL() 0 44 12
F gatherColumn() 0 66 20
A getDropDatabaseSQL() 0 9 1
B addDiscriminatorColumnDefinition() 0 21 5
A updateSchema() 0 7 2
B getDefiningClass() 0 22 6
C gatherRelationsSql() 0 57 10
A dropDatabase() 0 7 2
F gatherRelationJoinColumns() 0 112 24
F getSchemaFromMetadata() 0 223 43
A getNamespaces() 0 8 2

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\ORM\ORMException;
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\Tools\Event\GenerateSchemaTableEventArgs;
32
use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs;
33
34
/**
35
 * The SchemaTool is a tool to create/drop/update database schemas based on
36
 * <tt>ClassMetadata</tt> class descriptors.
37
 *
38
 * @link    www.doctrine-project.org
39
 * @since   2.0
40
 * @author  Guilherme Blanco <[email protected]>
41
 * @author  Jonathan Wage <[email protected]>
42
 * @author  Roman Borschel <[email protected]>
43
 * @author  Benjamin Eberlei <[email protected]>
44
 * @author  Stefano Rodriguez <[email protected]>
45
 */
46
class SchemaTool
47
{
48
    /**
49
     * @var \Doctrine\ORM\EntityManagerInterface
50
     */
51
    private $em;
52
53
    /**
54
     * @var \Doctrine\DBAL\Platforms\AbstractPlatform
55
     */
56
    private $platform;
57
58
    /**
59
     * The quote strategy.
60
     *
61
     * @var \Doctrine\ORM\Mapping\QuoteStrategy
62
     */
63
    private $quoteStrategy;
64
65
    /**
66
     * Initializes a new SchemaTool instance that uses the connection of the
67
     * provided EntityManager.
68
     *
69
     * @param \Doctrine\ORM\EntityManagerInterface $em
70
     */
71 1309
    public function __construct(EntityManagerInterface $em)
72
    {
73 1309
        $this->em               = $em;
74 1309
        $this->platform         = $em->getConnection()->getDatabasePlatform();
75 1309
        $this->quoteStrategy    = $em->getConfiguration()->getQuoteStrategy();
76 1309
    }
77
78
    /**
79
     * Creates the database schema for the given array of ClassMetadata instances.
80
     *
81
     * @param array $classes
82
     *
83
     * @return void
84
     *
85
     * @throws ToolsException
86
     */
87 293
    public function createSchema(array $classes)
88
    {
89 293
        $createSchemaSql = $this->getCreateSchemaSql($classes);
90 293
        $conn = $this->em->getConnection();
91
92 293
        foreach ($createSchemaSql as $sql) {
93
            try {
94 293
                $conn->executeQuery($sql);
95 90
            } catch (\Throwable $e) {
96 293
                throw ToolsException::schemaToolFailure($sql, $e);
97
            }
98
        }
99 203
    }
100
101
    /**
102
     * Gets the list of DDL statements that are required to create the database schema for
103
     * the given list of ClassMetadata instances.
104
     *
105
     * @param array $classes
106
     *
107
     * @return array The SQL statements needed to create the schema for the classes.
108
     */
109 293
    public function getCreateSchemaSql(array $classes)
110
    {
111 293
        $schema = $this->getSchemaFromMetadata($classes);
112
113 293
        return $schema->toSql($this->platform);
114
    }
115
116
    /**
117
     * Detects instances of ClassMetadata that don't need to be processed in the SchemaTool context.
118
     *
119
     * @param ClassMetadata $class
120
     * @param array         $processedClasses
121
     *
122
     * @return bool
123
     */
124 303
    private function processingNotRequired($class, array $processedClasses)
125
    {
126
        return (
127 303
            isset($processedClasses[$class->name]) ||
128 303
            $class->isMappedSuperclass ||
129 303
            $class->isEmbeddedClass ||
130 303
            ($class->isInheritanceTypeSingleTable() && $class->name != $class->rootEntityName)
131
        );
132
    }
133
134
    /**
135
     * Creates a Schema instance from a given set of metadata classes.
136
     *
137
     * @param array $classes
138
     *
139
     * @return Schema
140
     *
141
     * @throws \Doctrine\ORM\ORMException
142
     */
143 303
    public function getSchemaFromMetadata(array $classes)
144
    {
145
        // Reminder for processed classes, used for hierarchies
146 303
        $processedClasses       = [];
147 303
        $eventManager           = $this->em->getEventManager();
148 303
        $schemaManager          = $this->em->getConnection()->getSchemaManager();
149 303
        $metadataSchemaConfig   = $schemaManager->createSchemaConfig();
150
151 303
        $metadataSchemaConfig->setExplicitForeignKeyIndexes(false);
152 303
        $schema = new Schema([], [], $metadataSchemaConfig, $this->getNamespaces());
153
154 303
        $addedFks = [];
155 303
        $blacklistedFks = [];
156
157 303
        foreach ($classes as $class) {
158
            /** @var \Doctrine\ORM\Mapping\ClassMetadata $class */
159 303
            if ($this->processingNotRequired($class, $processedClasses)) {
160 39
                continue;
161
            }
162
163 303
            $table = $schema->createTable($this->quoteStrategy->getTableName($class, $this->platform));
164
165 303
            if ($class->isInheritanceTypeSingleTable()) {
166 36
                $this->gatherColumns($class, $table);
167 36
                $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
168
169
                // Add the discriminator column
170 36
                $this->addDiscriminatorColumnDefinition($class, $table);
171
172
                // Aggregate all the information from all classes in the hierarchy
173 36
                foreach ($class->parentClasses as $parentClassName) {
174
                    // Parent class information is already contained in this class
175
                    $processedClasses[$parentClassName] = true;
176
                }
177
178 36
                foreach ($class->subClasses as $subClassName) {
179 34
                    $subClass = $this->em->getClassMetadata($subClassName);
180 34
                    $this->gatherColumns($subClass, $table);
181 34
                    $this->gatherRelationsSql($subClass, $table, $schema, $addedFks, $blacklistedFks);
182 36
                    $processedClasses[$subClassName] = true;
183
                }
184 299
            } elseif ($class->isInheritanceTypeJoined()) {
185
                // Add all non-inherited fields as columns
186 61
                foreach ($class->fieldMappings as $fieldName => $mapping) {
187 61
                    if ( ! isset($mapping['inherited'])) {
188 61
                        $this->gatherColumn($class, $mapping, $table);
189
                    }
190
                }
191
192 61
                $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
193
194
                // Add the discriminator column only to the root table
195 61
                if ($class->name == $class->rootEntityName) {
196 61
                    $this->addDiscriminatorColumnDefinition($class, $table);
197
                } else {
198
                    // Add an ID FK column to child tables
199 60
                    $pkColumns           = [];
200 60
                    $inheritedKeyColumns = [];
201
202 60
                    foreach ($class->identifier as $identifierField) {
203 60
                        if (isset($class->fieldMappings[$identifierField]['inherited'])) {
204 60
                            $idMapping = $class->fieldMappings[$identifierField];
205 60
                            $this->gatherColumn($class, $idMapping, $table);
206 60
                            $columnName = $this->quoteStrategy->getColumnName(
207 60
                                $identifierField,
208 60
                                $class,
209 60
                                $this->platform
210
                            );
211
                            // TODO: This seems rather hackish, can we optimize it?
212 60
                            $table->getColumn($columnName)->setAutoincrement(false);
213
214 60
                            $pkColumns[] = $columnName;
215 60
                            $inheritedKeyColumns[] = $columnName;
216
217 60
                            continue;
218
                        }
219
220 2
                        if (isset($class->associationMappings[$identifierField]['inherited'])) {
221 1
                            $idMapping = $class->associationMappings[$identifierField];
222
223 1
                            $targetEntity = current(
224 1
                                array_filter(
225 1
                                    $classes,
226 1
                                    function (ClassMetadata $class) use ($idMapping) : bool {
227 1
                                        return $class->name === $idMapping['targetEntity'];
228 1
                                    }
229
                                )
230
                            );
231
232 1
                            foreach ($idMapping['joinColumns'] as $joinColumn) {
233 1
                                if (isset($targetEntity->fieldMappings[$joinColumn['referencedColumnName']])) {
234 1
                                    $columnName = $this->quoteStrategy->getJoinColumnName(
235 1
                                        $joinColumn,
236 1
                                        $class,
237 1
                                        $this->platform
238
                                    );
239
240 1
                                    $pkColumns[]           = $columnName;
241 2
                                    $inheritedKeyColumns[] = $columnName;
242
                                }
243
                            }
244
                        }
245
                    }
246
247 60
                    if ( ! empty($inheritedKeyColumns)) {
248
                        // Add a FK constraint on the ID column
249 60
                        $table->addForeignKeyConstraint(
250 60
                            $this->quoteStrategy->getTableName(
251 60
                                $this->em->getClassMetadata($class->rootEntityName),
252 60
                                $this->platform
253
                            ),
254 60
                            $inheritedKeyColumns,
255 60
                            $inheritedKeyColumns,
256 60
                            ['onDelete' => 'CASCADE']
257
                        );
258
                    }
259
260 60
                    if ( ! empty($pkColumns)) {
261 61
                        $table->setPrimaryKey($pkColumns);
262
                    }
263
                }
264 276
            } elseif ($class->isInheritanceTypeTablePerClass()) {
265
                throw ORMException::notSupported();
266
            } else {
267 276
                $this->gatherColumns($class, $table);
268 276
                $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
269
            }
270
271 303
            $pkColumns = [];
272
273 303
            foreach ($class->identifier as $identifierField) {
274 303
                if (isset($class->fieldMappings[$identifierField])) {
275 302
                    $pkColumns[] = $this->quoteStrategy->getColumnName($identifierField, $class, $this->platform);
276 34
                } elseif (isset($class->associationMappings[$identifierField])) {
277
                    /* @var $assoc \Doctrine\ORM\Mapping\OneToOne */
278 34
                    $assoc = $class->associationMappings[$identifierField];
279
280 34
                    foreach ($assoc['joinColumns'] as $joinColumn) {
281 303
                        $pkColumns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
282
                    }
283
                }
284
            }
285
286 303
            if ( ! $table->hasIndex('primary')) {
287 303
                $table->setPrimaryKey($pkColumns);
288
            }
289
290
            // there can be unique indexes automatically created for join column
291
            // if join column is also primary key we should keep only primary key on this column
292
            // so, remove indexes overruled by primary key
293 303
            $primaryKey = $table->getIndex('primary');
294
295 303
            foreach ($table->getIndexes() as $idxKey => $existingIndex) {
296 303
                if ($primaryKey->overrules($existingIndex)) {
297 303
                    $table->dropIndex($idxKey);
298
                }
299
            }
300
301 303
            if (isset($class->table['indexes'])) {
302 1
                foreach ($class->table['indexes'] as $indexName => $indexData) {
303 1
                    if ( ! isset($indexData['flags'])) {
304 1
                        $indexData['flags'] = [];
305
                    }
306
307 1
                    $table->addIndex($indexData['columns'], is_numeric($indexName) ? null : $indexName, (array) $indexData['flags'], $indexData['options'] ?? []);
308
                }
309
            }
310
311 303
            if (isset($class->table['uniqueConstraints'])) {
312 4
                foreach ($class->table['uniqueConstraints'] as $indexName => $indexData) {
313 4
                    $uniqIndex = new Index($indexName, $indexData['columns'], true, false, [], $indexData['options'] ?? []);
314
315 4
                    foreach ($table->getIndexes() as $tableIndexName => $tableIndex) {
316 4
                        if ($tableIndex->isFullfilledBy($uniqIndex)) {
317 3
                            $table->dropIndex($tableIndexName);
318 4
                            break;
319
                        }
320
                    }
321
322 4
                    $table->addUniqueIndex($indexData['columns'], is_numeric($indexName) ? null : $indexName, $indexData['options'] ?? []);
323
                }
324
            }
325
326 303
            if (isset($class->table['options'])) {
327 1
                foreach ($class->table['options'] as $key => $val) {
328 1
                    $table->addOption($key, $val);
329
                }
330
            }
331
332 303
            $processedClasses[$class->name] = true;
333
334 303
            if ($class->isIdGeneratorSequence() && $class->name == $class->rootEntityName) {
335
                $seqDef     = $class->sequenceGeneratorDefinition;
336
                $quotedName = $this->quoteStrategy->getSequenceName($seqDef, $class, $this->platform);
337
                if ( ! $schema->hasSequence($quotedName)) {
338
                    $schema->createSequence(
339
                        $quotedName,
340
                        $seqDef['allocationSize'],
341
                        $seqDef['initialValue']
342
                    );
343
                }
344
            }
345
346 303
            if ($eventManager->hasListeners(ToolEvents::postGenerateSchemaTable)) {
347 1
                $eventManager->dispatchEvent(
348 1
                    ToolEvents::postGenerateSchemaTable,
349 303
                    new GenerateSchemaTableEventArgs($class, $schema, $table)
350
                );
351
            }
352
        }
353
354 303
        if ( ! $this->platform->supportsSchemas() && ! $this->platform->canEmulateSchemas()) {
355 9
            $schema->visit(new RemoveNamespacedAssets());
356
        }
357
358 303
        if ($eventManager->hasListeners(ToolEvents::postGenerateSchema)) {
359 1
            $eventManager->dispatchEvent(
360 1
                ToolEvents::postGenerateSchema,
361 1
                new GenerateSchemaEventArgs($this->em, $schema)
362
            );
363
        }
364
365 303
        return $schema;
366
    }
367
368
    /**
369
     * Gets a portable column definition as required by the DBAL for the discriminator
370
     * column of a class.
371
     *
372
     * @param ClassMetadata $class
373
     * @param Table         $table
374
     *
375
     * @return void
376
     */
377 92
    private function addDiscriminatorColumnDefinition($class, Table $table)
378
    {
379 92
        $discrColumn = $class->discriminatorColumn;
380
381 92
        if ( ! isset($discrColumn['type']) ||
382 92
            (strtolower($discrColumn['type']) == 'string' && ! isset($discrColumn['length']))
383
        ) {
384 1
            $discrColumn['type'] = 'string';
385 1
            $discrColumn['length'] = 255;
386
        }
387
388
        $options = [
389 92
            'length'    => $discrColumn['length'] ?? null,
390
            'notnull'   => true
391
        ];
392
393 92
        if (isset($discrColumn['columnDefinition'])) {
394
            $options['columnDefinition'] = $discrColumn['columnDefinition'];
395
        }
396
397 92
        $table->addColumn($discrColumn['name'], $discrColumn['type'], $options);
398 92
    }
399
400
    /**
401
     * Gathers the column definitions as required by the DBAL of all field mappings
402
     * found in the given class.
403
     *
404
     * @param ClassMetadata $class
405
     * @param Table         $table
406
     *
407
     * @return void
408
     */
409 283
    private function gatherColumns($class, Table $table)
410
    {
411 283
        $pkColumns = [];
412
413 283
        foreach ($class->fieldMappings as $mapping) {
414 283
            if ($class->isInheritanceTypeSingleTable() && isset($mapping['inherited'])) {
415 34
                continue;
416
            }
417
418 283
            $this->gatherColumn($class, $mapping, $table);
419
420 283
            if ($class->isIdentifier($mapping['fieldName'])) {
421 283
                $pkColumns[] = $this->quoteStrategy->getColumnName($mapping['fieldName'], $class, $this->platform);
422
            }
423
        }
424 283
    }
425
426
    /**
427
     * Creates a column definition as required by the DBAL from an ORM field mapping definition.
428
     *
429
     * @param ClassMetadata $class   The class that owns the field mapping.
430
     * @param array         $mapping The field mapping.
431
     * @param Table         $table
432
     *
433
     * @return void
434
     */
435 303
    private function gatherColumn($class, array $mapping, Table $table)
436
    {
437 303
        $columnName = $this->quoteStrategy->getColumnName($mapping['fieldName'], $class, $this->platform);
438 303
        $columnType = $mapping['type'];
439
440 303
        $options = [];
441 303
        $options['length'] = $mapping['length'] ?? null;
442 303
        $options['notnull'] = isset($mapping['nullable']) ? ! $mapping['nullable'] : true;
443 303
        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...
444 7
            $options['notnull'] = false;
445
        }
446
447 303
        $options['platformOptions'] = [];
448 303
        $options['platformOptions']['version'] = $class->isVersioned && $class->versionField === $mapping['fieldName'];
449
450 303
        if (strtolower($columnType) === 'string' && null === $options['length']) {
451 159
            $options['length'] = 255;
452
        }
453
454 303
        if (isset($mapping['precision'])) {
455 301
            $options['precision'] = $mapping['precision'];
456
        }
457
458 303
        if (isset($mapping['scale'])) {
459 301
            $options['scale'] = $mapping['scale'];
460
        }
461
462 303
        if (isset($mapping['default'])) {
463 31
            $options['default'] = $mapping['default'];
464
        }
465
466 303
        if (isset($mapping['columnDefinition'])) {
467 1
            $options['columnDefinition'] = $mapping['columnDefinition'];
468
        }
469
470 303
        if (isset($mapping['options'])) {
471 3
            $knownOptions = ['comment', 'unsigned', 'fixed', 'default'];
472
473 3
            foreach ($knownOptions as $knownOption) {
474 3
                if (array_key_exists($knownOption, $mapping['options'])) {
475 2
                    $options[$knownOption] = $mapping['options'][$knownOption];
476
477 3
                    unset($mapping['options'][$knownOption]);
478
                }
479
            }
480
481 3
            $options['customSchemaOptions'] = $mapping['options'];
482
        }
483
484 303
        if ($class->isIdGeneratorIdentity() && $class->getIdentifierFieldNames() == [$mapping['fieldName']]) {
485 261
            $options['autoincrement'] = true;
486
        }
487 303
        if ($class->isInheritanceTypeJoined() && $class->name !== $class->rootEntityName) {
488 60
            $options['autoincrement'] = false;
489
        }
490
491 303
        if ($table->hasColumn($columnName)) {
492
            // required in some inheritance scenarios
493
            $table->changeColumn($columnName, $options);
494
        } else {
495 303
            $table->addColumn($columnName, $columnType, $options);
496
        }
497
498 303
        $isUnique = $mapping['unique'] ?? false;
499 303
        if ($isUnique) {
500 18
            $table->addUniqueIndex([$columnName]);
501
        }
502 303
    }
503
504
    /**
505
     * Gathers the SQL for properly setting up the relations of the given class.
506
     * This includes the SQL for foreign key constraints and join tables.
507
     *
508
     * @param ClassMetadata $class
509
     * @param Table         $table
510
     * @param Schema        $schema
511
     * @param array         $addedFks
512
     * @param array         $blacklistedFks
513
     *
514
     * @return void
515
     *
516
     * @throws \Doctrine\ORM\ORMException
517
     */
518 303
    private function gatherRelationsSql($class, $table, $schema, &$addedFks, &$blacklistedFks)
519
    {
520 303
        foreach ($class->associationMappings as $id => $mapping) {
521 210
            if (isset($mapping['inherited']) && ! \in_array($id, $class->identifier, true)) {
522 21
                continue;
523
            }
524
525 210
            $foreignClass = $this->em->getClassMetadata($mapping['targetEntity']);
526
527 210
            if ($mapping['type'] & ClassMetadata::TO_ONE && $mapping['isOwningSide']) {
528 188
                $primaryKeyColumns = []; // PK is unnecessary for this relation-type
529
530 188
                $this->gatherRelationJoinColumns(
531 188
                    $mapping['joinColumns'],
532 188
                    $table,
533 188
                    $foreignClass,
534 188
                    $mapping,
535 188
                    $primaryKeyColumns,
536 188
                    $addedFks,
537 188
                    $blacklistedFks
538
                );
539 153
            } elseif ($mapping['type'] == ClassMetadata::ONE_TO_MANY && $mapping['isOwningSide']) {
540
                //... create join table, one-many through join table supported later
541
                throw ORMException::notSupported();
542 153
            } elseif ($mapping['type'] == ClassMetadata::MANY_TO_MANY && $mapping['isOwningSide']) {
543
                // create join table
544 47
                $joinTable = $mapping['joinTable'];
545
546 47
                $theJoinTable = $schema->createTable(
547 47
                    $this->quoteStrategy->getJoinTableName($mapping, $foreignClass, $this->platform)
548
                );
549
550 47
                $primaryKeyColumns = [];
551
552
                // Build first FK constraint (relation table => source table)
553 47
                $this->gatherRelationJoinColumns(
554 47
                    $joinTable['joinColumns'],
555 47
                    $theJoinTable,
556 47
                    $class,
557 47
                    $mapping,
558 47
                    $primaryKeyColumns,
559 47
                    $addedFks,
560 47
                    $blacklistedFks
561
                );
562
563
                // Build second FK constraint (relation table => target table)
564 47
                $this->gatherRelationJoinColumns(
565 47
                    $joinTable['inverseJoinColumns'],
566 47
                    $theJoinTable,
567 47
                    $foreignClass,
568 47
                    $mapping,
569 47
                    $primaryKeyColumns,
570 47
                    $addedFks,
571 47
                    $blacklistedFks
572
                );
573
574 210
                $theJoinTable->setPrimaryKey($primaryKeyColumns);
575
            }
576
        }
577 303
    }
578
579
    /**
580
     * Gets the class metadata that is responsible for the definition of the referenced column name.
581
     *
582
     * Previously this was a simple task, but with DDC-117 this problem is actually recursive. If its
583
     * not a simple field, go through all identifier field names that are associations recursively and
584
     * find that referenced column name.
585
     *
586
     * TODO: Is there any way to make this code more pleasing?
587
     *
588
     * @param ClassMetadata $class
589
     * @param string        $referencedColumnName
590
     *
591
     * @return array (ClassMetadata, referencedFieldName)
592
     */
593 210
    private function getDefiningClass($class, $referencedColumnName)
594
    {
595 210
        $referencedFieldName = $class->getFieldName($referencedColumnName);
596
597 210
        if ($class->hasField($referencedFieldName)) {
598 210
            return [$class, $referencedFieldName];
599
        }
600
601 10
        if (in_array($referencedColumnName, $class->getIdentifierColumnNames())) {
602
            // it seems to be an entity as foreign key
603 10
            foreach ($class->getIdentifierFieldNames() as $fieldName) {
604 10
                if ($class->hasAssociation($fieldName)
605 10
                    && $class->getSingleAssociationJoinColumnName($fieldName) == $referencedColumnName) {
606 10
                    return $this->getDefiningClass(
607 10
                        $this->em->getClassMetadata($class->associationMappings[$fieldName]['targetEntity']),
608 10
                        $class->getSingleAssociationReferencedJoinColumnName($fieldName)
609
                    );
610
                }
611
            }
612
        }
613
614
        return null;
615
    }
616
617
    /**
618
     * Gathers columns and fk constraints that are required for one part of relationship.
619
     *
620
     * @param array         $joinColumns
621
     * @param Table         $theJoinTable
622
     * @param ClassMetadata $class
623
     * @param array         $mapping
624
     * @param array         $primaryKeyColumns
625
     * @param array         $addedFks
626
     * @param array         $blacklistedFks
627
     *
628
     * @return void
629
     *
630
     * @throws \Doctrine\ORM\ORMException
631
     */
632 210
    private function gatherRelationJoinColumns(
633
        $joinColumns,
634
        $theJoinTable,
635
        $class,
636
        $mapping,
637
        &$primaryKeyColumns,
638
        &$addedFks,
639
        &$blacklistedFks
640
    )
641
    {
642 210
        $localColumns       = [];
643 210
        $foreignColumns     = [];
644 210
        $fkOptions          = [];
645 210
        $foreignTableName   = $this->quoteStrategy->getTableName($class, $this->platform);
646 210
        $uniqueConstraints  = [];
647
648 210
        foreach ($joinColumns as $joinColumn) {
649
650 210
            list($definingClass, $referencedFieldName) = $this->getDefiningClass(
651 210
                $class,
652 210
                $joinColumn['referencedColumnName']
653
            );
654
655 210
            if ( ! $definingClass) {
656
                throw new \Doctrine\ORM\ORMException(
657
                    'Column name `' . $joinColumn['referencedColumnName'] . '` referenced for relation from '
658
                    . $mapping['sourceEntity'] . ' towards ' . $mapping['targetEntity'] . ' does not exist.'
659
                );
660
            }
661
662 210
            $quotedColumnName    = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
663 210
            $quotedRefColumnName = $this->quoteStrategy->getReferencedJoinColumnName(
664 210
                $joinColumn,
665 210
                $class,
666 210
                $this->platform
667
            );
668
669 210
            $primaryKeyColumns[] = $quotedColumnName;
670 210
            $localColumns[]      = $quotedColumnName;
671 210
            $foreignColumns[]    = $quotedRefColumnName;
672
673 210
            if ( ! $theJoinTable->hasColumn($quotedColumnName)) {
674
                // Only add the column to the table if it does not exist already.
675
                // It might exist already if the foreign key is mapped into a regular
676
                // property as well.
677
678 207
                $fieldMapping = $definingClass->getFieldMapping($referencedFieldName);
679
680 207
                $columnDef = null;
681 207
                if (isset($joinColumn['columnDefinition'])) {
682
                    $columnDef = $joinColumn['columnDefinition'];
683 207
                } elseif (isset($fieldMapping['columnDefinition'])) {
684 1
                    $columnDef = $fieldMapping['columnDefinition'];
685
                }
686
687 207
                $columnOptions = ['notnull' => false, 'columnDefinition' => $columnDef];
688
689 207
                if (isset($joinColumn['nullable'])) {
690 129
                    $columnOptions['notnull'] = ! $joinColumn['nullable'];
691
                }
692
693 207
                if (isset($fieldMapping['options'])) {
694
                    $columnOptions['options'] = $fieldMapping['options'];
695
                }
696
697 207
                if ($fieldMapping['type'] == "string" && isset($fieldMapping['length'])) {
698 3
                    $columnOptions['length'] = $fieldMapping['length'];
699 207
                } elseif ($fieldMapping['type'] == "decimal") {
700
                    $columnOptions['scale'] = $fieldMapping['scale'];
701
                    $columnOptions['precision'] = $fieldMapping['precision'];
702
                }
703
704 207
                $theJoinTable->addColumn($quotedColumnName, $fieldMapping['type'], $columnOptions);
705
            }
706
707 210
            if (isset($joinColumn['unique']) && $joinColumn['unique'] == true) {
708 63
                $uniqueConstraints[] = ['columns' => [$quotedColumnName]];
709
            }
710
711 210
            if (isset($joinColumn['onDelete'])) {
712 210
                $fkOptions['onDelete'] = $joinColumn['onDelete'];
713
            }
714
        }
715
716
        // Prefer unique constraints over implicit simple indexes created for foreign keys.
717
        // Also avoids index duplication.
718 210
        foreach ($uniqueConstraints as $indexName => $unique) {
719 63
            $theJoinTable->addUniqueIndex($unique['columns'], is_numeric($indexName) ? null : $indexName);
720
        }
721
722 210
        $compositeName = $theJoinTable->getName().'.'.implode('', $localColumns);
723 210
        if (isset($addedFks[$compositeName])
724 1
            && ($foreignTableName != $addedFks[$compositeName]['foreignTableName']
725 210
            || 0 < count(array_diff($foreignColumns, $addedFks[$compositeName]['foreignColumns'])))
726
        ) {
727 1
            foreach ($theJoinTable->getForeignKeys() as $fkName => $key) {
728 1
                if (0 === count(array_diff($key->getLocalColumns(), $localColumns))
729 1
                    && (($key->getForeignTableName() != $foreignTableName)
730 1
                    || 0 < count(array_diff($key->getForeignColumns(), $foreignColumns)))
731
                ) {
732 1
                    $theJoinTable->removeForeignKey($fkName);
733 1
                    break;
734
                }
735
            }
736 1
            $blacklistedFks[$compositeName] = true;
737 210
        } elseif ( ! isset($blacklistedFks[$compositeName])) {
738 210
            $addedFks[$compositeName] = ['foreignTableName' => $foreignTableName, 'foreignColumns' => $foreignColumns];
739 210
            $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

739
            /** @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...
740 210
                $foreignTableName,
741 210
                $localColumns,
742 210
                $foreignColumns,
743 210
                $fkOptions
744
            );
745
        }
746 210
    }
747
748
    /**
749
     * Drops the database schema for the given classes.
750
     *
751
     * In any way when an exception is thrown it is suppressed since drop was
752
     * issued for all classes of the schema and some probably just don't exist.
753
     *
754
     * @param array $classes
755
     *
756
     * @return void
757
     */
758 6
    public function dropSchema(array $classes)
759
    {
760 6
        $dropSchemaSql = $this->getDropSchemaSQL($classes);
761 6
        $conn = $this->em->getConnection();
762
763 6
        foreach ($dropSchemaSql as $sql) {
764
            try {
765 6
                $conn->executeQuery($sql);
766 6
            } catch (\Throwable $e) {
767
                // ignored
768
            }
769
        }
770 6
    }
771
772
    /**
773
     * Drops all elements in the database of the current connection.
774
     *
775
     * @return void
776
     */
777
    public function dropDatabase()
778
    {
779
        $dropSchemaSql = $this->getDropDatabaseSQL();
780
        $conn = $this->em->getConnection();
781
782
        foreach ($dropSchemaSql as $sql) {
783
            $conn->executeQuery($sql);
784
        }
785
    }
786
787
    /**
788
     * Gets the SQL needed to drop the database schema for the connections database.
789
     *
790
     * @return array
791
     */
792
    public function getDropDatabaseSQL()
793
    {
794
        $sm = $this->em->getConnection()->getSchemaManager();
795
        $schema = $sm->createSchema();
796
797
        $visitor = new DropSchemaSqlCollector($this->platform);
798
        $schema->visit($visitor);
799
800
        return $visitor->getQueries();
801
    }
802
803
    /**
804
     * Gets SQL to drop the tables defined by the passed classes.
805
     *
806
     * @param array $classes
807
     *
808
     * @return array
809
     */
810 6
    public function getDropSchemaSQL(array $classes)
811
    {
812 6
        $visitor = new DropSchemaSqlCollector($this->platform);
813 6
        $schema = $this->getSchemaFromMetadata($classes);
814
815 6
        $sm = $this->em->getConnection()->getSchemaManager();
816 6
        $fullSchema = $sm->createSchema();
817
818 6
        foreach ($fullSchema->getTables() as $table) {
819 6
            if ( ! $schema->hasTable($table->getName())) {
820 6
                foreach ($table->getForeignKeys() as $foreignKey) {
821
                    /* @var $foreignKey \Doctrine\DBAL\Schema\ForeignKeyConstraint */
822
                    if ($schema->hasTable($foreignKey->getForeignTableName())) {
823 6
                        $visitor->acceptForeignKey($table, $foreignKey);
824
                    }
825
                }
826
            } else {
827 6
                $visitor->acceptTable($table);
828 6
                foreach ($table->getForeignKeys() as $foreignKey) {
829 6
                    $visitor->acceptForeignKey($table, $foreignKey);
830
                }
831
            }
832
        }
833
834 6
        if ($this->platform->supportsSequences()) {
835
            foreach ($schema->getSequences() as $sequence) {
836
                $visitor->acceptSequence($sequence);
837
            }
838
839
            foreach ($schema->getTables() as $table) {
840
                /* @var $sequence Table */
841
                if ($table->hasPrimaryKey()) {
842
                    $columns = $table->getPrimaryKey()->getColumns();
843
                    if (count($columns) == 1) {
844
                        $checkSequence = $table->getName() . '_' . $columns[0] . '_seq';
845
                        if ($fullSchema->hasSequence($checkSequence)) {
846
                            $visitor->acceptSequence($fullSchema->getSequence($checkSequence));
847
                        }
848
                    }
849
                }
850
            }
851
        }
852
853 6
        return $visitor->getQueries();
854
    }
855
856
    /**
857
     * Updates the database schema of the given classes by comparing the ClassMetadata
858
     * instances to the current database schema that is inspected.
859
     *
860
     * @param array   $classes
861
     * @param boolean $saveMode If TRUE, only performs a partial update
862
     *                          without dropping assets which are scheduled for deletion.
863
     *
864
     * @return void
865
     */
866
    public function updateSchema(array $classes, $saveMode = false)
867
    {
868
        $updateSchemaSql = $this->getUpdateSchemaSql($classes, $saveMode);
869
        $conn = $this->em->getConnection();
870
871
        foreach ($updateSchemaSql as $sql) {
872
            $conn->executeQuery($sql);
873
        }
874
    }
875
876
    /**
877
     * Gets the sequence of SQL statements that need to be performed in order
878
     * to bring the given class mappings in-synch with the relational schema.
879
     *
880
     * @param array   $classes  The classes to consider.
881
     * @param boolean $saveMode If TRUE, only generates SQL for a partial update
882
     *                          that does not include SQL for dropping assets which are scheduled for deletion.
883
     *
884
     * @return array The sequence of SQL statements.
885
     */
886 1
    public function getUpdateSchemaSql(array $classes, $saveMode = false)
887
    {
888 1
        $sm = $this->em->getConnection()->getSchemaManager();
889
890 1
        $fromSchema = $sm->createSchema();
891 1
        $toSchema = $this->getSchemaFromMetadata($classes);
892
893 1
        $comparator = new Comparator();
894 1
        $schemaDiff = $comparator->compare($fromSchema, $toSchema);
895
896 1
        if ($saveMode) {
897
            return $schemaDiff->toSaveSql($this->platform);
898
        }
899
900 1
        return $schemaDiff->toSql($this->platform);
901
    }
902
903
    /**
904
     * Gets namespaces for the current platform
905
     *
906
     * @return string[]
907
     */
908 303
    private function getNamespaces() {
909 303
        if (! $this->platform->supportsSchemas()) {
910 303
            return [];
911
        }
912
913
        $namespaces = $this->em->getConnection()->getSchemaManager()->listNamespaceNames();
914
915
        return $namespaces;
916
    }
917
}
918