Failed Conditions
Pull Request — 2.7 (#8046)
by
unknown
06:06
created

SchemaTool   F

Complexity

Total Complexity 151

Size/Duplication

Total Lines 913
Duplicated Lines 0 %

Test Coverage

Coverage 86.65%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 151
eloc 382
c 3
b 0
f 0
dl 0
loc 913
ccs 344
cts 397
cp 0.8665
rs 2

20 Methods

Rating   Name   Duplication   Size   Complexity  
A updateSchema() 0 7 2
A __construct() 0 5 1
A getCreateSchemaSql() 0 5 1
A createSchema() 0 10 3
A processingNotRequired() 0 7 5
A dropSchema() 0 9 3
A createSchemaForComparison() 0 25 5
A gatherColumns() 0 13 5
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 addSchemaNameSpaceFromTable() 0 12 3
A getDefiningClass() 0 22 6
F getSchemaFromMetadata() 0 224 43
B gatherRelationsSql() 0 57 10
A dropDatabase() 0 7 2
F gatherRelationJoinColumns() 0 110 23

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

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