Passed
Pull Request — master (#6767)
by Tyler
14:13
created

SchemaTool   F

Complexity

Total Complexity 145

Size/Duplication

Total Lines 846
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 21

Test Coverage

Coverage 87.04%

Importance

Changes 0
Metric Value
wmc 145
lcom 1
cbo 21
dl 0
loc 846
ccs 329
cts 378
cp 0.8704
rs 1.263
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A createSchema() 0 13 3
B processingNotRequired() 0 9 5
A getCreateSchemaSql() 0 6 1
B getDefiningClass() 0 23 6
A dropSchema() 0 13 3
A dropDatabase() 0 9 2
A getDropDatabaseSQL() 0 10 1
C getDropSchemaSQL() 0 45 12
A updateSchema() 0 9 2
A getUpdateSchemaSql() 0 16 2
F getSchemaFromMetadata() 0 213 43
B addDiscriminatorColumnDefinition() 0 22 5
B gatherColumns() 0 16 5
F gatherColumn() 0 68 20
C gatherRelationsSql() 0 60 10
F gatherRelationJoinColumns() 0 115 24

How to fix   Complexity   

Complex Class

Complex classes like SchemaTool often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SchemaTool, and based on these observations, apply Extract Interface, too.

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
                    if ( ! isset($mapping['inherited'])) {
188 60
                        $this->gatherColumn($class, $mapping, $table);
189
                    }
190
                }
191
192 60
                $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
193
194
                // Add the discriminator column only to the root table
195 60
                if ($class->name == $class->rootEntityName) {
196 60
                    $this->addDiscriminatorColumnDefinition($class, $table);
197
                } else {
198
                    // Add an ID FK column to child tables
199 59
                    $pkColumns = [];
200 59
                    $inheritedKeyColumns = [];
201 59
                    foreach ($class->identifier as $identifierField) {
202 59
                        if (isset($class->fieldMappings[$identifierField]['inherited'])) {
203 59
                            $idMapping = $class->fieldMappings[$identifierField];
204 59
                            $this->gatherColumn($class, $idMapping, $table);
205 59
                            $columnName = $this->quoteStrategy->getColumnName(
206 59
                                $identifierField,
207 59
                                $class,
208 59
                                $this->platform
209
                            );
210
                            // TODO: This seems rather hackish, can we optimize it?
211 59
                            $table->getColumn($columnName)->setAutoincrement(false);
212
213 59
                            $pkColumns[] = $columnName;
214 59
                            $inheritedKeyColumns[] = $columnName;
215 2
                        } else if (isset($class->associationMappings[$identifierField]['inherited'])) {
216 1
                            $idMapping = $class->associationMappings[$identifierField];
217 1
                            $targetEntity = current(array_filter(
218 1
                                $classes,
219 1
                                function($class) use ($idMapping) {
220 1
                                    return $class->name === $idMapping['targetEntity'];
221 1
                                }));
222 1
                            foreach ($idMapping['joinColumns'] as $joinColumn) {
223 1
                                if (isset($targetEntity->fieldMappings[$joinColumn['referencedColumnName']])) {
224
                                    $idMapping =
225 1
                                        $targetEntity->fieldMappings[$joinColumn['referencedColumnName']];
226 1
                                    $columnName = $this->quoteStrategy->getJoinColumnName(
227 1
                                        $joinColumn,
228 1
                                        $class,
229 1
                                        $this->platform
230
                                    );
231 1
                                    $pkColumns[] = $columnName;
232 59
                                    $inheritedKeyColumns[] = $columnName;
233
                                }
234
                            }
235
                        }
236
                    }
237 59
                    if ( ! empty($inheritedKeyColumns)) {
238
                        // Add a FK constraint on the ID column
239 59
                        $table->addForeignKeyConstraint(
240 59
                            $this->quoteStrategy->getTableName(
241 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...
242 59
                                $this->platform
243
                            ),
244 59
                            $inheritedKeyColumns,
245 59
                            $inheritedKeyColumns,
246 59
                            ['onDelete' => 'CASCADE']
247
                        );
248
                    }
249 59
                    if (!empty($pkColumns)) {
250 60
                        $table->setPrimaryKey($pkColumns);
251
                    }
252
                }
