Completed
Pull Request — master (#5798)
by Sergey
12:33
created

SchemaTool::processingNotRequired()   B

Complexity

Conditions 5
Paths 7

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

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