Failed Conditions
Pull Request — 2.6 (#7119)
by
unknown
09:21
created

SchemaTool::addDiscriminatorColumnDefinition()   C

Complexity

Conditions 8
Paths 12

Size

Total Lines 28
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 8.0189

Importance

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

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