253 270
            } elseif ($class->isInheritanceTypeTablePerClass()) {
254
                throw ORMException::notSupported();
255
            } else {
256 270
                $this->gatherColumns($class, $table);
257 270
                $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
258
            }
259
260 296
            $pkColumns = [];
261
262 296
            foreach ($class->identifier as $identifierField) {
263 296
                if (isset($class->fieldMappings[$identifierField])) {
264 296
                    $pkColumns[] = $this->quoteStrategy->getColumnName($identifierField, $class, $this->platform);
265 31
                } elseif (isset($class->associationMappings[$identifierField])) {
266
                    /* @var $assoc \Doctrine\ORM\Mapping\OneToOne */
267 31
                    $assoc = $class->associationMappings[$identifierField];
268
269 31
                    foreach ($assoc['joinColumns'] as $joinColumn) {
270 296
                        $pkColumns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
271
                    }
272
                }
273
            }
274
275 296
            if ( ! $table->hasIndex('primary')) {
276 296
                $table->setPrimaryKey($pkColumns);
277
            }
278
279
            // there can be unique indexes automatically created for join column
280
            // if join column is also primary key we should keep only primary key on this column
281
            // so, remove indexes overruled by primary key
282 296
            $primaryKey = $table->getIndex('primary');
283
284 296
            foreach ($table->getIndexes() as $idxKey => $existingIndex) {
285 296
                if ($primaryKey->overrules($existingIndex)) {
286 296
                    $table->dropIndex($idxKey);
287
                }
288
            }
289
290 296
            if (isset($class->table['indexes'])) {
291 1
                foreach ($class->table['indexes'] as $indexName => $indexData) {
292 1
                    if ( ! isset($indexData['flags'])) {
293 1
                        $indexData['flags'] = [];
294
                    }
295
296 1
                    $table->addIndex($indexData['columns'], is_numeric($indexName) ? null : $indexName, (array) $indexData['flags'], $indexData['options'] ?? []);
297
                }
298
            }
299
300 296
            if (isset($class->table['uniqueConstraints'])) {
301 4
                foreach ($class->table['uniqueConstraints'] as $indexName => $indexData) {
302 4
                    $uniqIndex = new Index($indexName, $indexData['columns'], true, false, [], $indexData['options'] ?? []);
303
304 4
                    foreach ($table->getIndexes() as $tableIndexName => $tableIndex) {
305 4
                        if ($tableIndex->isFullfilledBy($uniqIndex)) {
306 3
                            $table->dropIndex($tableIndexName);
307 4
                            break;
308
                        }
309
                    }
310
311 4
                    $table->addUniqueIndex($indexData['columns'], is_numeric($indexName) ? null : $indexName, $indexData['options'] ?? []);
312
                }
313
            }
314
315 296
            if (isset($class->table['options'])) {
316 1
                foreach ($class->table['options'] as $key => $val) {
317 1
                    $table->addOption($key, $val);
318
                }
319
            }
320
321 296
            $processedClasses[$class->name] = true;
322
323 296
            if ($class->isIdGeneratorSequence() && $class->name == $class->rootEntityName) {
324
                $seqDef     = $class->sequenceGeneratorDefinition;
325
                $quotedName = $this->quoteStrategy->getSequenceName($seqDef, $class, $this->platform);
326
                if ( ! $schema->hasSequence($quotedName)) {
327
                    $schema->createSequence(
328
                        $quotedName,
329
                        $seqDef['allocationSize'],
330
                        $seqDef['initialValue']
331
                    );
332
                }
333
            }
334
335 296
            if ($eventManager->hasListeners(ToolEvents::postGenerateSchemaTable)) {
336 1
                $eventManager->dispatchEvent(
337 1
                    ToolEvents::postGenerateSchemaTable,
338 296
                    new GenerateSchemaTableEventArgs($class, $schema, $table)
339
                );
340
            }
341
        }
342
343 296
        if ( ! $this->platform->supportsSchemas() && ! $this->platform->canEmulateSchemas()) {
344 9
            $schema->visit(new RemoveNamespacedAssets());
345
        }
