Failed Conditions
Pull Request — 2.8.x (#7946)
by
unknown
09:22 queued 12s
created

SchemaTool::getSchemaFromMetadata()   F

Complexity

Conditions 43
Paths > 20000

Size

Total Lines 223
Code Lines 125

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 115
CRAP Score 43.7075

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 43
eloc 125
c 1
b 0
f 0
nop 1
dl 0
loc 223
ccs 115
cts 124
cp 0.9274
crap 43.7075
rs 0
nc 69129

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ORM\Tools;
21
22
use Doctrine\ORM\ORMException;
23
use Doctrine\DBAL\Schema\Comparator;
24
use Doctrine\DBAL\Schema\Index;
25
use Doctrine\DBAL\Schema\Schema;
26
use Doctrine\DBAL\Schema\Table;
27
use Doctrine\DBAL\Schema\Visitor\DropSchemaSqlCollector;
28
use Doctrine\DBAL\Schema\Visitor\RemoveNamespacedAssets;
29
use Doctrine\ORM\EntityManagerInterface;
30
use Doctrine\ORM\Mapping\ClassMetadata;
31
use Doctrine\ORM\Tools\Event\GenerateSchemaTableEventArgs;
32
use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs;
33
34
/**
35
 * The SchemaTool is a tool to create/drop/update database schemas based on
36
 * <tt>ClassMetadata</tt> class descriptors.
37
 *
38
 * @link    www.doctrine-project.org
39
 * @since   2.0
40
 * @author  Guilherme Blanco <[email protected]>
41
 * @author  Jonathan Wage <[email protected]>
42
 * @author  Roman Borschel <[email protected]>
43
 * @author  Benjamin Eberlei <[email protected]>
44
 * @author  Stefano Rodriguez <[email protected]>
45
 */
46
class SchemaTool
47
{
48
    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 1343
    public function __construct(EntityManagerInterface $em)
74
    {
75 1343
        $this->em               = $em;
76 1343
        $this->platform         = $em->getConnection()->getDatabasePlatform();
77 1343
        $this->quoteStrategy    = $em->getConfiguration()->getQuoteStrategy();
78 1343
    }
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 306
    public function createSchema(array $classes)
90
    {
91 306
        $createSchemaSql = $this->getCreateSchemaSql($classes);
92 306
        $conn = $this->em->getConnection();
93
94 306
        foreach ($createSchemaSql as $sql) {
95
            try {
96 306
                $conn->executeQuery($sql);
97 90
            } catch (\Throwable $e) {
98 90
                throw ToolsException::schemaToolFailure($sql, $e);
99
            }
100
        }
101 216
    }
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 306
    public function getCreateSchemaSql(array $classes)
112
    {
113 306
        $schema = $this->getSchemaFromMetadata($classes);
114
115 306
        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 317
    private function processingNotRequired($class, array $processedClasses)
127
    {
128
        return (
129 317
            isset($processedClasses[$class->name]) ||
130 317
            $class->isMappedSuperclass ||
131 317
            $class->isEmbeddedClass ||
132 317
            ($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 317
    public function getSchemaFromMetadata(array $classes)
146
    {
147
        // Reminder for processed classes, used for hierarchies
148 317
        $processedClasses       = [];
149 317
        $eventManager           = $this->em->getEventManager();
150 317
        $schemaManager          = $this->em->getConnection()->getSchemaManager();
151 317
        $metadataSchemaConfig   = $schemaManager->createSchemaConfig();
152
153 317
        $metadataSchemaConfig->setExplicitForeignKeyIndexes(false);
154 317
        $schema = new Schema([], [], $metadataSchemaConfig);
155
156 317
        $addedFks = [];
157 317
        $blacklistedFks = [];
158
159 317
        foreach ($classes as $class) {
160
            /** @var \Doctrine\ORM\Mapping\ClassMetadata $class */
161 317
            if ($this->processingNotRequired($class, $processedClasses)) {
162 38
                continue;
163
            }
164
165 317
            $table = $schema->createTable($this->quoteStrategy->getTableName($class, $this->platform));
166
167 317
            if ($class->isInheritanceTypeSingleTable()) {
168 35
                $this->gatherColumns($class, $table);
169 35
                $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
170
171
                // Add the discriminator column
172 35
                $this->addDiscriminatorColumnDefinition($class, $table);
173
174
                // Aggregate all the information from all classes in the hierarchy
175 35
                foreach ($class->parentClasses as $parentClassName) {
176
                    // Parent class information is already contained in this class
177
                    $processedClasses[$parentClassName] = true;
178
                }
179
180 35
                foreach ($class->subClasses as $subClassName) {
181 33
                    $subClass = $this->em->getClassMetadata($subClassName);
182 33
                    $this->gatherColumns($subClass, $table);
183 33
                    $this->gatherRelationsSql($subClass, $table, $schema, $addedFks, $blacklistedFks);
184 33
                    $processedClasses[$subClassName] = true;
185
                }
186 313
            } 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 1
                                    $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 289
            } elseif ($class->isInheritanceTypeTablePerClass()) {
267
                throw ORMException::notSupported();
268
            } else {
269 289
                $this->gatherColumns($class, $table);
270 289
                $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
271
            }
272
273 317
            $pkColumns = [];
274
275 317
            foreach ($class->identifier as $identifierField) {
276 317
                if (isset($class->fieldMappings[$identifierField])) {
277 316
                    $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 34
                        $pkColumns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
284
                    }
285
                }
286
            }
287
288 317
            if ( ! $table->hasIndex('primary')) {
289 317
                $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 317
            $primaryKey = $table->getIndex('primary');
296
297 317
            foreach ($table->getIndexes() as $idxKey => $existingIndex) {
298 317
                if ($primaryKey->overrules($existingIndex)) {
299 2
                    $table->dropIndex($idxKey);
300
                }
301
            }
302
303 317
            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 317
            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 2
                            $table->dropIndex($tableIndexName);
320 2
                            break;
321
                        }
322
                    }
323
324 4
                    $table->addUniqueIndex($indexData['columns'], is_numeric($indexName) ? null : $indexName, $indexData['options'] ?? []);
325
                }
326
            }
327
328 317
            if (isset($class->table['options'])) {
329 1
                foreach ($class->table['options'] as $key => $val) {
330 1
                    $table->addOption($key, $val);
331
                }
332
            }
333
334 317
            $processedClasses[$class->name] = true;
335
336 317
            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 317
            if ($eventManager->hasListeners(ToolEvents::postGenerateSchemaTable)) {
349 1
                $eventManager->dispatchEvent(
350 1
                    ToolEvents::postGenerateSchemaTable,
351 1
                    new GenerateSchemaTableEventArgs($class, $schema, $table)
352
                );
353
            }
354
        }
355
356 317
        if ( ! $this->platform->supportsSchemas() && ! $this->platform->canEmulateSchemas()) {
357 10
            $schema->visit(new RemoveNamespacedAssets());
358
        }
359
360 317
        if ($eventManager->hasListeners(ToolEvents::postGenerateSchema)) {
361 1
            $eventManager->dispatchEvent(
362 1
                ToolEvents::postGenerateSchema,
363 1
                new GenerateSchemaEventArgs($this->em, $schema)
364
            );
365
        }
366
367 317
        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 92
    private function addDiscriminatorColumnDefinition($class, Table $table)
380
    {
381 92
        $discrColumn = $class->discriminatorColumn;
382
383 92
        if ( ! isset($discrColumn['type']) ||
384 92
            (strtolower($discrColumn['type']) == 'string' && ! isset($discrColumn['length']))
385
        ) {
386 1
            $discrColumn['type'] = 'string';
387 1
            $discrColumn['length'] = 255;
388
        }
389
390
        $options = [
391 92
            'length'    => $discrColumn['length'] ?? null,
392
            'notnull'   => true
393
        ];
394
395 92
        if (isset($discrColumn['columnDefinition'])) {
396
            $options['columnDefinition'] = $discrColumn['columnDefinition'];
397
        }
398
399 92
        $table->addColumn($discrColumn['name'], $discrColumn['type'], $options);
400 92
    }
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 296
    private function gatherColumns($class, Table $table)
412
    {
413 296
        $pkColumns = [];
414
415 296
        foreach ($class->fieldMappings as $mapping) {
416 296
            if ($class->isInheritanceTypeSingleTable() && isset($mapping['inherited'])) {
417 33
                continue;
418
            }
419
420 296
            $this->gatherColumn($class, $mapping, $table);
421
422 296
            if ($class->isIdentifier($mapping['fieldName'])) {
423 295
                $pkColumns[] = $this->quoteStrategy->getColumnName($mapping['fieldName'], $class, $this->platform);
424
            }
425
        }
426 296
    }
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 317
    private function gatherColumn($class, array $mapping, Table $table)
438
    {
439 317
        $columnName = $this->quoteStrategy->getColumnName($mapping['fieldName'], $class, $this->platform);
440 317
        $columnType = $mapping['type'];
441
442 317
        $options = [];
443 317
        $options['length'] = $mapping['length'] ?? null;
444 317
        $options['notnull'] = isset($mapping['nullable']) ? ! $mapping['nullable'] : true;
445 317
        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 317
        $options['platformOptions'] = [];
450 317
        $options['platformOptions']['version'] = $class->isVersioned && $class->versionField === $mapping['fieldName'];
451
452 317
        if (strtolower($columnType) === 'string' && null === $options['length']) {
453 164
            $options['length'] = 255;
454
        }
455
456 317
        if (isset($mapping['precision'])) {
457 315
            $options['precision'] = $mapping['precision'];
458
        }
459
460 317
        if (isset($mapping['scale'])) {
461 315
            $options['scale'] = $mapping['scale'];
462
        }
463
464 317
        if (isset($mapping['default'])) {
465 32
            $options['default'] = $mapping['default'];
466
        }
467
468 317
        if (isset($mapping['columnDefinition'])) {
469 1
            $options['columnDefinition'] = $mapping['columnDefinition'];
470
        }
471
472
        // the 'default' option can be overwritten here
473 317
        $options = $this->gatherColumnOptions($mapping) + $options;
474
475 317
        if ($class->isIdGeneratorIdentity() && $class->getIdentifierFieldNames() == [$mapping['fieldName']]) {
476 270
            $options['autoincrement'] = true;
477
        }
478 317
        if ($class->isInheritanceTypeJoined() && $class->name !== $class->rootEntityName) {
479 61
            $options['autoincrement'] = false;
480
        }
481
482 317
        if ($table->hasColumn($columnName)) {
483
            // required in some inheritance scenarios
484
            $table->changeColumn($columnName, $options);
485
        } else {
486 317
            $table->addColumn($columnName, $columnType, $options);
487
        }
488
489 317
        $isUnique = $mapping['unique'] ?? false;
490 317
        if ($isUnique) {
491 18
            $table->addUniqueIndex([$columnName]);
492
        }
493 317
    }
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 317
    private function gatherRelationsSql($class, $table, $schema, &$addedFks, &$blacklistedFks)
510
    {
511 317
        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 158
            } 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 158
            } 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 49
                $theJoinTable->setPrimaryKey($primaryKeyColumns);
566
            }
567
        }
568 317
    }
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 133
                    $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 22
                $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
713 217
        if (! $this->platform->supportsForeignKeyConstraints()) {
714 210
            return;
715
        }
716
717 7
        if (isset($addedFks[$compositeName])
718
            && ($foreignTableName != $addedFks[$compositeName]['foreignTableName']
719 7
            || 0 < count(array_diff($foreignColumns, $addedFks[$compositeName]['foreignColumns'])))
720
        ) {
721
            foreach ($theJoinTable->getForeignKeys() as $fkName => $key) {
722
                if (0 === count(array_diff($key->getLocalColumns(), $localColumns))
723
                    && (($key->getForeignTableName() != $foreignTableName)
724
                    || 0 < count(array_diff($key->getForeignColumns(), $foreignColumns)))
725
                ) {
726
                    $theJoinTable->removeForeignKey($fkName);
727
                    break;
728
                }
729
            }
730
            $blacklistedFks[$compositeName] = true;
731 7
        } elseif ( ! isset($blacklistedFks[$compositeName])) {
732 7
            $addedFks[$compositeName] = ['foreignTableName' => $foreignTableName, 'foreignColumns' => $foreignColumns];
733 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

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