Failed Conditions
Push — 2.7 ( c036c0...266f0d )
by Jonathan
57:23 queued 50:07
created

lib/Doctrine/ORM/Tools/SchemaTool.php (1 issue)

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(
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