346
347 296
        if ($eventManager->hasListeners(ToolEvents::postGenerateSchema)) {
348 1
            $eventManager->dispatchEvent(
349 1
                ToolEvents::postGenerateSchema,
350 1
                new GenerateSchemaEventArgs($this->em, $schema)
351
            );
352
        }
353
354 296
        return $schema;
355
    }
356
357
    /**
358
     * Gets a portable column definition as required by the DBAL for the discriminator
359
     * column of a class.
360
     *
361
     * @param ClassMetadata $class
362
     * @param Table         $table
363
     *
364
     * @return void
365
     */
366 91
    private function addDiscriminatorColumnDefinition($class, Table $table)
367
    {
368 91
        $discrColumn = $class->discriminatorColumn;
369
370 91
        if ( ! isset($discrColumn['type']) ||
371 91
            (strtolower($discrColumn['type']) == 'string' && ! isset($discrColumn['length']))
372
        ) {
373 1
            $discrColumn['type'] = 'string';
374 1
            $discrColumn['length'] = 255;
375
        }
376
377
        $options = [
378 91
            'length'    => $discrColumn['length'] ?? null,
379
            'notnull'   => true
380
        ];
381
382 91
        if (isset($discrColumn['columnDefinition'])) {
383
            $options['columnDefinition'] = $discrColumn['columnDefinition'];
384
        }
385
386 91
        $table->addColumn($discrColumn['name'], $discrColumn['type'], $options);
387 91
    }
388
389
    /**
390
     * Gathers the column definitions as required by the DBAL of all field mappings
391
     * found in the given class.
392
     *
393
     * @param ClassMetadata $class
394
     * @param Table         $table
395
     *
396
     * @return void
397
     */
398 277
    private function gatherColumns($class, Table $table)
399
    {
400 277
        $pkColumns = [];
401
402 277
        foreach ($class->fieldMappings as $mapping) {
403 277
            if ($class->isInheritanceTypeSingleTable() && isset($mapping['inherited'])) {
404 34
                continue;
405
            }
406
407 277
            $this->gatherColumn($class, $mapping, $table);
408
409 277
            if ($class->isIdentifier($mapping['fieldName'])) {
410 277
                $pkColumns[] = $this->quoteStrategy->getColumnName($mapping['fieldName'], $class, $this->platform);
411
            }
412
        }
413 277
    }
414
415
    /**
416
     * Creates a column definition as required by the DBAL from an ORM field mapping definition.
417
     *
418
     * @param ClassMetadata $class   The class that owns the field mapping.
419
     * @param array         $mapping The field mapping.
420
     * @param Table         $table
421
     *
422
     * @return void
423
     */
424 296
    private function gatherColumn($class, array $mapping, Table $table)
425
    {
426 296
        $columnName = $this->quoteStrategy->getColumnName($mapping['fieldName'], $class, $this->platform);
427 296
        $columnType = $mapping['type'];
428
429 296
        $options = [];
430 296
        $options['length'] = $mapping['length'] ?? null;
431 296
        $options['notnull'] = isset($mapping['nullable']) ? ! $mapping['nullable'] : true;
432 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...
433 7
            $options['notnull'] = false;
434
        }
435
436 296
        $options['platformOptions'] = [];
437 296
        $options['platformOptions']['version'] = $class->isVersioned && $class->versionField === $mapping['fieldName'];
438
439 296
        if (strtolower($columnType) === 'string' && null === $options['length']) {
440 154
            $options['length'] = 255;
441
        }
442
443 296
        if (isset($mapping['precision'])) {
444 295
            $options['precision'] = $mapping['precision'];
445
        }
446
447 296
        if (isset($mapping['scale'])) {
448 295
            $options['scale'] = $mapping['scale'];
449
        }
450
451 296
        if (isset($mapping['default'])) {
452 30
            $options['default'] = $mapping['default'];
453
        }
454
455 296
        if (isset($mapping['columnDefinition'])) {
456 1
            $options['columnDefinition'] = $mapping['columnDefinition'];
457
        }
