Total Complexity | 115 |
Total Lines | 526 |
Duplicated Lines | 0 % |
Changes | 0 |
Complex classes like Comparator often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Comparator, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
21 | class Comparator |
||
22 | { |
||
23 | public const DETECT_COLUMN_RENAMINGS = 0b0001; |
||
24 | public const DETECT_INDEX_RENAMINGS = 0b0010; |
||
25 | |||
26 | /** @var int */ |
||
27 | private $flags; |
||
28 | |||
29 | public function __construct(int $flags = self::DETECT_COLUMN_RENAMINGS | self::DETECT_INDEX_RENAMINGS) |
||
30 | { |
||
31 | $this->flags = $flags; |
||
32 | } |
||
33 | |||
34 | /** |
||
35 | * @return SchemaDiff |
||
36 | */ |
||
37 | public static function compareSchemas(Schema $fromSchema, Schema $toSchema) |
||
38 | { |
||
39 | $c = new self(); |
||
40 | |||
41 | return $c->compare($fromSchema, $toSchema); |
||
42 | } |
||
43 | |||
44 | /** |
||
45 | * Returns a SchemaDiff object containing the differences between the schemas $fromSchema and $toSchema. |
||
46 | * |
||
47 | * The returned differences are returned in such a way that they contain the |
||
48 | * operations to change the schema stored in $fromSchema to the schema that is |
||
49 | * stored in $toSchema. |
||
50 | * |
||
51 | * @return SchemaDiff |
||
52 | */ |
||
53 | public function compare(Schema $fromSchema, Schema $toSchema) |
||
54 | { |
||
55 | $diff = new SchemaDiff(); |
||
56 | $diff->fromSchema = $fromSchema; |
||
57 | |||
58 | $foreignKeysToTable = []; |
||
59 | |||
60 | foreach ($toSchema->getNamespaces() as $namespace) { |
||
61 | if ($fromSchema->hasNamespace($namespace)) { |
||
62 | continue; |
||
63 | } |
||
64 | |||
65 | $diff->newNamespaces[$namespace] = $namespace; |
||
66 | } |
||
67 | |||
68 | foreach ($fromSchema->getNamespaces() as $namespace) { |
||
69 | if ($toSchema->hasNamespace($namespace)) { |
||
70 | continue; |
||
71 | } |
||
72 | |||
73 | $diff->removedNamespaces[$namespace] = $namespace; |
||
74 | } |
||
75 | |||
76 | foreach ($toSchema->getTables() as $table) { |
||
77 | $tableName = $table->getShortestName($toSchema->getName()); |
||
78 | if (! $fromSchema->hasTable($tableName)) { |
||
79 | $diff->newTables[$tableName] = $toSchema->getTable($tableName); |
||
80 | } else { |
||
81 | $tableDifferences = $this->diffTable($fromSchema->getTable($tableName), $toSchema->getTable($tableName)); |
||
82 | if ($tableDifferences !== false) { |
||
83 | $diff->changedTables[$tableName] = $tableDifferences; |
||
84 | } |
||
85 | } |
||
86 | } |
||
87 | |||
88 | /* Check if there are tables removed */ |
||
89 | foreach ($fromSchema->getTables() as $table) { |
||
90 | $tableName = $table->getShortestName($fromSchema->getName()); |
||
91 | |||
92 | $table = $fromSchema->getTable($tableName); |
||
93 | if (! $toSchema->hasTable($tableName)) { |
||
94 | $diff->removedTables[$tableName] = $table; |
||
95 | } |
||
96 | |||
97 | // also remember all foreign keys that point to a specific table |
||
98 | foreach ($table->getForeignKeys() as $foreignKey) { |
||
99 | $foreignTable = strtolower($foreignKey->getForeignTableName()); |
||
100 | if (! isset($foreignKeysToTable[$foreignTable])) { |
||
101 | $foreignKeysToTable[$foreignTable] = []; |
||
102 | } |
||
103 | $foreignKeysToTable[$foreignTable][] = $foreignKey; |
||
104 | } |
||
105 | } |
||
106 | |||
107 | foreach ($diff->removedTables as $tableName => $table) { |
||
108 | if (! isset($foreignKeysToTable[$tableName])) { |
||
109 | continue; |
||
110 | } |
||
111 | |||
112 | $diff->orphanedForeignKeys = array_merge($diff->orphanedForeignKeys, $foreignKeysToTable[$tableName]); |
||
113 | |||
114 | // deleting duplicated foreign keys present on both on the orphanedForeignKey |
||
115 | // and the removedForeignKeys from changedTables |
||
116 | foreach ($foreignKeysToTable[$tableName] as $foreignKey) { |
||
117 | // strtolower the table name to make if compatible with getShortestName |
||
118 | $localTableName = strtolower($foreignKey->getLocalTableName()); |
||
119 | if (! isset($diff->changedTables[$localTableName])) { |
||
120 | continue; |
||
121 | } |
||
122 | |||
123 | foreach ($diff->changedTables[$localTableName]->removedForeignKeys as $key => $removedForeignKey) { |
||
124 | assert($removedForeignKey instanceof ForeignKeyConstraint); |
||
125 | |||
126 | // We check if the key is from the removed table if not we skip. |
||
127 | if ($tableName !== strtolower($removedForeignKey->getForeignTableName())) { |
||
128 | continue; |
||
129 | } |
||
130 | unset($diff->changedTables[$localTableName]->removedForeignKeys[$key]); |
||
131 | } |
||
132 | } |
||
133 | } |
||
134 | |||
135 | foreach ($toSchema->getSequences() as $sequence) { |
||
136 | $sequenceName = $sequence->getShortestName($toSchema->getName()); |
||
137 | if (! $fromSchema->hasSequence($sequenceName)) { |
||
138 | if (! $this->isAutoIncrementSequenceInSchema($fromSchema, $sequence)) { |
||
139 | $diff->newSequences[] = $sequence; |
||
140 | } |
||
141 | } else { |
||
142 | if ($this->diffSequence($sequence, $fromSchema->getSequence($sequenceName))) { |
||
143 | $diff->changedSequences[] = $toSchema->getSequence($sequenceName); |
||
144 | } |
||
145 | } |
||
146 | } |
||
147 | |||
148 | foreach ($fromSchema->getSequences() as $sequence) { |
||
149 | if ($this->isAutoIncrementSequenceInSchema($toSchema, $sequence)) { |
||
150 | continue; |
||
151 | } |
||
152 | |||
153 | $sequenceName = $sequence->getShortestName($fromSchema->getName()); |
||
154 | |||
155 | if ($toSchema->hasSequence($sequenceName)) { |
||
156 | continue; |
||
157 | } |
||
158 | |||
159 | $diff->removedSequences[] = $sequence; |
||
160 | } |
||
161 | |||
162 | return $diff; |
||
163 | } |
||
164 | |||
165 | /** |
||
166 | * @param Schema $schema |
||
167 | * @param Sequence $sequence |
||
168 | * |
||
169 | * @return bool |
||
170 | */ |
||
171 | private function isAutoIncrementSequenceInSchema($schema, $sequence) |
||
172 | { |
||
173 | foreach ($schema->getTables() as $table) { |
||
174 | if ($sequence->isAutoIncrementsFor($table)) { |
||
175 | return true; |
||
176 | } |
||
177 | } |
||
178 | |||
179 | return false; |
||
180 | } |
||
181 | |||
182 | /** |
||
183 | * @return bool |
||
184 | */ |
||
185 | public function diffSequence(Sequence $sequence1, Sequence $sequence2) |
||
192 | } |
||
193 | |||
194 | /** |
||
195 | * Returns the difference between the tables $table1 and $table2. |
||
196 | * |
||
197 | * If there are no differences this method returns the boolean false. |
||
198 | * |
||
199 | * @return TableDiff|false |
||
200 | */ |
||
201 | public function diffTable(Table $table1, Table $table2) |
||
202 | { |
||
203 | $changes = 0; |
||
204 | $tableDifferences = new TableDiff($table1->getName()); |
||
205 | $tableDifferences->fromTable = $table1; |
||
206 | |||
207 | $table1Columns = $table1->getColumns(); |
||
208 | $table2Columns = $table2->getColumns(); |
||
209 | |||
210 | /* See if all the fields in table 1 exist in table 2 */ |
||
211 | foreach ($table2Columns as $columnName => $column) { |
||
212 | if ($table1->hasColumn($columnName)) { |
||
213 | continue; |
||
214 | } |
||
215 | |||
216 | $tableDifferences->addedColumns[$columnName] = $column; |
||
217 | $changes++; |
||
218 | } |
||
219 | /* See if there are any removed fields in table 2 */ |
||
220 | foreach ($table1Columns as $columnName => $column) { |
||
221 | // See if column is removed in table 2. |
||
222 | if (! $table2->hasColumn($columnName)) { |
||
223 | $tableDifferences->removedColumns[$columnName] = $column; |
||
224 | $changes++; |
||
225 | continue; |
||
226 | } |
||
227 | |||
228 | // See if column has changed properties in table 2. |
||
229 | $changedProperties = $this->diffColumn($column, $table2->getColumn($columnName)); |
||
230 | |||
231 | if (empty($changedProperties)) { |
||
232 | continue; |
||
233 | } |
||
234 | |||
235 | $columnDiff = new ColumnDiff($column->getName(), $table2->getColumn($columnName), $changedProperties); |
||
236 | $columnDiff->fromColumn = $column; |
||
237 | $tableDifferences->changedColumns[$column->getName()] = $columnDiff; |
||
238 | $changes++; |
||
239 | } |
||
240 | |||
241 | if ($this->flags & self::DETECT_COLUMN_RENAMINGS) { |
||
242 | $this->detectColumnRenamings($tableDifferences); |
||
243 | } |
||
244 | |||
245 | $table1Indexes = $table1->getIndexes(); |
||
246 | $table2Indexes = $table2->getIndexes(); |
||
247 | |||
248 | /* See if all the indexes in table 1 exist in table 2 */ |
||
249 | foreach ($table2Indexes as $indexName => $index) { |
||
250 | if (($index->isPrimary() && $table1->hasPrimaryKey()) || $table1->hasIndex($indexName)) { |
||
251 | continue; |
||
252 | } |
||
253 | |||
254 | $tableDifferences->addedIndexes[$indexName] = $index; |
||
255 | $changes++; |
||
256 | } |
||
257 | /* See if there are any removed indexes in table 2 */ |
||
258 | foreach ($table1Indexes as $indexName => $index) { |
||
259 | // See if index is removed in table 2. |
||
260 | if (($index->isPrimary() && ! $table2->hasPrimaryKey()) || |
||
261 | ! $index->isPrimary() && ! $table2->hasIndex($indexName) |
||
262 | ) { |
||
263 | $tableDifferences->removedIndexes[$indexName] = $index; |
||
264 | $changes++; |
||
265 | continue; |
||
266 | } |
||
267 | |||
268 | // See if index has changed in table 2. |
||
269 | $table2Index = $index->isPrimary() ? $table2->getPrimaryKey() : $table2->getIndex($indexName); |
||
270 | assert($table2Index instanceof Index); |
||
271 | |||
272 | if (! $this->diffIndex($index, $table2Index)) { |
||
273 | continue; |
||
274 | } |
||
275 | |||
276 | $tableDifferences->changedIndexes[$indexName] = $table2Index; |
||
277 | $changes++; |
||
278 | } |
||
279 | |||
280 | if ($this->flags & self::DETECT_INDEX_RENAMINGS) { |
||
281 | $this->detectIndexRenamings($tableDifferences); |
||
282 | } |
||
283 | |||
284 | $fromFkeys = $table1->getForeignKeys(); |
||
285 | $toFkeys = $table2->getForeignKeys(); |
||
286 | |||
287 | foreach ($fromFkeys as $key1 => $constraint1) { |
||
288 | foreach ($toFkeys as $key2 => $constraint2) { |
||
289 | if ($this->diffForeignKey($constraint1, $constraint2) === false) { |
||
290 | unset($fromFkeys[$key1], $toFkeys[$key2]); |
||
291 | } else { |
||
292 | if (strtolower($constraint1->getName()) === strtolower($constraint2->getName())) { |
||
293 | $tableDifferences->changedForeignKeys[] = $constraint2; |
||
294 | $changes++; |
||
295 | unset($fromFkeys[$key1], $toFkeys[$key2]); |
||
296 | } |
||
297 | } |
||
298 | } |
||
299 | } |
||
300 | |||
301 | foreach ($fromFkeys as $constraint1) { |
||
302 | $tableDifferences->removedForeignKeys[] = $constraint1; |
||
303 | $changes++; |
||
304 | } |
||
305 | |||
306 | foreach ($toFkeys as $constraint2) { |
||
307 | $tableDifferences->addedForeignKeys[] = $constraint2; |
||
308 | $changes++; |
||
309 | } |
||
310 | |||
311 | return $changes ? $tableDifferences : false; |
||
312 | } |
||
313 | |||
314 | /** |
||
315 | * Try to find columns that only changed their name, rename operations maybe cheaper than add/drop |
||
316 | * however ambiguities between different possibilities should not lead to renaming at all. |
||
317 | * |
||
318 | * @return void |
||
319 | */ |
||
320 | private function detectColumnRenamings(TableDiff $tableDifferences) |
||
321 | { |
||
322 | $renameCandidates = []; |
||
323 | foreach ($tableDifferences->addedColumns as $addedColumnName => $addedColumn) { |
||
324 | foreach ($tableDifferences->removedColumns as $removedColumn) { |
||
325 | if (count($this->diffColumn($addedColumn, $removedColumn)) !== 0) { |
||
326 | continue; |
||
327 | } |
||
328 | |||
329 | $renameCandidates[$addedColumn->getName()][] = [$removedColumn, $addedColumn, $addedColumnName]; |
||
330 | } |
||
331 | } |
||
332 | |||
333 | foreach ($renameCandidates as $candidateColumns) { |
||
334 | if (count($candidateColumns) !== 1) { |
||
335 | continue; |
||
336 | } |
||
337 | |||
338 | [$removedColumn, $addedColumn] = $candidateColumns[0]; |
||
339 | $removedColumnName = strtolower($removedColumn->getName()); |
||
340 | $addedColumnName = strtolower($addedColumn->getName()); |
||
341 | |||
342 | if (isset($tableDifferences->renamedColumns[$removedColumnName])) { |
||
343 | continue; |
||
344 | } |
||
345 | |||
346 | $tableDifferences->renamedColumns[$removedColumnName] = $addedColumn; |
||
347 | unset( |
||
348 | $tableDifferences->addedColumns[$addedColumnName], |
||
349 | $tableDifferences->removedColumns[$removedColumnName] |
||
350 | ); |
||
351 | } |
||
352 | } |
||
353 | |||
354 | /** |
||
355 | * Try to find indexes that only changed their name, rename operations maybe cheaper than add/drop |
||
356 | * however ambiguities between different possibilities should not lead to renaming at all. |
||
357 | * |
||
358 | * @return void |
||
359 | */ |
||
360 | private function detectIndexRenamings(TableDiff $tableDifferences) |
||
397 | ); |
||
398 | } |
||
399 | } |
||
400 | |||
401 | /** |
||
402 | * @return bool |
||
403 | */ |
||
404 | public function diffForeignKey(ForeignKeyConstraint $key1, ForeignKeyConstraint $key2) |
||
405 | { |
||
406 | if (array_map('strtolower', $key1->getUnquotedLocalColumns()) !== array_map('strtolower', $key2->getUnquotedLocalColumns())) { |
||
407 | return true; |
||
408 | } |
||
409 | |||
410 | if (array_map('strtolower', $key1->getUnquotedForeignColumns()) !== array_map('strtolower', $key2->getUnquotedForeignColumns())) { |
||
411 | return true; |
||
412 | } |
||
413 | |||
414 | if ($key1->getUnqualifiedForeignTableName() !== $key2->getUnqualifiedForeignTableName()) { |
||
415 | return true; |
||
416 | } |
||
417 | |||
418 | if ($key1->onUpdate() !== $key2->onUpdate()) { |
||
419 | return true; |
||
420 | } |
||
421 | |||
422 | return $key1->onDelete() !== $key2->onDelete(); |
||
423 | } |
||
424 | |||
425 | /** |
||
426 | * Returns the difference between the fields $field1 and $field2. |
||
427 | * |
||
428 | * If there are differences this method returns $field2, otherwise the |
||
429 | * boolean false. |
||
430 | * |
||
431 | * @return string[] |
||
432 | */ |
||
433 | public function diffColumn(Column $column1, Column $column2) |
||
519 | } |
||
520 | |||
521 | /** |
||
522 | * TODO: kill with fire on v3.0 |
||
523 | * |
||
524 | * @deprecated |
||
525 | */ |
||
526 | private function isALegacyJsonComparison(Types\Type $one, Types\Type $other) : bool |
||
534 | } |
||
535 | |||
536 | /** |
||
537 | * Finds the difference between the indexes $index1 and $index2. |
||
538 | * |
||
539 | * Compares $index1 with $index2 and returns $index2 if there are any |
||
540 | * differences or false in case there are no differences. |
||
541 | * |
||
542 | * @return bool |
||
543 | */ |
||
544 | public function diffIndex(Index $index1, Index $index2) |
||
547 | } |
||
548 | } |
||
549 |