Passed
Pull Request — master (#3233)
by Sergey
12:33
created

Comparator::compare()   F

Complexity

Conditions 30
Paths > 20000

Size

Total Lines 124
Code Lines 67

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 65
CRAP Score 30.0031

Importance

Changes 0
Metric Value
eloc 67
dl 0
loc 124
rs 0
c 0
b 0
f 0
ccs 65
cts 66
cp 0.9848
cc 30
nc 302400
nop 2
crap 30.0031

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
namespace Doctrine\DBAL\Schema;
4
5
use Doctrine\DBAL\Types;
6
use function array_intersect_key;
7
use function array_key_exists;
8
use function array_keys;
9
use function array_map;
10
use function array_merge;
11
use function array_shift;
12
use function array_unique;
13
use function count;
14
use function strtolower;
15
16
/**
17
 * Compares two Schemas and return an instance of SchemaDiff.
18
 */
19
class Comparator
20
{
21
    /**
22
     * @return SchemaDiff
23
     */
24 340
    public static function compareSchemas(Schema $fromSchema, Schema $toSchema)
25
    {
26 340
        $c = new self();
27
28 340
        return $c->compare($fromSchema, $toSchema);
29
    }
30
31
    /**
32
     * Returns a SchemaDiff object containing the differences between the schemas $fromSchema and $toSchema.
33
     *
34
     * The returned differences are returned in such a way that they contain the
35
     * operations to change the schema stored in $fromSchema to the schema that is
36
     * stored in $toSchema.
37
     *
38
     * @return SchemaDiff
39
     */
40 600
    public function compare(Schema $fromSchema, Schema $toSchema)
41
    {
42 600
        $diff             = new SchemaDiff();
43 600
        $diff->fromSchema = $fromSchema;
44
45 600
        $foreignKeysToTable = [];
46
47 600
        foreach ($toSchema->getNamespaces() as $namespace) {
48 40
            if ($fromSchema->hasNamespace($namespace)) {
49 40
                continue;
50
            }
51
52 40
            $diff->newNamespaces[$namespace] = $namespace;
53
        }
54
55 600
        foreach ($fromSchema->getNamespaces() as $namespace) {
56 40
            if ($toSchema->hasNamespace($namespace)) {
57 40
                continue;
58
            }
59
60 20
            $diff->removedNamespaces[$namespace] = $namespace;
61
        }
62
63 600
        foreach ($toSchema->getTables() as $table) {
64 420
            $tableName = $table->getShortestName($toSchema->getName());
65 420
            if (! $fromSchema->hasTable($tableName)) {
66 100
                $diff->newTables[$tableName] = $toSchema->getTable($tableName);
67
            } else {
68 380
                $tableDifferences = $this->diffTable($fromSchema->getTable($tableName), $toSchema->getTable($tableName));
69 380
                if ($tableDifferences !== false) {
70 420
                    $diff->changedTables[$tableName] = $tableDifferences;
71
                }
72
            }
73
        }
74
75
        /* Check if there are tables removed */
76 600
        foreach ($fromSchema->getTables() as $table) {
77 400
            $tableName = $table->getShortestName($fromSchema->getName());
78
79 400
            $table = $fromSchema->getTable($tableName);
80 400
            if (! $toSchema->hasTable($tableName)) {
81 100
                $diff->removedTables[$tableName] = $table;
82
            }
83
84
            // also remember all foreign keys that point to a specific table
85 400
            foreach ($table->getForeignKeys() as $foreignKey) {
86 40
                $foreignTable = strtolower($foreignKey->getForeignTableName());
87 40
                if (! isset($foreignKeysToTable[$foreignTable])) {
88 40
                    $foreignKeysToTable[$foreignTable] = [];
89
                }
90 400
                $foreignKeysToTable[$foreignTable][] = $foreignKey;
91
            }
92
        }
93
94 600
        foreach ($diff->removedTables as $tableName => $table) {
95 100
            if (! isset($foreignKeysToTable[$tableName])) {
96 60
                continue;
97
            }
98
99 40
            $diff->orphanedForeignKeys = array_merge($diff->orphanedForeignKeys, $foreignKeysToTable[$tableName]);
100
101
            // deleting duplicated foreign keys present on both on the orphanedForeignKey
102
            // and the removedForeignKeys from changedTables
103 40
            foreach ($foreignKeysToTable[$tableName] as $foreignKey) {
104
                // strtolower the table name to make if compatible with getShortestName
105 40
                $localTableName = strtolower($foreignKey->getLocalTableName());
106 40
                if (! isset($diff->changedTables[$localTableName])) {
107
                    continue;
108
                }
109
110 40
                foreach ($diff->changedTables[$localTableName]->removedForeignKeys as $key => $removedForeignKey) {
111
                    // We check if the key is from the removed table if not we skip.
112 40
                    if ($tableName !== strtolower($removedForeignKey->getForeignTableName())) {
113 20
                        continue;
114
                    }
115 40
                    unset($diff->changedTables[$localTableName]->removedForeignKeys[$key]);
116
                }
117
            }
118
        }
119
120 600
        foreach ($toSchema->getSequences() as $sequence) {
121 80
            $sequenceName = $sequence->getShortestName($toSchema->getName());
122 80
            if (! $fromSchema->hasSequence($sequenceName)) {
123 60
                if (! $this->isAutoIncrementSequenceInSchema($fromSchema, $sequence)) {
124 60
                    $diff->newSequences[] = $sequence;
125
                }
126
            } else {
127 40
                if ($this->diffSequence($sequence, $fromSchema->getSequence($sequenceName))) {
128 80
                    $diff->changedSequences[] = $toSchema->getSequence($sequenceName);
129
                }
130
            }
131
        }
132
133 600
        foreach ($fromSchema->getSequences() as $sequence) {
134 80
            if ($this->isAutoIncrementSequenceInSchema($toSchema, $sequence)) {
135 20
                continue;
136
            }
137
138 60
            $sequenceName = $sequence->getShortestName($fromSchema->getName());
139
140 60
            if ($toSchema->hasSequence($sequenceName)) {
141 40
                continue;
142
            }
143
144 40
            $diff->removedSequences[] = $sequence;
145
        }
146
147 600
        foreach ($toSchema->getViews() as $view) {
148 40
            $viewName = $view->getShortestName($toSchema->getName());
149 40
            if ( ! $fromSchema->hasView($viewName)) {
150 20
                $diff->newViews[$viewName] = $view;
151 20
            } elseif ($fromSchema->getView($viewName)->isSameAs($view)) {
152 40
                $diff->changedViews[$viewName] = $view;
153
            }
154
        }
155
156 600
        foreach ($fromSchema->getViews() as $view) {
157 40
            $viewName = $view->getShortestName($fromSchema->getName());
158 40
            if ( ! $toSchema->hasView($viewName)) {
159 40
                $diff->removedViews[$viewName] = $view;
160
            }
161
        }
162
163 600
        return $diff;
164
    }
165
166
    /**
167
     * @param Schema   $schema
168
     * @param Sequence $sequence
169
     *
170
     * @return bool
171
     */
172 120
    private function isAutoIncrementSequenceInSchema($schema, $sequence)
173
    {
174 120
        foreach ($schema->getTables() as $table) {
175 40
            if ($sequence->isAutoIncrementsFor($table)) {
176 40
                return true;
177
            }
178
        }
179
180 80
        return false;
181
    }
182
183
    /**
184
     * @return bool
185
     */
186 69
    public function diffSequence(Sequence $sequence1, Sequence $sequence2)
187
    {
188 69
        if ($sequence1->getAllocationSize() !== $sequence2->getAllocationSize()) {
189 40
            return true;
190
        }
191
192 49
        return $sequence1->getInitialValue() !== $sequence2->getInitialValue();
193
    }
194
195
    /**
196
     * Returns the difference between the tables $table1 and $table2.
197
     *
198
     * If there are no differences this method returns the boolean false.
199
     *
200
     * @return TableDiff|false
201
     */
202 2696
    public function diffTable(Table $table1, Table $table2)
203
    {
204 2696
        $changes                     = 0;
205 2696
        $tableDifferences            = new TableDiff($table1->getName());
206 2696
        $tableDifferences->fromTable = $table1;
207
208 2696
        $table1Columns = $table1->getColumns();
209 2696
        $table2Columns = $table2->getColumns();
210
211
        /* See if all the fields in table 1 exist in table 2 */
212 2696
        foreach ($table2Columns as $columnName => $column) {
213 2596
            if ($table1->hasColumn($columnName)) {
214 2176
                continue;
215
            }
216
217 618
            $tableDifferences->addedColumns[$columnName] = $column;
218 618
            $changes++;
219
        }
220
        /* See if there are any removed fields in table 2 */
221 2696
        foreach ($table1Columns as $columnName => $column) {
222
            // See if column is removed in table 2.
223 2596
            if (! $table2->hasColumn($columnName)) {
224 618
                $tableDifferences->removedColumns[$columnName] = $column;
225 618
                $changes++;
226 618
                continue;
227
            }
228
229
            // See if column has changed properties in table 2.
230 2176
            $changedProperties = $this->diffColumn($column, $table2->getColumn($columnName));
231
232 2176
            if (empty($changedProperties)) {
233 1460
                continue;
234
            }
235
236 957
            $columnDiff                                           = new ColumnDiff($column->getName(), $table2->getColumn($columnName), $changedProperties);
237 957
            $columnDiff->fromColumn                               = $column;
238 957
            $tableDifferences->changedColumns[$column->getName()] = $columnDiff;
239 957
            $changes++;
240
        }
241
242 2696
        $this->detectColumnRenamings($tableDifferences);
243
244 2696
        $table1Indexes = $table1->getIndexes();
245 2696
        $table2Indexes = $table2->getIndexes();
246
247
        /* See if all the indexes in table 1 exist in table 2 */
248 2696
        foreach ($table2Indexes as $indexName => $index) {
249 970
            if (($index->isPrimary() && $table1->hasPrimaryKey()) || $table1->hasIndex($indexName)) {
250 616
                continue;
251
            }
252
253 354
            $tableDifferences->addedIndexes[$indexName] = $index;
254 354
            $changes++;
255
        }
256
        /* See if there are any removed indexes in table 2 */
257 2696
        foreach ($table1Indexes as $indexName => $index) {
258
            // See if index is removed in table 2.
259 910
            if (($index->isPrimary() && ! $table2->hasPrimaryKey()) ||
260 910
                ! $index->isPrimary() && ! $table2->hasIndex($indexName)
261
            ) {
262 394
                $tableDifferences->removedIndexes[$indexName] = $index;
263 394
                $changes++;
264 394
                continue;
265
            }
266
267
            // See if index has changed in table 2.
268 616
            $table2Index = $index->isPrimary() ? $table2->getPrimaryKey() : $table2->getIndex($indexName);
269
270 616
            if (! $this->diffIndex($index, $table2Index)) {
0 ignored issues
show
Bug introduced by
It seems like $table2Index can also be of type null; however, parameter $index2 of Doctrine\DBAL\Schema\Comparator::diffIndex() does only seem to accept Doctrine\DBAL\Schema\Index, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

270
            if (! $this->diffIndex($index, /** @scrutinizer ignore-type */ $table2Index)) {
Loading history...
271 316
                continue;
272
            }
273
274 318
            $tableDifferences->changedIndexes[$indexName] = $table2Index;
275 318
            $changes++;
276
        }
277
278 2696
        $this->detectIndexRenamings($tableDifferences);
279
280 2696
        $fromFkeys = $table1->getForeignKeys();
281 2696
        $toFkeys   = $table2->getForeignKeys();
282
283 2696
        foreach ($fromFkeys as $key1 => $constraint1) {
284 262
            foreach ($toFkeys as $key2 => $constraint2) {
285 122
                if ($this->diffForeignKey($constraint1, $constraint2) === false) {
286 44
                    unset($fromFkeys[$key1], $toFkeys[$key2]);
287
                } else {
288 78
                    if (strtolower($constraint1->getName()) === strtolower($constraint2->getName())) {
289 40
                        $tableDifferences->changedForeignKeys[] = $constraint2;
290 40
                        $changes++;
291 262
                        unset($fromFkeys[$key1], $toFkeys[$key2]);
292
                    }
293
                }
294
            }
295
        }
296
297 2696
        foreach ($fromFkeys as $constraint1) {
298 178
            $tableDifferences->removedForeignKeys[] = $constraint1;
299 178
            $changes++;
300
        }
301
302 2696
        foreach ($toFkeys as $constraint2) {
303 58
            $tableDifferences->addedForeignKeys[] = $constraint2;
304 58
            $changes++;
305
        }
306
307 2696
        return $changes ? $tableDifferences : false;
308
    }
309
310
    /**
311
     * Try to find columns that only changed their name, rename operations maybe cheaper than add/drop
312
     * however ambiguities between different possibilities should not lead to renaming at all.
313
     *
314
     * @return void
315
     */
316 2696
    private function detectColumnRenamings(TableDiff $tableDifferences)
317
    {
318 2696
        $renameCandidates = [];
319 2696
        foreach ($tableDifferences->addedColumns as $addedColumnName => $addedColumn) {
320 618
            foreach ($tableDifferences->removedColumns as $removedColumn) {
321 478
                if (count($this->diffColumn($addedColumn, $removedColumn)) !== 0) {
322 380
                    continue;
323
                }
324
325 618
                $renameCandidates[$addedColumn->getName()][] = [$removedColumn, $addedColumn, $addedColumnName];
326
            }
327
        }
328
329 2696
        foreach ($renameCandidates as $candidateColumns) {
330 478
            if (count($candidateColumns) !== 1) {
331 20
                continue;
332
            }
333
334 458
            [$removedColumn, $addedColumn] = $candidateColumns[0];
335 458
            $removedColumnName             = strtolower($removedColumn->getName());
336 458
            $addedColumnName               = strtolower($addedColumn->getName());
337
338 458
            if (isset($tableDifferences->renamedColumns[$removedColumnName])) {
339 20
                continue;
340
            }
341
342 458
            $tableDifferences->renamedColumns[$removedColumnName] = $addedColumn;
343
            unset(
344 458
                $tableDifferences->addedColumns[$addedColumnName],
345 458
                $tableDifferences->removedColumns[$removedColumnName]
346
            );
347
        }
348 2696
    }
349
350
    /**
351
     * Try to find indexes that only changed their name, rename operations maybe cheaper than add/drop
352
     * however ambiguities between different possibilities should not lead to renaming at all.
353
     *
354
     * @return void
355
     */
356 2696
    private function detectIndexRenamings(TableDiff $tableDifferences)
357
    {
358 2696
        $renameCandidates = [];
359
360
        // Gather possible rename candidates by comparing each added and removed index based on semantics.
361 2696
        foreach ($tableDifferences->addedIndexes as $addedIndexName => $addedIndex) {
362 354
            foreach ($tableDifferences->removedIndexes as $removedIndex) {
363 166
                if ($this->diffIndex($addedIndex, $removedIndex)) {
364 88
                    continue;
365
                }
366
367 354
                $renameCandidates[$addedIndex->getName()][] = [$removedIndex, $addedIndex, $addedIndexName];
368
            }
369
        }
370
371 2696
        foreach ($renameCandidates as $candidateIndexes) {
372
            // If the current rename candidate contains exactly one semantically equal index,
373
            // we can safely rename it.
374
            // Otherwise it is unclear if a rename action is really intended,
375
            // therefore we let those ambiguous indexes be added/dropped.
376 78
            if (count($candidateIndexes) !== 1) {
377 20
                continue;
378
            }
379
380 58
            [$removedIndex, $addedIndex] = $candidateIndexes[0];
381
382 58
            $removedIndexName = strtolower($removedIndex->getName());
383 58
            $addedIndexName   = strtolower($addedIndex->getName());
384
385 58
            if (isset($tableDifferences->renamedIndexes[$removedIndexName])) {
386
                continue;
387
            }
388
389 58
            $tableDifferences->renamedIndexes[$removedIndexName] = $addedIndex;
390
            unset(
391 58
                $tableDifferences->addedIndexes[$addedIndexName],
392 58
                $tableDifferences->removedIndexes[$removedIndexName]
393
            );
394
        }
395 2696
    }
396
397
    /**
398
     * @return bool
399
     */
400 182
    public function diffForeignKey(ForeignKeyConstraint $key1, ForeignKeyConstraint $key2)
401
    {
402 182
        if (array_map('strtolower', $key1->getUnquotedLocalColumns()) !== array_map('strtolower', $key2->getUnquotedLocalColumns())) {
403 38
            return true;
404
        }
405
406 144
        if (array_map('strtolower', $key1->getUnquotedForeignColumns()) !== array_map('strtolower', $key2->getUnquotedForeignColumns())) {
407
            return true;
408
        }
409
410 144
        if ($key1->getUnqualifiedForeignTableName() !== $key2->getUnqualifiedForeignTableName()) {
411 20
            return true;
412
        }
413
414 124
        if ($key1->onUpdate() !== $key2->onUpdate()) {
415 20
            return true;
416
        }
417
418 104
        return $key1->onDelete() !== $key2->onDelete();
419
    }
420
421
    /**
422
     * Returns the difference between the fields $field1 and $field2.
423
     *
424
     * If there are differences this method returns $field2, otherwise the
425
     * boolean false.
426
     *
427
     * @return string[]
428
     */
429 3036
    public function diffColumn(Column $column1, Column $column2)
430
    {
431 3036
        $properties1 = $column1->toArray();
432 3036
        $properties2 = $column2->toArray();
433
434 3036
        $changedProperties = [];
435
436 3036
        foreach (['type', 'notnull', 'unsigned', 'autoincrement'] as $property) {
437 3036
            if ($properties1[$property] === $properties2[$property]) {
438 3036
                continue;
439
            }
440
441 371
            $changedProperties[] = $property;
442
        }
443
444
        // This is a very nasty hack to make comparator work with the legacy json_array type, which should be killed in v3
445 3036
        if ($this->isALegacyJsonComparison($properties1['type'], $properties2['type'])) {
0 ignored issues
show
Deprecated Code introduced by
The function Doctrine\DBAL\Schema\Com...ALegacyJsonComparison() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

445
        if (/** @scrutinizer ignore-deprecated */ $this->isALegacyJsonComparison($properties1['type'], $properties2['type'])) {
Loading history...
446 36
            array_shift($changedProperties);
447
448 36
            $changedProperties[] = 'comment';
449
        }
450
451 3036
        if ($properties1['default'] !== $properties2['default'] ||
452
            // Null values need to be checked additionally as they tell whether to create or drop a default value.
453
            // null != 0, null != false, null != '' etc. This affects platform's table alteration SQL generation.
454 2970
            ($properties1['default'] === null && $properties2['default'] !== null) ||
455 3036
            ($properties2['default'] === null && $properties1['default'] !== null)
456
        ) {
457 86
            $changedProperties[] = 'default';
458
        }
459
460 3036
        if (($properties1['type'] instanceof Types\StringType && ! $properties1['type'] instanceof Types\GuidType) ||
461 3036
            $properties1['type'] instanceof Types\BinaryType
462
        ) {
463
            // check if value of length is set at all, default value assumed otherwise.
464 620
            $length1 = $properties1['length'] ?: 255;
465 620
            $length2 = $properties2['length'] ?: 255;
466 620
            if ($length1 !== $length2) {
467 301
                $changedProperties[] = 'length';
468
            }
469
470 620
            if ($properties1['fixed'] !== $properties2['fixed']) {
471 620
                $changedProperties[] = 'fixed';
472
            }
473 2676
        } elseif ($properties1['type'] instanceof Types\DecimalType) {
474 39
            if (($properties1['precision'] ?: 10) !== ($properties2['precision'] ?: 10)) {
475
                $changedProperties[] = 'precision';
476
            }
477 39
            if ($properties1['scale'] !== $properties2['scale']) {
478
                $changedProperties[] = 'scale';
479
            }
480
        }
481
482
        // A null value and an empty string are actually equal for a comment so they should not trigger a change.
483 3036
        if ($properties1['comment'] !== $properties2['comment'] &&
484 3036
            ! ($properties1['comment'] === null && $properties2['comment'] === '') &&
485 3036
            ! ($properties2['comment'] === null && $properties1['comment'] === '')
486
        ) {
487 940
            $changedProperties[] = 'comment';
488
        }
489
490 3036
        $customOptions1 = $column1->getCustomSchemaOptions();
491 3036
        $customOptions2 = $column2->getCustomSchemaOptions();
492
493 3036
        foreach (array_merge(array_keys($customOptions1), array_keys($customOptions2)) as $key) {
494 40
            if (! array_key_exists($key, $properties1) || ! array_key_exists($key, $properties2)) {
495 20
                $changedProperties[] = $key;
496 40
            } elseif ($properties1[$key] !== $properties2[$key]) {
497 40
                $changedProperties[] = $key;
498
            }
499
        }
500
501 3036
        $platformOptions1 = $column1->getPlatformOptions();
502 3036
        $platformOptions2 = $column2->getPlatformOptions();
503
504 3036
        foreach (array_keys(array_intersect_key($platformOptions1, $platformOptions2)) as $key) {
505 40
            if ($properties1[$key] === $properties2[$key]) {
506 40
                continue;
507
            }
508
509 20
            $changedProperties[] = $key;
510
        }
511
512 3036
        return array_unique($changedProperties);
513
    }
514
515
    /**
516
     * TODO: kill with fire on v3.0
517
     *
518
     * @deprecated
519
     */
520 3036
    private function isALegacyJsonComparison(Types\Type $one, Types\Type $other) : bool
521
    {
522 3036
        if (! $one instanceof Types\JsonType || ! $other instanceof Types\JsonType) {
523 2972
            return false;
524
        }
525
526 64
        return ( ! $one instanceof Types\JsonArrayType && $other instanceof Types\JsonArrayType)
527 64
            || ( ! $other instanceof Types\JsonArrayType && $one instanceof Types\JsonArrayType);
528
    }
529
530
    /**
531
     * Finds the difference between the indexes $index1 and $index2.
532
     *
533
     * Compares $index1 with $index2 and returns $index2 if there are any
534
     * differences or false in case there are no differences.
535
     *
536
     * @return bool
537
     */
538 782
    public function diffIndex(Index $index1, Index $index2)
539
    {
540 782
        return ! ($index1->isFullfilledBy($index2) && $index2->isFullfilledBy($index1));
541
    }
542
}
543