Completed
Pull Request — master (#6027)
by Raul
10:49
created

SchemaTool::getCreateSchemaSql()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 3
cts 3
cp 1
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 1
crap 1
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\ORM\Tools;
21
22
use Doctrine\ORM\ORMException;
23
use Doctrine\DBAL\Schema\Comparator;
24
use Doctrine\DBAL\Schema\Index;
25
use Doctrine\DBAL\Schema\Schema;
26
use Doctrine\DBAL\Schema\Table;
27
use Doctrine\DBAL\Schema\Visitor\DropSchemaSqlCollector;
28
use Doctrine\DBAL\Schema\Visitor\RemoveNamespacedAssets;
29
use Doctrine\ORM\EntityManagerInterface;
30
use Doctrine\ORM\Mapping\ClassMetadata;
31
use Doctrine\ORM\Tools\Event\GenerateSchemaTableEventArgs;
32
use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs;
33
34
/**
35
 * The SchemaTool is a tool to create/drop/update database schemas based on
36
 * <tt>ClassMetadata</tt> class descriptors.
37
 *
38
 * @link    www.doctrine-project.org
39
 * @since   2.0
40
 * @author  Guilherme Blanco <[email protected]>
41
 * @author  Jonathan Wage <[email protected]>
42
 * @author  Roman Borschel <[email protected]>
43
 * @author  Benjamin Eberlei <[email protected]>
44
 * @author  Stefano Rodriguez <[email protected]>
45
 */
46
class SchemaTool
47
{
48
    /**
49
     * @var \Doctrine\ORM\EntityManagerInterface
50
     */
51
    private $em;
52
53
    /**
54
     * @var \Doctrine\DBAL\Platforms\AbstractPlatform
55
     */
56
    private $platform;
57
58
    /**
59
     * The quote strategy.
60
     *
61
     * @var \Doctrine\ORM\Mapping\QuoteStrategy
62
     */
63
    private $quoteStrategy;
64
65
    /**
66
     * Initializes a new SchemaTool instance that uses the connection of the
67
     * provided EntityManager.
68
     *
69
     * @param \Doctrine\ORM\EntityManagerInterface $em
70
     */
71 1244
    public function __construct(EntityManagerInterface $em)
72
    {
73 1244
        $this->em               = $em;
74 1244
        $this->platform         = $em->getConnection()->getDatabasePlatform();
75 1244
        $this->quoteStrategy    = $em->getConfiguration()->getQuoteStrategy();
76 1244
    }
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 270
    public function createSchema(array $classes)
88
    {
89 270
        $createSchemaSql = $this->getCreateSchemaSql($classes);
90 270
        $conn = $this->em->getConnection();
91
92 270
        foreach ($createSchemaSql as $sql) {
93
            try {
94 270
                $conn->executeQuery($sql);
95 95
            } catch (\Exception $e) {
96 270
                throw ToolsException::schemaToolFailure($sql, $e);
97
            }
98
        }
99 175
    }
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 270
    public function getCreateSchemaSql(array $classes)
110
    {
111 270
        $schema = $this->getSchemaFromMetadata($classes);
112
113 270
        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 280
    private function processingNotRequired($class, array $processedClasses)
125
    {
126
        return (
127 280
            isset($processedClasses[$class->name]) ||
128 280
            $class->isMappedSuperclass ||
129 280
            $class->isEmbeddedClass ||
130 280
            ($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 280
    public function getSchemaFromMetadata(array $classes)
144
    {
145
        // Reminder for processed classes, used for hierarchies
146 280
        $processedClasses       = array();
147 280
        $eventManager           = $this->em->getEventManager();
148 280
        $schemaManager          = $this->em->getConnection()->getSchemaManager();
149 280
        $metadataSchemaConfig   = $schemaManager->createSchemaConfig();
150
151 280
        $metadataSchemaConfig->setExplicitForeignKeyIndexes(false);
152 280
        $schema = new Schema(array(), array(), $metadataSchemaConfig);
153
154 280
        $addedFks = array();
155 280
        $blacklistedFks = array();
156 280
        $m2mDuplicates = array();
157
158 280
        foreach ($classes as $class) {
159
            /** @var \Doctrine\ORM\Mapping\ClassMetadata $class */
160 280
            if ($this->processingNotRequired($class, $processedClasses)) {
161 36
                continue;
162
            }
163
164
            //Precedence of Entity over auto-created JoinTable on a ManyToMany relationship
165
166 280
            $tableName = $this->quoteStrategy->getTableName($class, $this->platform);
167
168 280
            if (isset($m2mDuplicates[$tableName]) && $schema->hasTable($tableName)) {
169 1
                $m2mDuplicates[$tableName]++;
170
171 1
                if ($m2mDuplicates[$tableName] < 3) {
172 1
                    $schema->dropTable($tableName);
173
                }
174
            }
175
176
177 280
            $table = $schema->createTable($this->quoteStrategy->getTableName($class, $this->platform));
178
179 280
            if ($class->isInheritanceTypeSingleTable()) {
180 32
                $this->gatherColumns($class, $table);
181 32
                $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks, $m2mDuplicates);
182
183
                // Add the discriminator column
184 32
                $this->addDiscriminatorColumnDefinition($class, $table);
185
186
                // Aggregate all the information from all classes in the hierarchy
187 32
                foreach ($class->parentClasses as $parentClassName) {
188
                    // Parent class information is already contained in this class
189
                    $processedClasses[$parentClassName] = true;
190
                }
191
192 32
                foreach ($class->subClasses as $subClassName) {
193 31
                    $subClass = $this->em->getClassMetadata($subClassName);
194 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...
195 31
                    $this->gatherRelationsSql($subClass, $table, $schema, $addedFks, $blacklistedFks, $m2mDuplicates);
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...
196 32
                    $processedClasses[$subClassName] = true;
197
                }
198 277
            } elseif ($class->isInheritanceTypeJoined()) {
199
                // Add all non-inherited fields as columns
200 50
                $pkColumns = array();
201 50
                foreach ($class->fieldMappings as $fieldName => $mapping) {
202 50
                    if ( ! isset($mapping['inherited'])) {
203 50
                        $columnName = $this->quoteStrategy->getColumnName(
204 50
                            $mapping['fieldName'],
205
                            $class,
206 50
                            $this->platform
207
                        );
208 50
                        $this->gatherColumn($class, $mapping, $table);
209
210 50
                        if ($class->isIdentifier($fieldName)) {
211 50
                            $pkColumns[] = $columnName;
212
                        }
213
                    }
214
                }
215
216 50
                $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks, $m2mDuplicates);
217
218
                // Add the discriminator column only to the root table
219 50
                if ($class->name == $class->rootEntityName) {
220 50
                    $this->addDiscriminatorColumnDefinition($class, $table);
221
                } else {
222
                    // Add an ID FK column to child tables
223 49
                    $inheritedKeyColumns = array();
224 49
                    foreach ($class->identifier as $identifierField) {
225 49
                        $idMapping = $class->fieldMappings[$identifierField];
226 49
                        if (isset($idMapping['inherited'])) {
227 49
                            $this->gatherColumn($class, $idMapping, $table);
228 49
                            $columnName = $this->quoteStrategy->getColumnName(
229
                                $identifierField,
230
                                $class,
231 49
                                $this->platform
232
                            );
233
                            // TODO: This seems rather hackish, can we optimize it?
234 49
                            $table->getColumn($columnName)->setAutoincrement(false);
235
236 49
                            $pkColumns[] = $columnName;
237 49
                            $inheritedKeyColumns[] = $columnName;
238
                        }
239
                    }
240 49
                    if (!empty($inheritedKeyColumns)) {
241
                        // Add a FK constraint on the ID column
242 49
                        $table->addForeignKeyConstraint(
243 49
                            $this->quoteStrategy->getTableName(
244 49
                                $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...
245 49
                                $this->platform
246
                            ),
247
                            $inheritedKeyColumns,
248
                            $inheritedKeyColumns,
249 49
                            array('onDelete' => 'CASCADE')
250
                        );
251
                    }
252
253
                }
254
255 50
                $table->setPrimaryKey($pkColumns);
256
257 263
            } elseif ($class->isInheritanceTypeTablePerClass()) {
258
                throw ORMException::notSupported();
259
            } else {
260 263
                $this->gatherColumns($class, $table);
261 263
                $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks, $m2mDuplicates);
262
            }
263
264 280
            $pkColumns = array();
265
266 280
            foreach ($class->identifier as $identifierField) {
267 280
                if (isset($class->fieldMappings[$identifierField])) {
268 280
                    $pkColumns[] = $this->quoteStrategy->getColumnName($identifierField, $class, $this->platform);
269 29
                } elseif (isset($class->associationMappings[$identifierField])) {
270
                    /* @var $assoc \Doctrine\ORM\Mapping\OneToOne */
271 29
                    $assoc = $class->associationMappings[$identifierField];
272
273 29
                    foreach ($assoc['joinColumns'] as $joinColumn) {
274 280
                        $pkColumns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
275
                    }
276
                }
277
            }
278
279 280
            if ( ! $table->hasIndex('primary')) {
280 269
                $table->setPrimaryKey($pkColumns);
281
            }
282
283
            // there can be unique indexes automatically created for join column
284
            // if join column is also primary key we should keep only primary key on this column
285
            // so, remove indexes overruled by primary key
286 280
            $primaryKey = $table->getIndex('primary');
287
288 280
            foreach ($table->getIndexes() as $idxKey => $existingIndex) {
289 280
                if ($primaryKey->overrules($existingIndex)) {
290 280
                    $table->dropIndex($idxKey);
291
                }
292
            }
293
294 280
            if (isset($class->table['indexes'])) {
295 1
                foreach ($class->table['indexes'] as $indexName => $indexData) {
296 1
                    if ( ! isset($indexData['flags'])) {
297 1
                        $indexData['flags'] = array();
298
                    }
299
300 1
                    $table->addIndex($indexData['columns'], is_numeric($indexName) ? null : $indexName, (array) $indexData['flags'], isset($indexData['options']) ? $indexData['options'] : array());
301
                }
302
            }
303
304 280
            if (isset($class->table['uniqueConstraints'])) {
305 4
                foreach ($class->table['uniqueConstraints'] as $indexName => $indexData) {
306 4
                    $uniqIndex = new Index($indexName, $indexData['columns'], true, false, [], isset($indexData['options']) ? $indexData['options'] : []);
307
308 4
                    foreach ($table->getIndexes() as $tableIndexName => $tableIndex) {
309 4
                        if ($tableIndex->isFullfilledBy($uniqIndex)) {
310 3
                            $table->dropIndex($tableIndexName);
311 4
                            break;
312
                        }
313
                    }
314
315 4
                    $table->addUniqueIndex($indexData['columns'], is_numeric($indexName) ? null : $indexName, isset($indexData['options']) ? $indexData['options'] : array());
316
                }
317
            }
318
319 280
            if (isset($class->table['options'])) {
320 1
                foreach ($class->table['options'] as $key => $val) {
321 1
                    $table->addOption($key, $val);
322
                }
323
            }
324
325 280
            $processedClasses[$class->name] = true;
326
327 280
            if ($class->isIdGeneratorSequence() && $class->name == $class->rootEntityName) {
328
                $seqDef     = $class->sequenceGeneratorDefinition;
329
                $quotedName = $this->quoteStrategy->getSequenceName($seqDef, $class, $this->platform);
330
                if ( ! $schema->hasSequence($quotedName)) {
331
                    $schema->createSequence(
332
                        $quotedName,
333
                        $seqDef['allocationSize'],
334
                        $seqDef['initialValue']
335
                    );
336
                }
337
            }
338
339 280
            if ($eventManager->hasListeners(ToolEvents::postGenerateSchemaTable)) {
340 1
                $eventManager->dispatchEvent(
341 1
                    ToolEvents::postGenerateSchemaTable,
342 280
                    new GenerateSchemaTableEventArgs($class, $schema, $table)
343
                );
344
            }
345
        }
346
347 280
        if ( ! $this->platform->supportsSchemas() && ! $this->platform->canEmulateSchemas() ) {
348 9
            $schema->visit(new RemoveNamespacedAssets());
349
        }
350
351 280
        if ($eventManager->hasListeners(ToolEvents::postGenerateSchema)) {
352 1
            $eventManager->dispatchEvent(
353 1
                ToolEvents::postGenerateSchema,
354 1
                new GenerateSchemaEventArgs($this->em, $schema)
355
            );
356
        }
357
358 280
        return $schema;
359
    }
360
361
    /**
362
     * Gets a portable column definition as required by the DBAL for the discriminator
363
     * column of a class.
364
     *
365
     * @param ClassMetadata $class
366
     * @param Table         $table
367
     *
368
     * @return array The portable column definition of the discriminator column as required by
369
     *               the DBAL.
370
     */
371 77
    private function addDiscriminatorColumnDefinition($class, Table $table)
372
    {
373 77
        $discrColumn = $class->discriminatorColumn;
374
375 77
        if ( ! isset($discrColumn['type']) ||
376 77
            (strtolower($discrColumn['type']) == 'string' && $discrColumn['length'] === null)
377
        ) {
378
            $discrColumn['type'] = 'string';
379
            $discrColumn['length'] = 255;
380
        }
381
382
        $options = array(
383 77
            'length'    => isset($discrColumn['length']) ? $discrColumn['length'] : null,
384
            'notnull'   => true
385
        );
386
387 77
        if (isset($discrColumn['columnDefinition'])) {
388
            $options['columnDefinition'] = $discrColumn['columnDefinition'];
389
        }
390
391 77
        $table->addColumn($discrColumn['name'], $discrColumn['type'], $options);
392 77
    }
393
394
    /**
395
     * Gathers the column definitions as required by the DBAL of all field mappings
396
     * found in the given class.
397
     *
398
     * @param ClassMetadata $class
399
     * @param Table         $table
400
     *
401
     * @return array The list of portable column definitions as required by the DBAL.
402
     */
403 269
    private function gatherColumns($class, Table $table)
404
    {
405 269
        $pkColumns = array();
406
407 269
        foreach ($class->fieldMappings as $mapping) {
408 269
            if ($class->isInheritanceTypeSingleTable() && isset($mapping['inherited'])) {
409 31
                continue;
410
            }
411
412 269
            $this->gatherColumn($class, $mapping, $table);
413
414 269
            if ($class->isIdentifier($mapping['fieldName'])) {
415 269
                $pkColumns[] = $this->quoteStrategy->getColumnName($mapping['fieldName'], $class, $this->platform);
416
            }
417
        }
418
419
        // For now, this is a hack required for single table inheritence, since this method is called
420
        // twice by single table inheritence relations
421 269
        if (!$table->hasIndex('primary')) {
422
            //$table->setPrimaryKey($pkColumns);
423
        }
424 269
    }
425
426
    /**
427
     * Creates a column definition as required by the DBAL from an ORM field mapping definition.
428
     *
429
     * @param ClassMetadata $class   The class that owns the field mapping.
430
     * @param array         $mapping The field mapping.
431
     * @param Table         $table
432
     *
433
     * @return array The portable column definition as required by the DBAL.
434
     */
435 280
    private function gatherColumn($class, array $mapping, Table $table)
436
    {
437 280
        $columnName = $this->quoteStrategy->getColumnName($mapping['fieldName'], $class, $this->platform);
438 280
        $columnType = $mapping['type'];
439
440 280
        $options = array();
441 280
        $options['length'] = isset($mapping['length']) ? $mapping['length'] : null;
442 280
        $options['notnull'] = isset($mapping['nullable']) ? ! $mapping['nullable'] : true;
443 280
        if ($class->isInheritanceTypeSingleTable() && count($class->parentClasses) > 0) {
444 6
            $options['notnull'] = false;
445
        }
446
447 280
        $options['platformOptions'] = array();
448 280
        $options['platformOptions']['version'] = $class->isVersioned && $class->versionField == $mapping['fieldName'] ? true : false;
449
450 280
        if (strtolower($columnType) == 'string' && $options['length'] === null) {
451 152
            $options['length'] = 255;
452
        }
453
454 280
        if (isset($mapping['precision'])) {
455 279
            $options['precision'] = $mapping['precision'];
456
        }
457
458 280
        if (isset($mapping['scale'])) {
459 279
            $options['scale'] = $mapping['scale'];
460
        }
461
462 280
        if (isset($mapping['default'])) {
463 29
            $options['default'] = $mapping['default'];
464
        }
465
466 280
        if (isset($mapping['columnDefinition'])) {
467 1
            $options['columnDefinition'] = $mapping['columnDefinition'];
468
        }
469
470 280
        if (isset($mapping['options'])) {
471 3
            $knownOptions = array('comment', 'unsigned', 'fixed', 'default');
472
473 3
            foreach ($knownOptions as $knownOption) {
474 3
                if (array_key_exists($knownOption, $mapping['options'])) {
475 2
                    $options[$knownOption] = $mapping['options'][$knownOption];
476
477 3
                    unset($mapping['options'][$knownOption]);
478
                }
479
            }
480
481 3
            $options['customSchemaOptions'] = $mapping['options'];
482
        }
483
484 280
        if ($class->isIdGeneratorIdentity() && $class->getIdentifierFieldNames() == array($mapping['fieldName'])) {
485 249
            $options['autoincrement'] = true;
486
        }
487 280
        if ($class->isInheritanceTypeJoined() && $class->name != $class->rootEntityName) {
488 49
            $options['autoincrement'] = false;
489
        }
490
491 280
        if ($table->hasColumn($columnName)) {
492
            // required in some inheritance scenarios
493
            $table->changeColumn($columnName, $options);
494
        } else {
495 280
            $table->addColumn($columnName, $columnType, $options);
496
        }
497
498 280
        $isUnique = isset($mapping['unique']) ? $mapping['unique'] : false;
499 280
        if ($isUnique) {
500 17
            $table->addUniqueIndex(array($columnName));
501
        }
502 280
    }
503
504
    /**
505
     * Gathers the SQL for properly setting up the relations of the given class.
506
     * This includes the SQL for foreign key constraints and join tables.
507
     *
508
     * @param ClassMetadata $class
509
     * @param Table         $table
510
     * @param Schema        $schema
511
     * @param array         $addedFks
512
     * @param array         $blacklistedFks
513
     * @param array        $m2mDuplicates
514
     *
515
     * @return void
516
     *
517
     * @throws \Doctrine\ORM\ORMException
518
     */
519 280
    private function gatherRelationsSql($class, $table, $schema, &$addedFks, &$blacklistedFks, &$m2mDuplicates)
520
    {
521 280
        foreach ($class->associationMappings as $mapping) {
522 202
            if (isset($mapping['inherited'])) {
523 19
                continue;
524
            }
525
526 202
            $foreignClass = $this->em->getClassMetadata($mapping['targetEntity']);
527
528 202
            if ($mapping['type'] & ClassMetadata::TO_ONE && $mapping['isOwningSide']) {
529 178
                $primaryKeyColumns = array(); // PK is unnecessary for this relation-type
530
531 178
                $this->gatherRelationJoinColumns(
532 178
                    $mapping['joinColumns'],
533
                    $table,
534
                    $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...
535
                    $mapping,
536
                    $primaryKeyColumns,
537
                    $addedFks,
538
                    $blacklistedFks
539
                );
540 150
            } elseif ($mapping['type'] == ClassMetadata::ONE_TO_MANY && $mapping['isOwningSide']) {
541
                //... create join table, one-many through join table supported later
542
                throw ORMException::notSupported();
543 150
            } elseif ($mapping['type'] == ClassMetadata::MANY_TO_MANY && $mapping['isOwningSide']) {
544
                // create join table
545 54
                $joinTable = $mapping['joinTable'];
546 54
                $jointTableName = $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...
547
548 54
                if ($schema->hasTable($jointTableName)) {
549 1
                    return;
550
                }
551
552 53
                $theJoinTable = $schema->createTable($jointTableName);
553
554 53
                if (isset($m2mDuplicates[$jointTableName])) {
555
                    $m2mDuplicates[$jointTableName]++;
556
                } else {
557 53
                    $m2mDuplicates[$jointTableName] = 1;
558
                }
559
560 53
                $primaryKeyColumns = array();
561
562
                // Build first FK constraint (relation table => source table)
563 53
                $this->gatherRelationJoinColumns(
564 53
                    $joinTable['joinColumns'],
565
                    $theJoinTable,
566
                    $class,
567
                    $mapping,
568
                    $primaryKeyColumns,
569
                    $addedFks,
570
                    $blacklistedFks
571
                );
572
573
                // Build second FK constraint (relation table => target table)
574 53
                $this->gatherRelationJoinColumns(
575 53
                    $joinTable['inverseJoinColumns'],
576
                    $theJoinTable,
577
                    $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...
578
                    $mapping,
579
                    $primaryKeyColumns,
580
                    $addedFks,
581
                    $blacklistedFks
582
                );
583
584 202
                $theJoinTable->setPrimaryKey($primaryKeyColumns);
585
            }
586
        }
587 280
    }
588
589
    /**
590
     * Check if the schema event is create or update the schema
591
     *
592
     * @param $schemaAction
593
     * @return bool
594
     */
595
    private function isSchemaActionCreateOrUpdate($schemaAction)
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
596
    {
597
        return in_array($schemaAction, array( self::SCHEMA_CREATE, self::SCHEMA_UPDATE));
598
    }
599
600
    /**
601
     * Gets the class metadata that is responsible for the definition of the referenced column name.
602
     *
603
     * Previously this was a simple task, but with DDC-117 this problem is actually recursive. If its
604
     * not a simple field, go through all identifier field names that are associations recursively and
605
     * find that referenced column name.
606
     *
607
     * TODO: Is there any way to make this code more pleasing?
608
     *
609
     * @param ClassMetadata $class
610
     * @param string        $referencedColumnName
611
     *
612
     * @return array (ClassMetadata, referencedFieldName)
613
     */
614 201
    private function getDefiningClass($class, $referencedColumnName)
615
    {
616 201
        $referencedFieldName = $class->getFieldName($referencedColumnName);
617
618 201
        if ($class->hasField($referencedFieldName)) {
619 201
            return array($class, $referencedFieldName);
620
        }
621
622 9
        if (in_array($referencedColumnName, $class->getIdentifierColumnNames())) {
623
            // it seems to be an entity as foreign key
624 9
            foreach ($class->getIdentifierFieldNames() as $fieldName) {
625 9
                if ($class->hasAssociation($fieldName)
626 9
                    && $class->getSingleAssociationJoinColumnName($fieldName) == $referencedColumnName) {
627 9
                    return $this->getDefiningClass(
628 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...
629 9
                        $class->getSingleAssociationReferencedJoinColumnName($fieldName)
630
                    );
631
                }
632
            }
633
        }
634
635
        return null;
636
    }
637
638
    /**
639
     * Gathers columns and fk constraints that are required for one part of relationship.
640
     *
641
     * @param array         $joinColumns
642
     * @param Table         $theJoinTable
643
     * @param ClassMetadata $class
644
     * @param array         $mapping
645
     * @param array         $primaryKeyColumns
646
     * @param array         $addedFks
647
     * @param array         $blacklistedFks
648
     *
649
     * @return void
650
     *
651
     * @throws \Doctrine\ORM\ORMException
652
     */
653 201
    private function gatherRelationJoinColumns(
654
        $joinColumns,
655
        $theJoinTable,
656
        $class,
657
        $mapping,
658
        &$primaryKeyColumns,
659
        &$addedFks,
660
        &$blacklistedFks
661
    )
662
    {
663 201
        $localColumns       = array();
664 201
        $foreignColumns     = array();
665 201
        $fkOptions          = array();
666 201
        $foreignTableName   = $this->quoteStrategy->getTableName($class, $this->platform);
667 201
        $uniqueConstraints  = array();
668
669 201
        foreach ($joinColumns as $joinColumn) {
670
671 201
            list($definingClass, $referencedFieldName) = $this->getDefiningClass(
672
                $class,
673 201
                $joinColumn['referencedColumnName']
674
            );
675
676 201
            if ( ! $definingClass) {
677
                throw new \Doctrine\ORM\ORMException(
678
                    "Column name `".$joinColumn['referencedColumnName']."` referenced for relation from ".
679
                    $mapping['sourceEntity'] . " towards ". $mapping['targetEntity'] . " does not exist."
680
                );
681
            }
682
683 201
            $quotedColumnName       = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
684 201
            $quotedRefColumnName    = $this->quoteStrategy->getReferencedJoinColumnName(
685
                $joinColumn,
686
                $class,
687 201
                $this->platform
688
            );
689
690 201
            $primaryKeyColumns[]    = $quotedColumnName;
691 201
            $localColumns[]         = $quotedColumnName;
692 201
            $foreignColumns[]       = $quotedRefColumnName;
693
694 201
            if ( ! $theJoinTable->hasColumn($quotedColumnName)) {
695
                // Only add the column to the table if it does not exist already.
696
                // It might exist already if the foreign key is mapped into a regular
697
                // property as well.
698
699 199
                $fieldMapping = $definingClass->getFieldMapping($referencedFieldName);
700
701 199
                $columnDef = null;
702 199
                if (isset($joinColumn['columnDefinition'])) {
703
                    $columnDef = $joinColumn['columnDefinition'];
704 199
                } elseif (isset($fieldMapping['columnDefinition'])) {
705 1
                    $columnDef = $fieldMapping['columnDefinition'];
706
                }
707
708 199
                $columnOptions = array('notnull' => false, 'columnDefinition' => $columnDef);
709
710 199
                if (isset($joinColumn['nullable'])) {
711 129
                    $columnOptions['notnull'] = !$joinColumn['nullable'];
712
                }
713
714 199
                if (isset($fieldMapping['options'])) {
715
                    $columnOptions['options'] = $fieldMapping['options'];
716
                }
717
718 199
                if ($fieldMapping['type'] == "string" && isset($fieldMapping['length'])) {
719 3
                    $columnOptions['length'] = $fieldMapping['length'];
720 199
                } elseif ($fieldMapping['type'] == "decimal") {
721
                    $columnOptions['scale'] = $fieldMapping['scale'];
722
                    $columnOptions['precision'] = $fieldMapping['precision'];
723
                }
724
725 199
                $theJoinTable->addColumn($quotedColumnName, $fieldMapping['type'], $columnOptions);
726
            }
727
728 201
            if (isset($joinColumn['unique']) && $joinColumn['unique'] == true) {
729 65
                $uniqueConstraints[] = array('columns' => array($quotedColumnName));
730
            }
731
732 201
            if (isset($joinColumn['onDelete'])) {
733 201
                $fkOptions['onDelete'] = $joinColumn['onDelete'];
734
            }
735
        }
736
737
        // Prefer unique constraints over implicit simple indexes created for foreign keys.
738
        // Also avoids index duplication.
739 201
        foreach ($uniqueConstraints as $indexName => $unique) {
740 65
            $theJoinTable->addUniqueIndex($unique['columns'], is_numeric($indexName) ? null : $indexName);
741
        }
742
743 201
        $compositeName = $theJoinTable->getName().'.'.implode('', $localColumns);
744 201
        if (isset($addedFks[$compositeName])
745 1
            && ($foreignTableName != $addedFks[$compositeName]['foreignTableName']
746 201
            || 0 < count(array_diff($foreignColumns, $addedFks[$compositeName]['foreignColumns'])))
747
        ) {
748 1
            foreach ($theJoinTable->getForeignKeys() as $fkName => $key) {
749 1
                if (0 === count(array_diff($key->getLocalColumns(), $localColumns))
750 1
                    && (($key->getForeignTableName() != $foreignTableName)
751 1
                    || 0 < count(array_diff($key->getForeignColumns(), $foreignColumns)))
752
                ) {
753 1
                    $theJoinTable->removeForeignKey($fkName);
754 1
                    break;
755
                }
756
            }
757 1
            $blacklistedFks[$compositeName] = true;
758 201
        } elseif (!isset($blacklistedFks[$compositeName])) {
759 201
            $addedFks[$compositeName] = array('foreignTableName' => $foreignTableName, 'foreignColumns' => $foreignColumns);
760 201
            $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...
761
                $foreignTableName,
762
                $localColumns,
763
                $foreignColumns,
764
                $fkOptions
765
            );
766
        }
767 201
    }
768
769
    /**
770
     * Drops the database schema for the given classes.
771
     *
772
     * In any way when an exception is thrown it is suppressed since drop was
773
     * issued for all classes of the schema and some probably just don't exist.
774
     *
775
     * @param array $classes
776
     *
777
     * @return void
778
     */
779 3
    public function dropSchema(array $classes)
780
    {
781 3
        $dropSchemaSql = $this->getDropSchemaSQL($classes);
782
        $conn = $this->em->getConnection();
783
784
        foreach ($dropSchemaSql as $sql) {
785
            try {
786
                $conn->executeQuery($sql);
787
            } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
788
789
            }
790
        }
791
    }
792
793
    /**
794
     * Drops all elements in the database of the current connection.
795
     *
796
     * @return void
797
     */
798
    public function dropDatabase()
799
    {
800
        $dropSchemaSql = $this->getDropDatabaseSQL();
801
        $conn = $this->em->getConnection();
802
803
        foreach ($dropSchemaSql as $sql) {
804
            $conn->executeQuery($sql);
805
        }
806
    }
807
808
    /**
809
     * Gets the SQL needed to drop the database schema for the connections database.
810
     *
811
     * @return array
812
     */
813
    public function getDropDatabaseSQL()
814
    {
815
        $sm = $this->em->getConnection()->getSchemaManager();
816
        $schema = $sm->createSchema();
817
818
        $visitor = new DropSchemaSqlCollector($this->platform);
819
        $schema->visit($visitor);
820
821
        return $visitor->getQueries();
822
    }
823
824
    /**
825
     * Gets SQL to drop the tables defined by the passed classes.
826
     *
827
     * @param array $classes
828
     *
829
     * @return array
830
     */
831 3
    public function getDropSchemaSQL(array $classes)
832
    {
833 3
        $visitor = new DropSchemaSqlCollector($this->platform);
834 3
        $schema = $this->getSchemaFromMetadata($classes, self::SCHEMA_DROP);
0 ignored issues
show
Unused Code introduced by
The call to SchemaTool::getSchemaFromMetadata() has too many arguments starting with self::SCHEMA_DROP.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
835
836
        $sm = $this->em->getConnection()->getSchemaManager();
837
        $fullSchema = $sm->createSchema();
838
839
        foreach ($fullSchema->getTables() as $table) {
840
            if ( ! $schema->hasTable($table->getName())) {
841
                foreach ($table->getForeignKeys() as $foreignKey) {
842
                    /* @var $foreignKey \Doctrine\DBAL\Schema\ForeignKeyConstraint */
843
                    if ($schema->hasTable($foreignKey->getForeignTableName())) {
844
                        $visitor->acceptForeignKey($table, $foreignKey);
845
                    }
846
                }
847
            } else {
848
                $visitor->acceptTable($table);
849
                foreach ($table->getForeignKeys() as $foreignKey) {
850
                    $visitor->acceptForeignKey($table, $foreignKey);
851
                }
852
            }
853
        }
854
855
        if ($this->platform->supportsSequences()) {
856
            foreach ($schema->getSequences() as $sequence) {
857
                $visitor->acceptSequence($sequence);
858
            }
859
860
            foreach ($schema->getTables() as $table) {
861
                /* @var $sequence Table */
862
                if ($table->hasPrimaryKey()) {
863
                    $columns = $table->getPrimaryKey()->getColumns();
864
                    if (count($columns) == 1) {
865
                        $checkSequence = $table->getName() . "_" . $columns[0] . "_seq";
866
                        if ($fullSchema->hasSequence($checkSequence)) {
867
                            $visitor->acceptSequence($fullSchema->getSequence($checkSequence));
868
                        }
869
                    }
870
                }
871
            }
872
        }
873
874
        return $visitor->getQueries();
875
    }
876
877
    /**
878
     * Updates the database schema of the given classes by comparing the ClassMetadata
879
     * instances to the current database schema that is inspected.
880
     *
881
     * @param array   $classes
882
     * @param boolean $saveMode If TRUE, only performs a partial update
883
     *                          without dropping assets which are scheduled for deletion.
884
     *
885
     * @return void
886
     */
887
    public function updateSchema(array $classes, $saveMode = false)
888
    {
889
        $updateSchemaSql = $this->getUpdateSchemaSql($classes, $saveMode);
890
        $conn = $this->em->getConnection();
891
892
        foreach ($updateSchemaSql as $sql) {
893
            $conn->executeQuery($sql);
894
        }
895
    }
896
897
    /**
898
     * Gets the sequence of SQL statements that need to be performed in order
899
     * to bring the given class mappings in-synch with the relational schema.
900
     *
901
     * @param array   $classes  The classes to consider.
902
     * @param boolean $saveMode If TRUE, only generates SQL for a partial update
903
     *                          that does not include SQL for dropping assets which are scheduled for deletion.
904
     *
905
     * @return array The sequence of SQL statements.
906
     */
907 1
    public function getUpdateSchemaSql(array $classes, $saveMode = false)
908
    {
909 1
        $sm = $this->em->getConnection()->getSchemaManager();
910
911 1
        $fromSchema = $sm->createSchema();
912 1
        $toSchema = $this->getSchemaFromMetadata($classes, self::SCHEMA_UPDATE);
0 ignored issues
show
Unused Code introduced by
The call to SchemaTool::getSchemaFromMetadata() has too many arguments starting with self::SCHEMA_UPDATE.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
913
914
        $comparator = new Comparator();
915
        $schemaDiff = $comparator->compare($fromSchema, $toSchema);
916
917
        if ($saveMode) {
918
            return $schemaDiff->toSaveSql($this->platform);
919
        }
920
921
        return $schemaDiff->toSql($this->platform);
922
    }
923
}
924