458
459 296
        if (isset($mapping['options'])) {
460 3
            $knownOptions = ['comment', 'unsigned', 'fixed', 'default'];
461
462 3
            foreach ($knownOptions as $knownOption) {
463 3
                if (array_key_exists($knownOption, $mapping['options'])) {
464 2
                    $options[$knownOption] = $mapping['options'][$knownOption];
465
466 3
                    unset($mapping['options'][$knownOption]);
467
                }
468
            }
469
470 3
            $options['customSchemaOptions'] = $mapping['options'];
471
        }
472
473 296
        if ($class->isIdGeneratorIdentity() && $class->getIdentifierFieldNames() == [$mapping['fieldName']]) {
474 257
            $options['autoincrement'] = true;
475
        }
476 296
        if ($class->isInheritanceTypeJoined() && $class->name !== $class->rootEntityName) {
477 59
            $options['autoincrement'] = false;
478
        }
479
480 296
        if ($table->hasColumn($columnName)) {
481
            // required in some inheritance scenarios
482
            $table->changeColumn($columnName, $options);
483
        } else {
484 296
            $table->addColumn($columnName, $columnType, $options);
485
        }
486
487 296
        $isUnique = $mapping['unique'] ?? false;
488 296
        if ($isUnique) {
489 18
            $table->addUniqueIndex([$columnName]);
490
        }
491 296
    }
492
493
    /**
494
     * Gathers the SQL for properly setting up the relations of the given class.
495
     * This includes the SQL for foreign key constraints and join tables.
496
     *
497
     * @param ClassMetadata $class
498
     * @param Table         $table
499
     * @param Schema        $schema
500
     * @param array         $addedFks
501
     * @param array         $blacklistedFks
502
     *
503
     * @return void
504
     *
505
     * @throws \Doctrine\ORM\ORMException
506
     */
507 296
    private function gatherRelationsSql($class, $table, $schema, &$addedFks, &$blacklistedFks)
508
    {
509 296
        foreach ($class->associationMappings as $id => $mapping) {
510 205
            if (isset($mapping['inherited']) && array_search($id, $class->identifier) === false) {
511 21
                continue;
512
            }
513
514 205
            $foreignClass = $this->em->getClassMetadata($mapping['targetEntity']);
515
516 205
            if ($mapping['type'] & ClassMetadata::TO_ONE && $mapping['isOwningSide']) {
517 183
                $primaryKeyColumns = []; // PK is unnecessary for this relation-type
518
519 183
                $this->gatherRelationJoinColumns(
520 183
                    $mapping['joinColumns'],
521 183
                    $table,
522 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...
523 183
                    $mapping,
524 183
                    $primaryKeyColumns,
525 183
                    $addedFks,
526 183
                    $blacklistedFks
527
                );
528 150
            } elseif ($mapping['type'] == ClassMetadata::ONE_TO_MANY && $mapping['isOwningSide']) {
529
                //... create join table, one-many through join table supported later
530
                throw ORMException::notSupported();
531 150
            } elseif ($mapping['type'] == ClassMetadata::MANY_TO_MANY && $mapping['isOwningSide']) {
532
                // create join table
533 46
                $joinTable = $mapping['joinTable'];
534
535 46
                $theJoinTable = $schema->createTable(
536 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...
537
                );
538
539 46
                $primaryKeyColumns = [];
540
541
                // Build first FK constraint (relation table => source table)
542 46
                $this->gatherRelationJoinColumns(
543 46
                    $joinTable['joinColumns'],
544 46
                    $theJoinTable,
545 46
                    $class,
546 46
                    $mapping,
547 46
                    $primaryKeyColumns,
548 46
                    $addedFks,
549 46
                    $blacklistedFks
550
                );
551
552
                // Build second FK constraint (relation table => target table)
553 46
                $this->gatherRelationJoinColumns(
554 46
                    $joinTable['inverseJoinColumns'],
555 46
                    $theJoinTable,
556 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...
557 46
                    $mapping,
558 46
                    $primaryKeyColumns,
559 46
                    $addedFks,
560 46
                    $blacklistedFks
561
                );
562
563 205
                $theJoinTable->setPrimaryKey($primaryKeyColumns);
564
            }
565
        }
566 296
    }
