Failed Conditions
Push — master ( b210c1...4c8949 )
by Marco
12:15
created

SchemaTool::gatherRelationsSql()   C

Complexity

Conditions 10
Paths 6

Size

Total Lines 57
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 38
CRAP Score 10.0016

Importance

Changes 0
Metric Value
cc 10
eloc 38
nc 6
nop 5
dl 0
loc 57
ccs 38
cts 39
cp 0.9744
crap 10.0016
rs 6.7123
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/*
3
 * 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 1298
    public function __construct(EntityManagerInterface $em)
72
    {
73 1298
        $this->em               = $em;
74 1298
        $this->platform         = $em->getConnection()->getDatabasePlatform();
75 1298
        $this->quoteStrategy    = $em->getConfiguration()->getQuoteStrategy();
76 1298
    }
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 288
    public function createSchema(array $classes)
88
    {
89 288
        $createSchemaSql = $this->getCreateSchemaSql($classes);
90 288
        $conn = $this->em->getConnection();
91
92 288
        foreach ($createSchemaSql as $sql) {
93
            try {
94 288
                $conn->executeQuery($sql);
95 90
            } catch (\Throwable $e) {
96 288
                throw ToolsException::schemaToolFailure($sql, $e);
97
            }
98
        }
99 198
    }
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 288
    public function getCreateSchemaSql(array $classes)
110
    {
111 288
        $schema = $this->getSchemaFromMetadata($classes);
112
113 288
        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 298
    private function processingNotRequired($class, array $processedClasses)
125
    {
126
        return (
127 298
            isset($processedClasses[$class->name]) ||
128 298
            $class->isMappedSuperclass ||
129 298
            $class->isEmbeddedClass ||
130 298
            ($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 298
    public function getSchemaFromMetadata(array $classes)
144
    {
145
        // Reminder for processed classes, used for hierarchies
146 298
        $processedClasses       = [];
147 298
        $eventManager           = $this->em->getEventManager();
148 298
        $schemaManager          = $this->em->getConnection()->getSchemaManager();
149 298
        $metadataSchemaConfig   = $schemaManager->createSchemaConfig();
150
151 298
        $metadataSchemaConfig->setExplicitForeignKeyIndexes(false);
152 298
        $schema = new Schema([], [], $metadataSchemaConfig);
153
154 298
        $addedFks = [];
155 298
        $blacklistedFks = [];
156
157 298
        foreach ($classes as $class) {
158
            /** @var \Doctrine\ORM\Mapping\ClassMetadata $class */
159 298
            if ($this->processingNotRequired($class, $processedClasses)) {
160 39
                continue;
161
            }
162
163 298
            $table = $schema->createTable($this->quoteStrategy->getTableName($class, $this->platform));
