Completed
Push — 2.7 ( c9e41d...587324 )
by Luís
06:51 queued 12s
created

SchemaTool::getDropSchemaSQL()   C

Complexity

Conditions 12
Paths 12

Size

Total Lines 44
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 22.4191

Importance

Changes 0
Metric Value
cc 12
eloc 24
nc 12
nop 1
dl 0
loc 44
ccs 14
cts 24
cp 0.5833
crap 22.4191
rs 6.9666
c 0
b 0
f 0

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

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