567
568
    /**
569
     * Gets the class metadata that is responsible for the definition of the referenced column name.
570
     *
571
     * Previously this was a simple task, but with DDC-117 this problem is actually recursive. If its
572
     * not a simple field, go through all identifier field names that are associations recursively and
573
     * find that referenced column name.
574
     *
575
     * TODO: Is there any way to make this code more pleasing?
576
     *
577
     * @param ClassMetadata $class
578
     * @param string        $referencedColumnName
579
     *
580
     * @return array (ClassMetadata, referencedFieldName)
581
     */
582 205
    private function getDefiningClass($class, $referencedColumnName)
583
    {
584 205
        $referencedFieldName = $class->getFieldName($referencedColumnName);
585
586 205
        if ($class->hasField($referencedFieldName)) {
587 205
            return [$class, $referencedFieldName];
588
        }
589
590 9
        if (in_array($referencedColumnName, $class->getIdentifierColumnNames())) {
591
            // it seems to be an entity as foreign key
592 9
            foreach ($class->getIdentifierFieldNames() as $fieldName) {
593 9
                if ($class->hasAssociation($fieldName)
594 9
                    && $class->getSingleAssociationJoinColumnName($fieldName) == $referencedColumnName) {
595 9
                    return $this->getDefiningClass(
596 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...
597 9
                        $class->getSingleAssociationReferencedJoinColumnName($fieldName)
598
                    );
599
                }
600
            }
601
        }
602
603
        return null;
604
    }
605
606
    /**
607
     * Gathers columns and fk constraints that are required for one part of relationship.
608
     *
609
     * @param array         $joinColumns
610
     * @param Table         $theJoinTable
611
     * @param ClassMetadata $class
612
     * @param array         $mapping
613
     * @param array         $primaryKeyColumns
614
     * @param array         $addedFks
615
     * @param array         $blacklistedFks
616
     *
617
     * @return void
618
     *
619
     * @throws \Doctrine\ORM\ORMException
620
     */
621 205
    private function gatherRelationJoinColumns(
622
        $joinColumns,
623
        $theJoinTable,
624
        $class,
625
        $mapping,
626
        &$primaryKeyColumns,
627
        &$addedFks,
628
        &$blacklistedFks
629
    )
