Completed
Pull Request — 2.6 (#7875)
by
unknown
64:07
created

SchemaTool::gatherRelationJoinColumns()   F

Complexity

Conditions 24
Paths 925

Size

Total Lines 115
Code Lines 64

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 51
CRAP Score 29.7565

Importance

Changes 0
Metric Value
eloc 64
dl 0
loc 115
rs 0.1041
c 0
b 0
f 0
ccs 51
cts 65
cp 0.7846
cc 24
nc 925
nop 7
crap 29.7565

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

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

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

939
            $filterExpression = $hasFilterExpressionMethod ? /** @scrutinizer ignore-deprecated */ $config->getFilterSchemaAssetsExpression() : null;

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...
940
            $filter           = $config->getSchemaAssetsFilter();
941
942
            if ($filter !== null) {
943
                // whitelist assets we already know about in $toSchema, use the existing filter otherwise
944
                $config->setSchemaAssetsFilter(static function ($asset) use ($filter, $toSchema) : bool {
0 ignored issues
show
Unused Code introduced by
The import $toSchema is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
945
                    $assetName = $asset instanceof AbstractAsset ? $asset->getName() : $asset;
0 ignored issues
show
Unused Code introduced by
The assignment to $assetName is dead and can be removed.
Loading history...
946
947
                    return $filter($asset);//$toSchema->hasTable($assetName) || $toSchema->hasSequence($assetName) || $filter($asset);
948
                });
949
            }
950
        }
951
952
        try {
953
            return $sm->createSchema();
954
        } finally {
955
            if ($hasFilterMethod && $filter !== null) {
956
                // restore schema assets filter and filter-expression
957
                if ($filterExpression !== null) {
958
                    $config->setFilterSchemaAssetsExpression($filterExpression);
959
                } else {
960
                    $config->setSchemaAssetsFilter($filter);
961
                }
962
            }
963
        }
964
    }
965
}
966