Failed Conditions
Pull Request — 2.6 (#7875)
by
unknown
07:55
created

SchemaTool::getUpdateSchemaSql()   B

Complexity

Conditions 11
Paths 60

Size

Total Lines 51
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 11.8604

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 27
c 1
b 0
f 0
dl 0
loc 51
ccs 21
cts 26
cp 0.8077
rs 7.3166
cc 11
nc 60
nop 2
crap 11.8604

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

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

916
            $filterExpression = $hasFilterSchemaAssetsExpression ? /** @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...
917 2
            $filter = $config->getSchemaAssetsFilter();
918
919 2
            if (null !== $filter) {
920
                // whitelist assets we already know about in $toSchema, use the existing filter otherwise
921
                $config->setSchemaAssetsFilter(static function ($asset) use ($filter, $toSchema) : bool {
922
                    $assetName = $asset instanceof AbstractAsset ? $asset->getName() : $asset;
923
924
                    return $toSchema->hasTable($assetName) || $toSchema->hasSequence($assetName) || $filter($asset);
925
                });
926
            }
927
        }
928
929
        try {
930 2
            $fromSchema = $sm->createSchema();
931 2
        } finally {
932 2
            if ($hasSchemaAssetsFilter && null !== $filter) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $filter does not seem to be defined for all execution paths leading up to this point.
Loading history...
933
                // restore schema assets filter and filter-expression
934
                if (null !== $filterExpression) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $filterExpression does not seem to be defined for all execution paths leading up to this point.
Loading history...
935
                    $config->setFilterSchemaAssetsExpression($filterExpression);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $config does not seem to be defined for all execution paths leading up to this point.
Loading history...
936
                } else {
937 2
                    $config->setSchemaAssetsFilter($filter);
938
                }
939
            }
940
        }
941
942 2
        $comparator = new Comparator();
943 2
        $schemaDiff = $comparator->compare($fromSchema, $toSchema);
944
945 2
        if ($saveMode) {
946 1
            return $schemaDiff->toSaveSql($this->platform);
947
        }
948
949 1
        return $schemaDiff->toSql($this->platform);
950
    }
951
}
952