630
    {
631 205
        $localColumns       = [];
632 205
        $foreignColumns     = [];
633 205
        $fkOptions          = [];
634 205
        $foreignTableName   = $this->quoteStrategy->getTableName($class, $this->platform);
635 205
        $uniqueConstraints  = [];
636
637 205
        foreach ($joinColumns as $joinColumn) {
638
639 205
            list($definingClass, $referencedFieldName) = $this->getDefiningClass(
640 205
                $class,
641 205
                $joinColumn['referencedColumnName']
642
            );
643
644 205
            if ( ! $definingClass) {
645
                throw new \Doctrine\ORM\ORMException(
646
                    "Column name `".$joinColumn['referencedColumnName']."` referenced for relation from ".
647
                    $mapping['sourceEntity']." towards ".$mapping['targetEntity']." does not exist."
648
                );
649
            }
650
651 205
            $quotedColumnName       = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
652 205
            $quotedRefColumnName    = $this->quoteStrategy->getReferencedJoinColumnName(
653 205
                $joinColumn,
654 205
                $class,
655 205
                $this->platform
656
            );
657
658 205
            $primaryKeyColumns[]    = $quotedColumnName;
659 205
            $localColumns[]         = $quotedColumnName;
660 205
            $foreignColumns[]       = $quotedRefColumnName;
661
662 205
            if ( ! $theJoinTable->hasColumn($quotedColumnName)) {
663
                // Only add the column to the table if it does not exist already.
664
                // It might exist already if the foreign key is mapped into a regular
665
                // property as well.
666
667 202
                $fieldMapping = $definingClass->getFieldMapping($referencedFieldName);
668
669 202
                $columnDef = null;
670 202
                if (isset($joinColumn['columnDefinition'])) {
671
                    $columnDef = $joinColumn['columnDefinition'];
672 202
                } elseif (isset($fieldMapping['columnDefinition'])) {
673 1
                    $columnDef = $fieldMapping['columnDefinition'];
674
                }
675
676 202
                $columnOptions = ['notnull' => false, 'columnDefinition' => $columnDef];
677
678 202
                if (isset($joinColumn['nullable'])) {
679 126
                    $columnOptions['notnull'] = ! $joinColumn['nullable'];
680
                }
681
682 202
                if (isset($fieldMapping['options'])) {
683
                    $columnOptions['options'] = $fieldMapping['options'];
684
                }
685
686 202
                if ($fieldMapping['type'] == "string" && isset($fieldMapping['length'])) {
687 3
                    $columnOptions['length'] = $fieldMapping['length'];
688 202
                } elseif ($fieldMapping['type'] == "decimal") {
689
                    $columnOptions['scale'] = $fieldMapping['scale'];
690
                    $columnOptions['precision'] = $fieldMapping['precision'];
691
                }
692
693 202
                $theJoinTable->addColumn($quotedColumnName, $fieldMapping['type'], $columnOptions);
694
            }
695
696 205
            if (isset($joinColumn['unique']) && $joinColumn['unique'] == true) {
697 63
                $uniqueConstraints[] = ['columns' => [$quotedColumnName]];
698
            }
699
700 205
            if (isset($joinColumn['onDelete'])) {
701 205
                $fkOptions['onDelete'] = $joinColumn['onDelete'];
702
            }
703
        }
704
705
        // Prefer unique constraints over implicit simple indexes created for foreign keys.
706
        // Also avoids index duplication.
707 205
        foreach ($uniqueConstraints as $indexName => $unique) {
708 63
            $theJoinTable->addUniqueIndex($unique['columns'], is_numeric($indexName) ? null : $indexName);
709
        }
710
711 205
        $compositeName = $theJoinTable->getName().'.'.implode('', $localColumns);
712 205
        if (isset($addedFks[$compositeName])
713 1
            && ($foreignTableName != $addedFks[$compositeName]['foreignTableName']
714 205
            || 0 < count(array_diff($foreignColumns, $addedFks[$compositeName]['foreignColumns'])))
715
        ) {
716 1
            foreach ($theJoinTable->getForeignKeys() as $fkName => $key) {
717 1
                if (0 === count(array_diff($key->getLocalColumns(), $localColumns))
718 1
                    && (($key->getForeignTableName() != $foreignTableName)
719 1
                    || 0 < count(array_diff($key->getForeignColumns(), $foreignColumns)))
720
                ) {
721 1
                    $theJoinTable->removeForeignKey($fkName);
722 1
                    break;
723
                }
724
            }
725 1
            $blacklistedFks[$compositeName] = true;
726 205
        } elseif ( ! isset($blacklistedFks[$compositeName])) {
727 205
            $addedFks[$compositeName] = ['foreignTableName' => $foreignTableName, 'foreignColumns' => $foreignColumns];
728 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...
729 205
                $foreignTableName,
730 205
                $localColumns,
731 205
                $foreignColumns,
732 205
                $fkOptions
733
            );
734
        }
735 205
    }
736
737
    /**
738
     * Drops the database schema for the given classes.
739
     *
740
     * In any way when an exception is thrown it is suppressed since drop was
741
     * issued for all classes of the schema and some probably just don't exist.
742
     *
743
     * @param array $classes
744
     *
745
     * @return void
746
     */
747 6
    public function dropSchema(array $classes)
748
    {
749 6
        $dropSchemaSql = $this->getDropSchemaSQL($classes);
750 6
        $conn = $this->em->getConnection();
751
752 6
        foreach ($dropSchemaSql as $sql) {
753
            try {
754 6
                $conn->executeQuery($sql);
755 6
            } catch (\Throwable $e) {
756
                // ignored
757
            }
758
        }
759 6
    }
760
761
    /**
762
     * Drops all elements in the database of the current connection.
763
     *
764
     * @return void
765
     */
766
    public function dropDatabase()
767
    {
768
        $dropSchemaSql = $this->getDropDatabaseSQL();
769
        $conn = $this->em->getConnection();
770
771
        foreach ($dropSchemaSql as $sql) {
772
            $conn->executeQuery($sql);
773
        }
774
    }
775
776
    /**
777
     * Gets the SQL needed to drop the database schema for the connections database.
778
     *
779
     * @return array
780
     */
781
    public function getDropDatabaseSQL()
