Passed
Push — 2.7 ( 62fb49...c036c0 )
by Luís
08:55
created

SchemaTool::gatherColumnOptions()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 10
ccs 6
cts 6
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\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 1316
    public function __construct(EntityManagerInterface $em)
74
    {
75 1316
        $this->em               = $em;
76 1316
        $this->platform         = $em->getConnection()->getDatabasePlatform();
77 1316
        $this->quoteStrategy    = $em->getConfiguration()->getQuoteStrategy();
78 1316
    }
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 297
    public function createSchema(array $classes)
90
    {
91 297
        $createSchemaSql = $this->getCreateSchemaSql($classes);
92 297
        $conn = $this->em->getConnection();
93
94 297
        foreach ($createSchemaSql as $sql) {
95
            try {
96 297
                $conn->executeQuery($sql);
97 90
            } catch (\Throwable $e) {
98 297
                throw ToolsException::schemaToolFailure($sql, $e);
99
            }
100
        }
101 207
    }
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 297
    public function getCreateSchemaSql(array $classes)
112
    {
113 297
        $schema = $this->getSchemaFromMetadata($classes);
114
115 297
        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 308
    private function processingNotRequired($class, array $processedClasses)
127
    {
128
        return (
129 308
            isset($processedClasses[$class->name]) ||
130 308
            $class->isMappedSuperclass ||
131 308
            $class->isEmbeddedClass ||
132 308
            ($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 308
    public function getSchemaFromMetadata(array $classes)
146
    {
147
        // Reminder for processed classes, used for hierarchies
148 308
        $processedClasses       = [];
149 308
        $eventManager           = $this->em->getEventManager();
150 308
        $schemaManager          = $this->em->getConnection()->getSchemaManager();
151 308
        $metadataSchemaConfig   = $schemaManager->createSchemaConfig();
152
153 308
        $metadataSchemaConfig->setExplicitForeignKeyIndexes(false);
154 308
        $schema = new Schema([], [], $metadataSchemaConfig);
155
156 308
        $addedFks = [];
157 308
        $blacklistedFks = [];
158
159 308
        foreach ($classes as $class) {
160
            /** @var \Doctrine\ORM\Mapping\ClassMetadata $class */
161 308
            if ($this->processingNotRequired($class, $processedClasses)) {
162 39
                continue;
163
            }
164
165 308
            $table = $schema->createTable($this->quoteStrategy->getTableName($class, $this->platform));
166
167 308
            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 304
            } elseif ($class->isInheritanceTypeJoined()) {
187
                // Add all non-inherited fields as columns
188 61
                foreach ($class->fieldMappings as $fieldName => $mapping) {
189 61
                    if ( ! isset($mapping['inherited'])) {
190 61
                        $this->gatherColumn($class, $mapping, $table);
191
                    }
192
                }
193
194 61
                $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
195
196
                // Add the discriminator column only to the root table
197 61
                if ($class->name == $class->rootEntityName) {
198 61
                    $this->addDiscriminatorColumnDefinition($class, $table);
199
                } else {
200
                    // Add an ID FK column to child tables
201 60
                    $pkColumns           = [];
202 60
                    $inheritedKeyColumns = [];
203
204 60
                    foreach ($class->identifier as $identifierField) {
205 60
                        if (isset($class->fieldMappings[$identifierField]['inherited'])) {
206 60
                            $idMapping = $class->fieldMappings[$identifierField];
207 60
                            $this->gatherColumn($class, $idMapping, $table);
208 60
                            $columnName = $this->quoteStrategy->getColumnName(
209 60
                                $identifierField,
210 60
                                $class,
211 60
                                $this->platform
212
                            );
213
                            // TODO: This seems rather hackish, can we optimize it?
214 60
                            $table->getColumn($columnName)->setAutoincrement(false);
215
216 60
                            $pkColumns[] = $columnName;
217 60
                            $inheritedKeyColumns[] = $columnName;
218
219 60
                            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 1
                                    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 60
                    if ( ! empty($inheritedKeyColumns)) {
250
                        // Add a FK constraint on the ID column
251 60
                        $table->addForeignKeyConstraint(
252 60
                            $this->quoteStrategy->getTableName(
253 60
                                $this->em->getClassMetadata($class->rootEntityName),
254 60
                                $this->platform
255
                            ),
256 60
                            $inheritedKeyColumns,
257 60
                            $inheritedKeyColumns,
258 60
                            ['onDelete' => 'CASCADE']
259
                        );
260
                    }
261
262 60
                    if ( ! empty($pkColumns)) {
263 61
                        $table->setPrimaryKey($pkColumns);
264
                    }
265
                }
266 281
            } elseif ($class->isInheritanceTypeTablePerClass()) {
267
                throw ORMException::notSupported();
268
            } else {
269 281
                $this->gatherColumns($class, $table);
270 281
                $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
271
            }
272
273 308
            $pkColumns = [];
274
275 308
            foreach ($class->identifier as $identifierField) {
276 308
                if (isset($class->fieldMappings[$identifierField])) {
277 307
                    $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 308
                        $pkColumns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
284
                    }
285
                }
286
            }
287
288 308
            if ( ! $table->hasIndex('primary')) {
289 308
                $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 308
            $primaryKey = $table->getIndex('primary');
296
297 308
            foreach ($table->getIndexes() as $idxKey => $existingIndex) {
298 308
                if ($primaryKey->overrules($existingIndex)) {
299 308
                    $table->dropIndex($idxKey);
300
                }
301
            }
302
303 308
            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 308
            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 308
            if (isset($class->table['options'])) {
329 1
                foreach ($class->table['options'] as $key => $val) {
330 1
                    $table->addOption($key, $val);
331
                }
332
            }
333
334 308
            $processedClasses[$class->name] = true;
335
336 308
            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 308
            if ($eventManager->hasListeners(ToolEvents::postGenerateSchemaTable)) {
349 1
                $eventManager->dispatchEvent(
350 1
                    ToolEvents::postGenerateSchemaTable,
351 308
                    new GenerateSchemaTableEventArgs($class, $schema, $table)
352
                );
353
            }
354
        }
355
356 308
        if ( ! $this->platform->supportsSchemas() && ! $this->platform->canEmulateSchemas()) {
357 10
            $schema->visit(new RemoveNamespacedAssets());
358
        }
359
360 308
        if ($eventManager->hasListeners(ToolEvents::postGenerateSchema)) {
361 1
            $eventManager->dispatchEvent(
362 1
                ToolEvents::postGenerateSchema,
363 1
                new GenerateSchemaEventArgs($this->em, $schema)
364
            );
365
        }
366
367 308
        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 288
    private function gatherColumns($class, Table $table)
412
    {
413 288
        $pkColumns = [];
414
415 288
        foreach ($class->fieldMappings as $mapping) {
416 288
            if ($class->isInheritanceTypeSingleTable() && isset($mapping['inherited'])) {
417 34
                continue;
418
            }
419
420 288
            $this->gatherColumn($class, $mapping, $table);
421
422 288
            if ($class->isIdentifier($mapping['fieldName'])) {
423 288
                $pkColumns[] = $this->quoteStrategy->getColumnName($mapping['fieldName'], $class, $this->platform);
424
            }
425
        }
426 288
    }
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 308
    private function gatherColumn($class, array $mapping, Table $table)
438
    {
439 308
        $columnName = $this->quoteStrategy->getColumnName($mapping['fieldName'], $class, $this->platform);
440 308
        $columnType = $mapping['type'];
441
442 308
        $options = [];
443 308
        $options['length'] = $mapping['length'] ?? null;
444 308
        $options['notnull'] = isset($mapping['nullable']) ? ! $mapping['nullable'] : true;
445 308
        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 308
        $options['platformOptions'] = [];
450 308
        $options['platformOptions']['version'] = $class->isVersioned && $class->versionField === $mapping['fieldName'];
451
452 308
        if (strtolower($columnType) === 'string' && null === $options['length']) {
453 160
            $options['length'] = 255;
454
        }
455
456 308
        if (isset($mapping['precision'])) {
457 306
            $options['precision'] = $mapping['precision'];
458
        }
459
460 308
        if (isset($mapping['scale'])) {
461 306
            $options['scale'] = $mapping['scale'];
462
        }
463
464 308
        if (isset($mapping['default'])) {
465 32
            $options['default'] = $mapping['default'];
466
        }
467
468 308
        if (isset($mapping['columnDefinition'])) {
469 1
            $options['columnDefinition'] = $mapping['columnDefinition'];
470
        }
471
472
        // the 'default' option can be overwritten here
473 308
        $options = $this->gatherColumnOptions($mapping) + $options;
474
475 308
        if ($class->isIdGeneratorIdentity() && $class->getIdentifierFieldNames() == [$mapping['fieldName']]) {
476 265
            $options['autoincrement'] = true;
477
        }
478 308
        if ($class->isInheritanceTypeJoined() && $class->name !== $class->rootEntityName) {
479 60
            $options['autoincrement'] = false;
480
        }
481
482 308
        if ($table->hasColumn($columnName)) {
483
            // required in some inheritance scenarios
484
            $table->changeColumn($columnName, $options);
485
        } else {
486 308
            $table->addColumn($columnName, $columnType, $options);
487
        }
488
489 308
        $isUnique = $mapping['unique'] ?? false;
490 308
        if ($isUnique) {
491 18
            $table->addUniqueIndex([$columnName]);
492
        }
493 308
    }
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 308
    private function gatherRelationsSql($class, $table, $schema, &$addedFks, &$blacklistedFks)
510
    {
511 308
        foreach ($class->associationMappings as $id => $mapping) {
512 212
            if (isset($mapping['inherited']) && ! \in_array($id, $class->identifier, true)) {
513 21
                continue;
514
            }
515
516 212
            $foreignClass = $this->em->getClassMetadata($mapping['targetEntity']);
517
518 212
            if ($mapping['type'] & ClassMetadata::TO_ONE && $mapping['isOwningSide']) {
519 190
                $primaryKeyColumns = []; // PK is unnecessary for this relation-type
520
521 190
                $this->gatherRelationJoinColumns(
522 190
                    $mapping['joinColumns'],
523 190
                    $table,
524 190
                    $foreignClass,
525 190
                    $mapping,
526 190
                    $primaryKeyColumns,
527 190
                    $addedFks,
528 190
                    $blacklistedFks
529
                );
530 154
            } 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 154
            } elseif ($mapping['type'] == ClassMetadata::MANY_TO_MANY && $mapping['isOwningSide']) {
534
                // create join table
535 47
                $joinTable = $mapping['joinTable'];
536
537 47
                $theJoinTable = $schema->createTable(
538 47
                    $this->quoteStrategy->getJoinTableName($mapping, $foreignClass, $this->platform)
539
                );
540
541 47
                $primaryKeyColumns = [];
542
543
                // Build first FK constraint (relation table => source table)
544 47
                $this->gatherRelationJoinColumns(
545 47
                    $joinTable['joinColumns'],
546 47
                    $theJoinTable,
547 47
                    $class,
548 47
                    $mapping,
549 47
                    $primaryKeyColumns,
550 47
                    $addedFks,
551 47
                    $blacklistedFks
552
                );
553
554
                // Build second FK constraint (relation table => target table)
555 47
                $this->gatherRelationJoinColumns(
556 47
                    $joinTable['inverseJoinColumns'],
557 47
                    $theJoinTable,
558 47
                    $foreignClass,
559 47
                    $mapping,
560 47
                    $primaryKeyColumns,
561 47
                    $addedFks,
562 47
                    $blacklistedFks
563
                );
564
565 212
                $theJoinTable->setPrimaryKey($primaryKeyColumns);
566
            }
567
        }
568 308
    }
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 212
    private function getDefiningClass($class, $referencedColumnName)
585
    {
586 212
        $referencedFieldName = $class->getFieldName($referencedColumnName);
587
588 212
        if ($class->hasField($referencedFieldName)) {
589 212
            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 212
    private function gatherRelationJoinColumns(
624
        $joinColumns,
625
        $theJoinTable,
626
        $class,
627
        $mapping,
628
        &$primaryKeyColumns,
629
        &$addedFks,
630
        &$blacklistedFks
631
    )
632
    {
633 212
        $localColumns       = [];
634 212
        $foreignColumns     = [];
635 212
        $fkOptions          = [];
636 212
        $foreignTableName   = $this->quoteStrategy->getTableName($class, $this->platform);
637 212
        $uniqueConstraints  = [];
638
639 212
        foreach ($joinColumns as $joinColumn) {
640
641 212
            list($definingClass, $referencedFieldName) = $this->getDefiningClass(
642 212
                $class,
643 212
                $joinColumn['referencedColumnName']
644
            );
645
646 212
            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 212
            $quotedColumnName    = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
654 212
            $quotedRefColumnName = $this->quoteStrategy->getReferencedJoinColumnName(
655 212
                $joinColumn,
656 212
                $class,
657 212
                $this->platform
658
            );
659
660 212
            $primaryKeyColumns[] = $quotedColumnName;
661 212
            $localColumns[]      = $quotedColumnName;
662 212
            $foreignColumns[]    = $quotedRefColumnName;
663
664 212
            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 209
                $fieldMapping = $definingClass->getFieldMapping($referencedFieldName);
670
671 209
                $columnDef = null;
672 209
                if (isset($joinColumn['columnDefinition'])) {
673
                    $columnDef = $joinColumn['columnDefinition'];
674 209
                } elseif (isset($fieldMapping['columnDefinition'])) {
675 1
                    $columnDef = $fieldMapping['columnDefinition'];
676
                }
677
678 209
                $columnOptions = ['notnull' => false, 'columnDefinition' => $columnDef];
679
680 209
                if (isset($joinColumn['nullable'])) {
681 131
                    $columnOptions['notnull'] = ! $joinColumn['nullable'];
682
                }
683
684 209
                $columnOptions = $columnOptions + $this->gatherColumnOptions($fieldMapping);
685
686 209
                if ($fieldMapping['type'] == "string" && isset($fieldMapping['length'])) {
687 4
                    $columnOptions['length'] = $fieldMapping['length'];
688 208
                } elseif ($fieldMapping['type'] == "decimal") {
689
                    $columnOptions['scale'] = $fieldMapping['scale'];
690
                    $columnOptions['precision'] = $fieldMapping['precision'];
691
                }
692
693 209
                $theJoinTable->addColumn($quotedColumnName, $fieldMapping['type'], $columnOptions);
694
            }
695
696 212
            if (isset($joinColumn['unique']) && $joinColumn['unique'] == true) {
697 63
                $uniqueConstraints[] = ['columns' => [$quotedColumnName]];
698
            }
699
700 212
            if (isset($joinColumn['onDelete'])) {
701 212
                $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 212
        foreach ($uniqueConstraints as $indexName => $unique) {
708 63
            $theJoinTable->addUniqueIndex($unique['columns'], is_numeric($indexName) ? null : $indexName);
709
        }
710
711 212
        $compositeName = $theJoinTable->getName().'.'.implode('', $localColumns);
712 212
        if (isset($addedFks[$compositeName])
713 1
            && ($foreignTableName != $addedFks[$compositeName]['foreignTableName']
714 212
            || 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 212
        } elseif ( ! isset($blacklistedFks[$compositeName])) {
727 212
            $addedFks[$compositeName] = ['foreignTableName' => $foreignTableName, 'foreignColumns' => $foreignColumns];
728 212
            $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 212
                $foreignTableName,
730 212
                $localColumns,
731 212
                $foreignColumns,
732 212
                $fkOptions
733
            );
734
        }
735 212
    }
736
737
    /**
738
     * @param mixed[] $mapping
739
     *
740
     * @return mixed[]
741
     */
742 308
    private function gatherColumnOptions(array $mapping) : array
743
    {
744 308
        if (! isset($mapping['options'])) {
745 308
            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