164
165 298
            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 294
            } elseif ($class->isInheritanceTypeJoined()) {
185
                // Add all non-inherited fields as columns
186 60
                foreach ($class->fieldMappings as $fieldName => $mapping) {
187 60
                    if ( ! isset($mapping['inherited'])) {
188 60
                        $this->gatherColumn($class, $mapping, $table);
189
                    }
190
                }
191
192 60
                $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
193
194
                // Add the discriminator column only to the root table
195 60
                if ($class->name == $class->rootEntityName) {
196 60
                    $this->addDiscriminatorColumnDefinition($class, $table);
197
                } else {
198
                    // Add an ID FK column to child tables
199 59
                    $pkColumns           = [];
200 59
                    $inheritedKeyColumns = [];
201
202 59
                    foreach ($class->identifier as $identifierField) {
203 59
                        if (isset($class->fieldMappings[$identifierField]['inherited'])) {
204 59
                            $idMapping = $class->fieldMappings[$identifierField];
205 59
                            $this->gatherColumn($class, $idMapping, $table);
206 59
                            $columnName = $this->quoteStrategy->getColumnName(
207 59
                                $identifierField,
208 59
                                $class,
209 59
                                $this->platform
210
                            );
211
                            // TODO: This seems rather hackish, can we optimize it?
212 59
                            $table->getColumn($columnName)->setAutoincrement(false);
213
214 59
                            $pkColumns[] = $columnName;
215 59
                            $inheritedKeyColumns[] = $columnName;
216
217 59
                            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 View Code Duplication
                                if (isset($targetEntity->fieldMappings[$joinColumn['referencedColumnName']])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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 59
                    if ( ! empty($inheritedKeyColumns)) {
248
                        // Add a FK constraint on the ID column
249 59
                        $table->addForeignKeyConstraint(
250 59
                            $this->quoteStrategy->getTableName(
251 59
                                $this->em->getClassMetadata($class->rootEntityName),
252 59
                                $this->platform
253
                            ),
254 59
                            $inheritedKeyColumns,
255 59
                            $inheritedKeyColumns,
256 59
                            ['onDelete' => 'CASCADE']
257
                        );
258
                    }
259
260 59
                    if ( ! empty($pkColumns)) {
261 60
                        $table->setPrimaryKey($pkColumns);
262
                    }
263
                }
264 272
            } elseif ($class->isInheritanceTypeTablePerClass()) {
265
                throw ORMException::notSupported();
266
            } else {
267 272
                $this->gatherColumns($class, $table);
268 272
                $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
269
            }
270
271 298
            $pkColumns = [];
272
273 298
            foreach ($class->identifier as $identifierField) {
274 298
                if (isset($class->fieldMappings[$identifierField])) {
275 298
                    $pkColumns[] = $this->quoteStrategy->getColumnName($identifierField, $class, $this->platform);
276 31 View Code Duplication
                } elseif (isset($class->associationMappings[$identifierField])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
277
                    /* @var $assoc \Doctrine\ORM\Mapping\OneToOne */
278 31
                    $assoc = $class->associationMappings[$identifierField];
279
280 31
                    foreach ($assoc['joinColumns'] as $joinColumn) {
281 298
                        $pkColumns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
282
                    }
283
                }
284
            }
285
286 298
            if ( ! $table->hasIndex('primary')) {
287 298
                $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 298
            $primaryKey = $table->getIndex('primary');
294
295 298
            foreach ($table->getIndexes() as $idxKey => $existingIndex) {
296 298
                if ($primaryKey->overrules($existingIndex)) {
297 298
                    $table->dropIndex($idxKey);
298
                }
299
            }
300
301 298
            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 298
            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 298
            if (isset($class->table['options'])) {
327 1
                foreach ($class->table['options'] as $key => $val) {
328 1
                    $table->addOption($key, $val);
329
                }
330
            }
331
332 298
            $processedClasses[$class->name] = true;
333
334 298
            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 298
            if ($eventManager->hasListeners(ToolEvents::postGenerateSchemaTable)) {
347 1
                $eventManager->dispatchEvent(
348 1
                    ToolEvents::postGenerateSchemaTable,
349 298
                    new GenerateSchemaTableEventArgs($class, $schema, $table)
350
                );
351
            }
352
        }
353
354 298
        if ( ! $this->platform->supportsSchemas() && ! $this->platform->canEmulateSchemas()) {
355 9
            $schema->visit(new RemoveNamespacedAssets());
356
        }
357
358 298
        if ($eventManager->hasListeners(ToolEvents::postGenerateSchema)) {
359 1
            $eventManager->dispatchEvent(
360 1
                ToolEvents::postGenerateSchema,
361 1
                new GenerateSchemaEventArgs($this->em, $schema)
362
            );
363
        }
364
365 298
        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 91
    private function addDiscriminatorColumnDefinition($class, Table $table)
378
    {
379 91
        $discrColumn = $class->discriminatorColumn;
380
381 91
        if ( ! isset($discrColumn['type']) ||
382 91
            (strtolower($discrColumn['type']) == 'string' && ! isset($discrColumn['length']))
383
        ) {
384 1
            $discrColumn['type'] = 'string';
385 1
            $discrColumn['length'] = 255;
386
        }
387
388
        $options = [
389 91
            'length'    => $discrColumn['length'] ?? null,
390
            'notnull'   => true
391
        ];
392
393 91
        if (isset($discrColumn['columnDefinition'])) {
394
            $options['columnDefinition'] = $discrColumn['columnDefinition'];
395
        }
396
397 91
        $table->addColumn($discrColumn['name'], $discrColumn['type'], $options);
398 91
    }
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 279
    private function gatherColumns($class, Table $table)
410
    {
411 279
        $pkColumns = [];
412
413 279
        foreach ($class->fieldMappings as $mapping) {
414 279
            if ($class->isInheritanceTypeSingleTable() && isset($mapping['inherited'])) {
415 34
                continue;
416
            }
417
418 279
            $this->gatherColumn($class, $mapping, $table);
419
420 279
            if ($class->isIdentifier($mapping['fieldName'])) {
421 279
                $pkColumns[] = $this->quoteStrategy->getColumnName($mapping['fieldName'], $class, $this->platform);
422
            }
423
        }
424 279
    }
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 298
    private function gatherColumn($class, array $mapping, Table $table)
436
    {
437 298
        $columnName = $this->quoteStrategy->getColumnName($mapping['fieldName'], $class, $this->platform);
438 298
        $columnType = $mapping['type'];
439
440 298
        $options = [];
441 298
        $options['length'] = $mapping['length'] ?? null;
442 298
        $options['notnull'] = isset($mapping['nullable']) ? ! $mapping['nullable'] : true;
443 298
        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 298
        $options['platformOptions'] = [];
448 298
        $options['platformOptions']['version'] = $class->isVersioned && $class->versionField === $mapping['fieldName'];
449
450 298
        if (strtolower($columnType) === 'string' && null === $options['length']) {
451 155
            $options['length'] = 255;
452
        }
453
454 298
        if (isset($mapping['precision'])) {
455 296
            $options['precision'] = $mapping['precision'];
456
        }
457
458 298
        if (isset($mapping['scale'])) {
459 296
            $options['scale'] = $mapping['scale'];
460
        }
461
462 298
        if (isset($mapping['default'])) {
463 30
            $options['default'] = $mapping['default'];
464
        }
465
466 298
        if (isset($mapping['columnDefinition'])) {
467 1
            $options['columnDefinition'] = $mapping['columnDefinition'];
468
        }
469
470 298
        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 298
        if ($class->isIdGeneratorIdentity() && $class->getIdentifierFieldNames() == [$mapping['fieldName']]) {
485 258
            $options['autoincrement'] = true;
486
        }
487 298
        if ($class->isInheritanceTypeJoined() && $class->name !== $class->rootEntityName) {
488 59
            $options['autoincrement'] = false;
489
        }
490
491 298
        if ($table->hasColumn($columnName)) {
492
            // required in some inheritance scenarios
493
            $table->changeColumn($columnName, $options);
494
        } else {
495 298
            $table->addColumn($columnName, $columnType, $options);
496
        }
497
498 298
        $isUnique = $mapping['unique'] ?? false;
499 298
        if ($isUnique) {
500 18
            $table->addUniqueIndex([$columnName]);
501
        }
502 298
    }
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 298
    private function gatherRelationsSql($class, $table, $schema, &$addedFks, &$blacklistedFks)
519
    {
520 298
        foreach ($class->associationMappings as $id => $mapping) {
521 207
            if (isset($mapping['inherited']) && ! \in_array($id, $class->identifier, true)) {
522 21
                continue;
523
            }
524
525 207
            $foreignClass = $this->em->getClassMetadata($mapping['targetEntity']);
526
527 207
            if ($mapping['type'] & ClassMetadata::TO_ONE && $mapping['isOwningSide']) {
528 185
                $primaryKeyColumns = []; // PK is unnecessary for this relation-type
529
530 185
                $this->gatherRelationJoinColumns(
531 185
                    $mapping['joinColumns'],
532 185
                    $table,
533 185
                    $foreignClass,
534 185
                    $mapping,
535 185
                    $primaryKeyColumns,
536 185
                    $addedFks,
537 185
                    $blacklistedFks
538
                );
539 151
            } 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 151
            } 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 207
                $theJoinTable->setPrimaryKey($primaryKeyColumns);
575
            }
576
        }
577 298
    }
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 207
    private function getDefiningClass($class, $referencedColumnName)
594
    {
595 207
        $referencedFieldName = $class->getFieldName($referencedColumnName);
596
597 207
        if ($class->hasField($referencedFieldName)) {
598 207
            return [$class, $referencedFieldName];
599
        }
600
601 9
        if (in_array($referencedColumnName, $class->getIdentifierColumnNames())) {
602
            // it seems to be an entity as foreign key
603 9
            foreach ($class->getIdentifierFieldNames() as $fieldName) {
604 9
                if ($class->hasAssociation($fieldName)
605 9
                    && $class->getSingleAssociationJoinColumnName($fieldName) == $referencedColumnName) {
606 9
                    return $this->getDefiningClass(
607 9
                        $this->em->getClassMetadata($class->associationMappings[$fieldName]['targetEntity']),
608 9
                        $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 207
    private function gatherRelationJoinColumns(
633
        $joinColumns,
634
        $theJoinTable,
635
        $class,
636
        $mapping,
637
        &$primaryKeyColumns,
638
        &$addedFks,
639
        &$blacklistedFks
640
    )
641
    {
642 207
        $localColumns       = [];
643 207
        $foreignColumns     = [];
644 207
        $fkOptions          = [];
645 207
        $foreignTableName   = $this->quoteStrategy->getTableName($class, $this->platform);
646 207
        $uniqueConstraints  = [];
647
648 207
        foreach ($joinColumns as $joinColumn) {
649
650 207
            list($definingClass, $referencedFieldName) = $this->getDefiningClass(
651 207
                $class,
652 207
                $joinColumn['referencedColumnName']
653
            );
654
655 207
            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 207
            $quotedColumnName    = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
663 207
            $quotedRefColumnName = $this->quoteStrategy->getReferencedJoinColumnName(
664 207
                $joinColumn,
665 207
                $class,
666 207
                $this->platform
667
            );
668
669 207
            $primaryKeyColumns[] = $quotedColumnName;
670 207
            $localColumns[]      = $quotedColumnName;
671 207
            $foreignColumns[]    = $quotedRefColumnName;
672
673 207
            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 204
                $fieldMapping = $definingClass->getFieldMapping($referencedFieldName);
679
680 204
                $columnDef = null;
681 204
                if (isset($joinColumn['columnDefinition'])) {
682
                    $columnDef = $joinColumn['columnDefinition'];
683 204
                } elseif (isset($fieldMapping['columnDefinition'])) {
684 1
                    $columnDef = $fieldMapping['columnDefinition'];
685
                }
686
687 204
                $columnOptions = ['notnull' => false, 'columnDefinition' => $columnDef];
688
689 204
                if (isset($joinColumn['nullable'])) {
690 127
                    $columnOptions['notnull'] = ! $joinColumn['nullable'];
691
                }
692
693 204
                if (isset($fieldMapping['options'])) {
694
                    $columnOptions['options'] = $fieldMapping['options'];
695
                }
696
697 204
                if ($fieldMapping['type'] == "string" && isset($fieldMapping['length'])) {
698 3
                    $columnOptions['length'] = $fieldMapping['length'];
699 204
                } elseif ($fieldMapping['type'] == "decimal") {
700
                    $columnOptions['scale'] = $fieldMapping['scale'];
701
                    $columnOptions['precision'] = $fieldMapping['precision'];
702
                }
703
704 204
                $theJoinTable->addColumn($quotedColumnName, $fieldMapping['type'], $columnOptions);
705
            }
706
707 207
            if (isset($joinColumn['unique']) && $joinColumn['unique'] == true) {
708 63
                $uniqueConstraints[] = ['columns' => [$quotedColumnName]];
709
            }
710
711 207
            if (isset($joinColumn['onDelete'])) {
712 207
                $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 207
        foreach ($uniqueConstraints as $indexName => $unique) {
719 63
            $theJoinTable->addUniqueIndex($unique['columns'], is_numeric($indexName) ? null : $indexName);
720
        }
721
722 207
        $compositeName = $theJoinTable->getName().'.'.implode('', $localColumns);
723 207
        if (isset($addedFks[$compositeName])
724 1
            && ($foreignTableName != $addedFks[$compositeName]['foreignTableName']
725 207
            || 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 207
        } elseif ( ! isset($blacklistedFks[$compositeName])) {
738 207
            $addedFks[$compositeName] = ['foreignTableName' => $foreignTableName, 'foreignColumns' => $foreignColumns];
739 207
            $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 207
                $foreignTableName,
741 207
                $localColumns,
742 207
                $foreignColumns,
743 207
                $fkOptions
744
            );
745
        }
746 207
    }
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