782
    {
783
        $sm = $this->em->getConnection()->getSchemaManager();
784
        $schema = $sm->createSchema();
785
786
        $visitor = new DropSchemaSqlCollector($this->platform);
787
        $schema->visit($visitor);
788
789
        return $visitor->getQueries();
790
    }
791
792
    /**
793
     * Gets SQL to drop the tables defined by the passed classes.
794
     *
795
     * @param array $classes
796
     *
797
     * @return array
798
     */
799 6
    public function getDropSchemaSQL(array $classes)
800
    {
801 6
        $visitor = new DropSchemaSqlCollector($this->platform);
802 6
        $schema = $this->getSchemaFromMetadata($classes);
803
804 6
        $sm = $this->em->getConnection()->getSchemaManager();
805 6
        $fullSchema = $sm->createSchema();
806
807 6
        foreach ($fullSchema->getTables() as $table) {
808 6
            if ( ! $schema->hasTable($table->getName())) {
809 6
                foreach ($table->getForeignKeys() as $foreignKey) {
810
                    /* @var $foreignKey \Doctrine\DBAL\Schema\ForeignKeyConstraint */
811
                    if ($schema->hasTable($foreignKey->getForeignTableName())) {
812 6
                        $visitor->acceptForeignKey($table, $foreignKey);
813
                    }
814
                }
815
            } else {
816 6
                $visitor->acceptTable($table);
817 6
                foreach ($table->getForeignKeys() as $foreignKey) {
818 6
                    $visitor->acceptForeignKey($table, $foreignKey);
819
                }
820
            }
821
        }
822
823 6
        if ($this->platform->supportsSequences()) {
824
            foreach ($schema->getSequences() as $sequence) {
825
                $visitor->acceptSequence($sequence);
826
            }
827
828
            foreach ($schema->getTables() as $table) {
829
                /* @var $sequence Table */
830
                if ($table->hasPrimaryKey()) {
831
                    $columns = $table->getPrimaryKey()->getColumns();
832
                    if (count($columns) == 1) {
833
                        $checkSequence = $table->getName()."_".$columns[0]."_seq";
834
                        if ($fullSchema->hasSequence($checkSequence)) {
835
                            $visitor->acceptSequence($fullSchema->getSequence($checkSequence));
836
                        }
837
                    }
838
                }
839
            }
840
        }
841
842 6
        return $visitor->getQueries();
843
    }
844
845
    /**
846
     * Updates the database schema of the given classes by comparing the ClassMetadata
847
     * instances to the current database schema that is inspected.
848
     *
849
     * @param array   $classes
850
     * @param boolean $saveMode If TRUE, only performs a partial update
851
     *                          without dropping assets which are scheduled for deletion.
852
     *
853
     * @return void
854
     */
855
    public function updateSchema(array $classes, $saveMode = false)
856
    {
857
        $updateSchemaSql = $this->getUpdateSchemaSql($classes, $saveMode);
858
        $conn = $this->em->getConnection();
859
860
        foreach ($updateSchemaSql as $sql) {
861
            $conn->executeQuery($sql);
862
        }
863
    }
864
865
    /**
866
     * Gets the sequence of SQL statements that need to be performed in order
867
     * to bring the given class mappings in-synch with the relational schema.
868
     *
869
     * @param array   $classes  The classes to consider.
870
     * @param boolean $saveMode If TRUE, only generates SQL for a partial update
871
     *                          that does not include SQL for dropping assets which are scheduled for deletion.
872
     *
873
     * @return array The sequence of SQL statements.
874
     */
875 1
    public function getUpdateSchemaSql(array $classes, $saveMode = false)
876
    {
877 1
        $sm = $this->em->getConnection()->getSchemaManager();
878
879 1
        $fromSchema = $sm->createSchema();
880 1
        $toSchema = $this->getSchemaFromMetadata($classes);
881
882 1
        $comparator = new Comparator();
883 1
        $schemaDiff = $comparator->compare($fromSchema, $toSchema);
884
885 1
        if ($saveMode) {
886
            return $schemaDiff->toSaveSql($this->platform);
887
        }
888
889 1
        return $schemaDiff->toSql($this->platform);
890
    }
891
}
892