Failed Conditions
Pull Request — master (#6392)
by Alessandro
11:37
created

SchemaTool::gatherColumns()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5

Importance

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