Failed Conditions
Pull Request — 2.6 (#7235)
by Aleksey
10:51
created

SchemaTool::processingNotRequired()   B

Complexity

Conditions 5
Paths 7

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 5

Importance

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

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