Failed Conditions
Pull Request — master (#6767)
by Tyler
15:17
created

SchemaTool::getCreateSchemaSql()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 1
crap 1
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 1275
    public function __construct(EntityManagerInterface $em)
72
    {
73 1275
        $this->em               = $em;
74 1275
        $this->platform         = $em->getConnection()->getDatabasePlatform();
75 1275
        $this->quoteStrategy    = $em->getConfiguration()->getQuoteStrategy();
76 1275
    }
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 286
    public function createSchema(array $classes)
88
    {
89 286
        $createSchemaSql = $this->getCreateSchemaSql($classes);
90 286
        $conn = $this->em->getConnection();
91
92 286
        foreach ($createSchemaSql as $sql) {
93
            try {
94 286
                $conn->executeQuery($sql);
95 90
            } catch (\Throwable $e) {
96 286
                throw ToolsException::schemaToolFailure($sql, $e);
97
            }
98
        }
99 196
    }
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 286
    public function getCreateSchemaSql(array $classes)
110
    {
111 286
        $schema = $this->getSchemaFromMetadata($classes);
112
113 286
        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 296
    private function processingNotRequired($class, array $processedClasses)
125
    {
126
        return (
127 296
            isset($processedClasses[$class->name]) ||
128 296
            $class->isMappedSuperclass ||
129 296
            $class->isEmbeddedClass ||
130 296
            ($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 296
    public function getSchemaFromMetadata(array $classes)
144
    {
145
        // Reminder for processed classes, used for hierarchies
146 296
        $processedClasses       = [];
147 296
        $eventManager           = $this->em->getEventManager();
148 296
        $schemaManager          = $this->em->getConnection()->getSchemaManager();
149 296
        $metadataSchemaConfig   = $schemaManager->createSchemaConfig();
150
151 296
        $metadataSchemaConfig->setExplicitForeignKeyIndexes(false);
152 296
        $schema = new Schema([], [], $metadataSchemaConfig);
153
154 296
        $addedFks = [];
155 296
        $blacklistedFks = [];
156
157 296
        foreach ($classes as $class) {
158
            /** @var \Doctrine\ORM\Mapping\ClassMetadata $class */
159 296
            if ($this->processingNotRequired($class, $processedClasses)) {
160 39
                continue;
161
            }
162
163 296
            $table = $schema->createTable($this->quoteStrategy->getTableName($class, $this->platform));
164
165 296
            if ($class->isInheritanceTypeSingleTable()) {
166 36
                $this->gatherColumns($class, $table);
167 36
                $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
168
169
                // Add the discriminator column
170 36
                $this->addDiscriminatorColumnDefinition($class, $table);
171
172
                // Aggregate all the information from all classes in the hierarchy
173 36
                foreach ($class->parentClasses as $parentClassName) {
174
                    // Parent class information is already contained in this class
175
                    $processedClasses[$parentClassName] = true;
176
                }
177
178 36
                foreach ($class->subClasses as $subClassName) {
179 34
                    $subClass = $this->em->getClassMetadata($subClassName);
180 34
                    $this->gatherColumns($subClass, $table);
0 ignored issues
show
Compatibility introduced by
$subClass of type object<Doctrine\Common\P...\Mapping\ClassMetadata> is not a sub-type of object<Doctrine\ORM\Mapping\ClassMetadata>. It seems like you assume a concrete implementation of the interface Doctrine\Common\Persistence\Mapping\ClassMetadata to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
181 34
                    $this->gatherRelationsSql($subClass, $table, $schema, $addedFks, $blacklistedFks);
0 ignored issues
show
Compatibility introduced by
$subClass of type object<Doctrine\Common\P...\Mapping\ClassMetadata> is not a sub-type of object<Doctrine\ORM\Mapping\ClassMetadata>. It seems like you assume a concrete implementation of the interface Doctrine\Common\Persistence\Mapping\ClassMetadata to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
182 36
                    $processedClasses[$subClassName] = true;
183
                }
184 292
            } elseif ($class->isInheritanceTypeJoined()) {
185
                // Add all non-inherited fields as columns
186 60
                foreach ($class->fieldMappings as $fieldName => $mapping) {
187 60 View Code Duplication
                    if ( ! isset($mapping['inherited'])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
188 60
                        $columnName = $this->quoteStrategy->getColumnName(
0 ignored issues
show
Unused Code introduced by
$columnName is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
189 60
                            $mapping['fieldName'],
190 60
                            $class,
191 60
                            $this->platform
192
                        );
193 60
                        $this->gatherColumn($class, $mapping, $table);
194
                    }
195
                }
196
197 60
                $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
198
199
                // Add the discriminator column only to the root table
200 60
                if ($class->name == $class->rootEntityName) {
201 60
                    $this->addDiscriminatorColumnDefinition($class, $table);
202
                } else {
203
                    // Add an ID FK column to child tables
204 59
                    $pkColumns = [];
205 59
                    $inheritedKeyColumns = [];
206 59
                    foreach ($class->identifier as $identifierField) {
207 59
                        if (isset($class->fieldMappings[$identifierField]['inherited'])) {
208 59
                            $idMapping = $class->fieldMappings[$identifierField];
209 59
                            $this->gatherColumn($class, $idMapping, $table);
210 59
                            $columnName = $this->quoteStrategy->getColumnName(
211 59
                                $identifierField,
212 59
                                $class,
213 59
                                $this->platform
214
                            );
215
                            // TODO: This seems rather hackish, can we optimize it?
216 59
                            $table->getColumn($columnName)->setAutoincrement(false);
217
218 59
                            $pkColumns[] = $columnName;
219 59
                            $inheritedKeyColumns[] = $columnName;
220 2
                        } else if (isset($class->associationMappings[$identifierField]['inherited'])) {
221 1
                            $idMapping = $class->associationMappings[$identifierField];
222 1
                            $targetEntity = current(array_filter(
223 1
                                $classes,
224 1
                                function ($class) use ($idMapping) {
225 1
                                    return $class->name === $idMapping['targetEntity'];
226 1
                                }));
227 1
                            foreach ($idMapping['joinColumns'] as $joinColumn) {
228 1
                                if (isset($targetEntity->fieldMappings[$joinColumn['referencedColumnName']])) {
229
                                    $idMapping =
230 1
                                        $targetEntity->fieldMappings[$joinColumn['referencedColumnName']];
231 1
                                    $columnName = $this->quoteStrategy->getJoinColumnName(
232 1
                                        $joinColumn,
233 1
                                        $class,
234 1
                                        $this->platform
235
                                    );
236 1
                                    $pkColumns[] = $columnName;
237 59
                                    $inheritedKeyColumns[] = $columnName;
238
                                }
239
                            }
240
                        }
241
                    }
242 59
                    if (!empty($inheritedKeyColumns)) {
243
                        // Add a FK constraint on the ID column
244 59
                        $table->addForeignKeyConstraint(
245 59
                            $this->quoteStrategy->getTableName(
246 59
                                $this->em->getClassMetadata($class->rootEntityName),
0 ignored issues
show
Compatibility introduced by
$this->em->getClassMetad...$class->rootEntityName) of type object<Doctrine\Common\P...\Mapping\ClassMetadata> is not a sub-type of object<Doctrine\ORM\Mapping\ClassMetadata>. It seems like you assume a concrete implementation of the interface Doctrine\Common\Persistence\Mapping\ClassMetadata to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
247 59
                                $this->platform
248
                            ),
249 59
                            $inheritedKeyColumns,
250 59
                            $inheritedKeyColumns,
251 59
                            ['onDelete' => 'CASCADE']
252
                        );
253
                    }
254 59
                    if (!empty($pkColumns)) {
255 60
                        $table->setPrimaryKey($pkColumns);
256
                    }
257
                }
258 270
            } elseif ($class->isInheritanceTypeTablePerClass()) {
259
                throw ORMException::notSupported();
260
            } else {
261 270
                $this->gatherColumns($class, $table);
262 270
                $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
263
            }
264
265 296
            $pkColumns = [];
266
267 296
            foreach ($class->identifier as $identifierField) {
268 296
                if (isset($class->fieldMappings[$identifierField])) {
269 296
                    $pkColumns[] = $this->quoteStrategy->getColumnName($identifierField, $class, $this->platform);
270 31
                } elseif (isset($class->associationMappings[$identifierField])) {
271
                    /* @var $assoc \Doctrine\ORM\Mapping\OneToOne */
272 31
                    $assoc = $class->associationMappings[$identifierField];
273
274 31
                    foreach ($assoc['joinColumns'] as $joinColumn) {
275 296
                        $pkColumns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
276
                    }
277
                }
278
            }
279
280 296
            if ( ! $table->hasIndex('primary')) {
281 296
                $table->setPrimaryKey($pkColumns);
282
            }
283
284
            // there can be unique indexes automatically created for join column
285
            // if join column is also primary key we should keep only primary key on this column
286
            // so, remove indexes overruled by primary key
287 296
            $primaryKey = $table->getIndex('primary');
288
289 296
            foreach ($table->getIndexes() as $idxKey => $existingIndex) {
290 296
                if ($primaryKey->overrules($existingIndex)) {
291 296
                    $table->dropIndex($idxKey);
292
                }
293
            }
294
295 296
            if (isset($class->table['indexes'])) {
296 1
                foreach ($class->table['indexes'] as $indexName => $indexData) {
297 1
                    if ( ! isset($indexData['flags'])) {
298 1
                        $indexData['flags'] = [];
299
                    }
300
301 1
                    $table->addIndex($indexData['columns'], is_numeric($indexName) ? null : $indexName, (array) $indexData['flags'], $indexData['options'] ?? []);
302
                }
303
            }
304
305 296
            if (isset($class->table['uniqueConstraints'])) {
306 4
                foreach ($class->table['uniqueConstraints'] as $indexName => $indexData) {
307 4
                    $uniqIndex = new Index($indexName, $indexData['columns'], true, false, [], $indexData['options'] ?? []);
308
309 4
                    foreach ($table->getIndexes() as $tableIndexName => $tableIndex) {
310 4
                        if ($tableIndex->isFullfilledBy($uniqIndex)) {
311 3
                            $table->dropIndex($tableIndexName);
312 4
                            break;
313
                        }
314
                    }
315
316 4
                    $table->addUniqueIndex($indexData['columns'], is_numeric($indexName) ? null : $indexName, $indexData['options'] ?? []);
317
                }
318
            }
319
320 296
            if (isset($class->table['options'])) {
321 1
                foreach ($class->table['options'] as $key => $val) {
322 1
                    $table->addOption($key, $val);
323
                }
324
            }
325
326 296
            $processedClasses[$class->name] = true;
327
328 296
            if ($class->isIdGeneratorSequence() && $class->name == $class->rootEntityName) {
329
                $seqDef     = $class->sequenceGeneratorDefinition;
330
                $quotedName = $this->quoteStrategy->getSequenceName($seqDef, $class, $this->platform);
331
                if ( ! $schema->hasSequence($quotedName)) {
332
                    $schema->createSequence(
333
                        $quotedName,
334
                        $seqDef['allocationSize'],
335
                        $seqDef['initialValue']
336
                    );
337
                }
338
            }
339
340 296
            if ($eventManager->hasListeners(ToolEvents::postGenerateSchemaTable)) {
341 1
                $eventManager->dispatchEvent(
342 1
                    ToolEvents::postGenerateSchemaTable,
343 296
                    new GenerateSchemaTableEventArgs($class, $schema, $table)
344
                );
345
            }
346
        }
347
348 296
        if ( ! $this->platform->supportsSchemas() && ! $this->platform->canEmulateSchemas() ) {
349 9
            $schema->visit(new RemoveNamespacedAssets());
350
        }
351
352 296
        if ($eventManager->hasListeners(ToolEvents::postGenerateSchema)) {
353 1
            $eventManager->dispatchEvent(
354 1
                ToolEvents::postGenerateSchema,
355 1
                new GenerateSchemaEventArgs($this->em, $schema)
356
            );
357
        }
358
359 296
        return $schema;
360
    }
361
362
    /**
363
     * Gets a portable column definition as required by the DBAL for the discriminator
364
     * column of a class.
365
     *
366
     * @param ClassMetadata $class
367
     * @param Table         $table
368
     *
369
     * @return void
370
     */
371 91
    private function addDiscriminatorColumnDefinition($class, Table $table)
372
    {
373 91
        $discrColumn = $class->discriminatorColumn;
374
375 91
        if ( ! isset($discrColumn['type']) ||
376 91
            (strtolower($discrColumn['type']) == 'string' && ! isset($discrColumn['length']))
377
        ) {
378 1
            $discrColumn['type'] = 'string';
379 1
            $discrColumn['length'] = 255;
380
        }
381
382
        $options = [
383 91
            'length'    => $discrColumn['length'] ?? null,
384
            'notnull'   => true
385
        ];
386
387 91
        if (isset($discrColumn['columnDefinition'])) {
388
            $options['columnDefinition'] = $discrColumn['columnDefinition'];
389
        }
390
391 91
        $table->addColumn($discrColumn['name'], $discrColumn['type'], $options);
392 91
    }
393
394
    /**
395
     * Gathers the column definitions as required by the DBAL of all field mappings
396
     * found in the given class.
397
     *
398
     * @param ClassMetadata $class
399
     * @param Table         $table
400
     *
401
     * @return void
402
     */
403 277
    private function gatherColumns($class, Table $table)
404
    {
405 277
        $pkColumns = [];
406
407 277
        foreach ($class->fieldMappings as $mapping) {
408 277
            if ($class->isInheritanceTypeSingleTable() && isset($mapping['inherited'])) {
409 34
                continue;
410
            }
411
412 277
            $this->gatherColumn($class, $mapping, $table);
413
414 277
            if ($class->isIdentifier($mapping['fieldName'])) {
415 277
                $pkColumns[] = $this->quoteStrategy->getColumnName($mapping['fieldName'], $class, $this->platform);
416
            }
417
        }
418 277
    }
419
420
    /**
421
     * Creates a column definition as required by the DBAL from an ORM field mapping definition.
422
     *
423
     * @param ClassMetadata $class   The class that owns the field mapping.
424
     * @param array         $mapping The field mapping.
425
     * @param Table         $table
426
     *
427
     * @return void
428
     */
429 296
    private function gatherColumn($class, array $mapping, Table $table)
430
    {
431 296
        $columnName = $this->quoteStrategy->getColumnName($mapping['fieldName'], $class, $this->platform);
432 296
        $columnType = $mapping['type'];
433
434 296
        $options = [];
435 296
        $options['length'] = $mapping['length'] ?? null;
436 296
        $options['notnull'] = isset($mapping['nullable']) ? ! $mapping['nullable'] : true;
437 296
        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...
438 7
            $options['notnull'] = false;
439
        }
440
441 296
        $options['platformOptions'] = [];
442 296
        $options['platformOptions']['version'] = $class->isVersioned && $class->versionField === $mapping['fieldName'];
443
444 296
        if (strtolower($columnType) === 'string' && null === $options['length']) {
445 154
            $options['length'] = 255;
446
        }
447
448 296
        if (isset($mapping['precision'])) {
449 295
            $options['precision'] = $mapping['precision'];
450
        }
451
452 296
        if (isset($mapping['scale'])) {
453 295
            $options['scale'] = $mapping['scale'];
454
        }
455
456 296
        if (isset($mapping['default'])) {
457 30
            $options['default'] = $mapping['default'];
458
        }
459
460 296
        if (isset($mapping['columnDefinition'])) {
461 1
            $options['columnDefinition'] = $mapping['columnDefinition'];
462
        }
463
464 296
        if (isset($mapping['options'])) {
465 3
            $knownOptions = ['comment', 'unsigned', 'fixed', 'default'];
466
467 3
            foreach ($knownOptions as $knownOption) {
468 3
                if (array_key_exists($knownOption, $mapping['options'])) {
469 2
                    $options[$knownOption] = $mapping['options'][$knownOption];
470
471 3
                    unset($mapping['options'][$knownOption]);
472
                }
473
            }
474
475 3
            $options['customSchemaOptions'] = $mapping['options'];
476
        }
477
478 296
        if ($class->isIdGeneratorIdentity() && $class->getIdentifierFieldNames() == [$mapping['fieldName']]) {
479 257
            $options['autoincrement'] = true;
480
        }
481 296
        if ($class->isInheritanceTypeJoined() && $class->name !== $class->rootEntityName) {
482 59
            $options['autoincrement'] = false;
483
        }
484
485 296
        if ($table->hasColumn($columnName)) {
486
            // required in some inheritance scenarios
487
            $table->changeColumn($columnName, $options);
488
        } else {
489 296
            $table->addColumn($columnName, $columnType, $options);
490
        }
491
492 296
        $isUnique = $mapping['unique'] ?? false;
493 296
        if ($isUnique) {
494 18
            $table->addUniqueIndex([$columnName]);
495
        }
496 296
    }
497
498
    /**
499
     * Gathers the SQL for properly setting up the relations of the given class.
500
     * This includes the SQL for foreign key constraints and join tables.
501
     *
502
     * @param ClassMetadata $class
503
     * @param Table         $table
504
     * @param Schema        $schema
505
     * @param array         $addedFks
506
     * @param array         $blacklistedFks
507
     *
508
     * @return void
509
     *
510
     * @throws \Doctrine\ORM\ORMException
511
     */
512 296
    private function gatherRelationsSql($class, $table, $schema, &$addedFks, &$blacklistedFks)
513
    {
514 296
        foreach ($class->associationMappings as $id => $mapping) {
515 205
            if (isset($mapping['inherited']) && array_search($id, $class->identifier) === false) {
516 21
                continue;
517
            }
518
519 205
            $foreignClass = $this->em->getClassMetadata($mapping['targetEntity']);
520
521 205
            if ($mapping['type'] & ClassMetadata::TO_ONE && $mapping['isOwningSide']) {
522 183
                $primaryKeyColumns = []; // PK is unnecessary for this relation-type
523
524 183
                $this->gatherRelationJoinColumns(
525 183
                    $mapping['joinColumns'],
526 183
                    $table,
527 183
                    $foreignClass,
0 ignored issues
show
Compatibility introduced by
$foreignClass of type object<Doctrine\Common\P...\Mapping\ClassMetadata> is not a sub-type of object<Doctrine\ORM\Mapping\ClassMetadata>. It seems like you assume a concrete implementation of the interface Doctrine\Common\Persistence\Mapping\ClassMetadata to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
528 183
                    $mapping,
529 183
                    $primaryKeyColumns,
530 183
                    $addedFks,
531 183
                    $blacklistedFks
532
                );
533 150
            } elseif ($mapping['type'] == ClassMetadata::ONE_TO_MANY && $mapping['isOwningSide']) {
534
                //... create join table, one-many through join table supported later
535
                throw ORMException::notSupported();
536 150
            } elseif ($mapping['type'] == ClassMetadata::MANY_TO_MANY && $mapping['isOwningSide']) {
537
                // create join table
538 46
                $joinTable = $mapping['joinTable'];
539
540 46
                $theJoinTable = $schema->createTable(
541 46
                    $this->quoteStrategy->getJoinTableName($mapping, $foreignClass, $this->platform)
0 ignored issues
show
Compatibility introduced by
$foreignClass of type object<Doctrine\Common\P...\Mapping\ClassMetadata> is not a sub-type of object<Doctrine\ORM\Mapping\ClassMetadata>. It seems like you assume a concrete implementation of the interface Doctrine\Common\Persistence\Mapping\ClassMetadata to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
542
                );
543
544 46
                $primaryKeyColumns = [];
545
546
                // Build first FK constraint (relation table => source table)
547 46
                $this->gatherRelationJoinColumns(
548 46
                    $joinTable['joinColumns'],
549 46
                    $theJoinTable,
550 46
                    $class,
551 46
                    $mapping,
552 46
                    $primaryKeyColumns,
553 46
                    $addedFks,
554 46
                    $blacklistedFks
555
                );
556
557
                // Build second FK constraint (relation table => target table)
558 46
                $this->gatherRelationJoinColumns(
559 46
                    $joinTable['inverseJoinColumns'],
560 46
                    $theJoinTable,
561 46
                    $foreignClass,
0 ignored issues
show
Compatibility introduced by
$foreignClass of type object<Doctrine\Common\P...\Mapping\ClassMetadata> is not a sub-type of object<Doctrine\ORM\Mapping\ClassMetadata>. It seems like you assume a concrete implementation of the interface Doctrine\Common\Persistence\Mapping\ClassMetadata to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
562 46
                    $mapping,
563 46
                    $primaryKeyColumns,
564 46
                    $addedFks,
565 46
                    $blacklistedFks
566
                );
567
568 205
                $theJoinTable->setPrimaryKey($primaryKeyColumns);
569
            }
570
        }
571 296
    }
572
573
    /**
574
     * Gets the class metadata that is responsible for the definition of the referenced column name.
575
     *
576
     * Previously this was a simple task, but with DDC-117 this problem is actually recursive. If its
577
     * not a simple field, go through all identifier field names that are associations recursively and
578
     * find that referenced column name.
579
     *
580
     * TODO: Is there any way to make this code more pleasing?
581
     *
582
     * @param ClassMetadata $class
583
     * @param string        $referencedColumnName
584
     *
585
     * @return array (ClassMetadata, referencedFieldName)
586
     */
587 205
    private function getDefiningClass($class, $referencedColumnName)
588
    {
589 205
        $referencedFieldName = $class->getFieldName($referencedColumnName);
590
591 205
        if ($class->hasField($referencedFieldName)) {
592 205
            return [$class, $referencedFieldName];
593
        }
594
595 9
        if (in_array($referencedColumnName, $class->getIdentifierColumnNames())) {
596
            // it seems to be an entity as foreign key
597 9
            foreach ($class->getIdentifierFieldNames() as $fieldName) {
598 9
                if ($class->hasAssociation($fieldName)
599 9
                    && $class->getSingleAssociationJoinColumnName($fieldName) == $referencedColumnName) {
600 9
                    return $this->getDefiningClass(
601 9
                        $this->em->getClassMetadata($class->associationMappings[$fieldName]['targetEntity']),
0 ignored issues
show
Compatibility introduced by
$this->em->getClassMetad...dName]['targetEntity']) of type object<Doctrine\Common\P...\Mapping\ClassMetadata> is not a sub-type of object<Doctrine\ORM\Mapping\ClassMetadata>. It seems like you assume a concrete implementation of the interface Doctrine\Common\Persistence\Mapping\ClassMetadata to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
602 9
                        $class->getSingleAssociationReferencedJoinColumnName($fieldName)
603
                    );
604
                }
605
            }
606
        }
607
608
        return null;
609
    }
610
611
    /**
612
     * Gathers columns and fk constraints that are required for one part of relationship.
613
     *
614
     * @param array         $joinColumns
615
     * @param Table         $theJoinTable
616
     * @param ClassMetadata $class
617
     * @param array         $mapping
618
     * @param array         $primaryKeyColumns
619
     * @param array         $addedFks
620
     * @param array         $blacklistedFks
621
     *
622
     * @return void
623
     *
624
     * @throws \Doctrine\ORM\ORMException
625
     */
626 205
    private function gatherRelationJoinColumns(
627
        $joinColumns,
628
        $theJoinTable,
629
        $class,
630
        $mapping,
631
        &$primaryKeyColumns,
632
        &$addedFks,
633
        &$blacklistedFks
634
    )
635
    {
636 205
        $localColumns       = [];
637 205
        $foreignColumns     = [];
638 205
        $fkOptions          = [];
639 205
        $foreignTableName   = $this->quoteStrategy->getTableName($class, $this->platform);
640 205
        $uniqueConstraints  = [];
641
642 205
        foreach ($joinColumns as $joinColumn) {
643
644 205
            list($definingClass, $referencedFieldName) = $this->getDefiningClass(
645 205
                $class,
646 205
                $joinColumn['referencedColumnName']
647
            );
648
649 205
            if ( ! $definingClass) {
650
                throw new \Doctrine\ORM\ORMException(
651
                    "Column name `".$joinColumn['referencedColumnName']."` referenced for relation from ".
652
                    $mapping['sourceEntity'] . " towards ". $mapping['targetEntity'] . " does not exist."
653
                );
654
            }
655
656 205
            $quotedColumnName       = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
657 205
            $quotedRefColumnName    = $this->quoteStrategy->getReferencedJoinColumnName(
658 205
                $joinColumn,
659 205
                $class,
660 205
                $this->platform
661
            );
662
663 205
            $primaryKeyColumns[]    = $quotedColumnName;
664 205
            $localColumns[]         = $quotedColumnName;
665 205
            $foreignColumns[]       = $quotedRefColumnName;
666
667 205
            if ( ! $theJoinTable->hasColumn($quotedColumnName)) {
668
                // Only add the column to the table if it does not exist already.
669
                // It might exist already if the foreign key is mapped into a regular
670
                // property as well.
671
672 202
                $fieldMapping = $definingClass->getFieldMapping($referencedFieldName);
673
674 202
                $columnDef = null;
675 202
                if (isset($joinColumn['columnDefinition'])) {
676
                    $columnDef = $joinColumn['columnDefinition'];
677 202
                } elseif (isset($fieldMapping['columnDefinition'])) {
678 1
                    $columnDef = $fieldMapping['columnDefinition'];
679
                }
680
681 202
                $columnOptions = ['notnull' => false, 'columnDefinition' => $columnDef];
682
683 202
                if (isset($joinColumn['nullable'])) {
684 126
                    $columnOptions['notnull'] = !$joinColumn['nullable'];
685
                }
686
687 202
                if (isset($fieldMapping['options'])) {
688
                    $columnOptions['options'] = $fieldMapping['options'];
689
                }
690
691 202
                if ($fieldMapping['type'] == "string" && isset($fieldMapping['length'])) {
692 3
                    $columnOptions['length'] = $fieldMapping['length'];
693 202
                } elseif ($fieldMapping['type'] == "decimal") {
694
                    $columnOptions['scale'] = $fieldMapping['scale'];
695
                    $columnOptions['precision'] = $fieldMapping['precision'];
696
                }
697
698 202
                $theJoinTable->addColumn($quotedColumnName, $fieldMapping['type'], $columnOptions);
699
            }
700
701 205
            if (isset($joinColumn['unique']) && $joinColumn['unique'] == true) {
702 63
                $uniqueConstraints[] = ['columns' => [$quotedColumnName]];
703
            }
704
705 205
            if (isset($joinColumn['onDelete'])) {
706 205
                $fkOptions['onDelete'] = $joinColumn['onDelete'];
707
            }
708
        }
709
710
        // Prefer unique constraints over implicit simple indexes created for foreign keys.
711
        // Also avoids index duplication.
712 205
        foreach ($uniqueConstraints as $indexName => $unique) {
713 63
            $theJoinTable->addUniqueIndex($unique['columns'], is_numeric($indexName) ? null : $indexName);
714
        }
715
716 205
        $compositeName = $theJoinTable->getName().'.'.implode('', $localColumns);
717 205
        if (isset($addedFks[$compositeName])
718 1
            && ($foreignTableName != $addedFks[$compositeName]['foreignTableName']
719 205
            || 0 < count(array_diff($foreignColumns, $addedFks[$compositeName]['foreignColumns'])))
720
        ) {
721 1
            foreach ($theJoinTable->getForeignKeys() as $fkName => $key) {
722 1
                if (0 === count(array_diff($key->getLocalColumns(), $localColumns))
723 1
                    && (($key->getForeignTableName() != $foreignTableName)
724 1
                    || 0 < count(array_diff($key->getForeignColumns(), $foreignColumns)))
725
                ) {
726 1
                    $theJoinTable->removeForeignKey($fkName);
727 1
                    break;
728
                }
729
            }
730 1
            $blacklistedFks[$compositeName] = true;
731 205
        } elseif (!isset($blacklistedFks[$compositeName])) {
732 205
            $addedFks[$compositeName] = ['foreignTableName' => $foreignTableName, 'foreignColumns' => $foreignColumns];
733 205
            $theJoinTable->addUnnamedForeignKeyConstraint(
0 ignored issues
show
Deprecated Code introduced by
The method Doctrine\DBAL\Schema\Tab...dForeignKeyConstraint() has been deprecated with message: Use {@link addForeignKeyConstraint}

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

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

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