Completed
Pull Request — master (#6027)
by Raul
08:29
created

SchemaTool::gatherRelationsSql()   C

Complexity

Conditions 12
Paths 8

Size

Total Lines 69
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 12.0585

Importance

Changes 0
Metric Value
dl 0
loc 69
ccs 25
cts 27
cp 0.9259
rs 5.7089
c 0
b 0
f 0
cc 12
eloc 45
nc 8
nop 7
crap 12.0585

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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