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