Failed Conditions
Pull Request — 2.6 (#7875)
by
unknown
13:39
created

SchemaTool::updateSchema()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

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

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

736
            /** @scrutinizer ignore-deprecated */ $theJoinTable->addUnnamedForeignKeyConstraint(

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

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

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

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

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
940 2
            $filter           = $config->getSchemaAssetsFilter();
941
942 2
            if ($filter !== null) {
943
                // whitelist assets we already know about in $toSchema, use the existing filter otherwise
944
                $config->setSchemaAssetsFilter(static function ($asset) use ($filter, $toSchema) : bool {
945
                    $assetName = $asset instanceof AbstractAsset ? $asset->getName() : $asset;
946
947
                    return $toSchema->hasTable($assetName) || $toSchema->hasSequence($assetName) || $filter($asset);
948
                });
949
            }
950
        }
951
952
        try {
953 2
            return $sm->createSchema();
954
        } finally {
955 2
            if ($hasFilterMethod && $filter !== null) {
956
                // restore schema assets filter and filter-expression
957
                if ($filterExpression !== null) {
958
                    $config->setFilterSchemaAssetsExpression($filterExpression);
959
                } else {
960 2
                    $config->setSchemaAssetsFilter($filter);
961
                }
962
            }
963
        }
964
    }
965
}
966