Completed
Pull Request — master (#1753)
by
unknown
01:44
created

Plan   F

Complexity

Total Complexity 75

Size/Duplication

Total Lines 482
Duplicated Lines 18.26 %

Coupling/Cohesion

Components 1
Dependencies 19

Importance

Changes 0
Metric Value
wmc 75
lcom 1
cbo 19
dl 88
loc 482
rs 2.4
c 0
b 0
f 0

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A createPlan() 0 9 1
A updatesSequence() 10 10 1
A inverseUpdatesSequence() 10 10 1
A execute() 12 12 4
A executeInverse() 12 12 4
B resolveConflicts() 0 65 9
A forgetTable() 0 12 3
A remapContraintAndIndexConflicts() 0 20 4
A forgetDropIndex() 22 22 5
A forgetRemoveColumn() 22 22 5
B gatherCreates() 0 25 9
B gatherUpdates() 0 29 10
B gatherTableMoves() 0 21 7
A gatherIndexes() 0 19 6
A gatherConstraints() 0 16 5

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Plan 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 Plan, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * MIT License
5
 * For full license information, please view the LICENSE file that was distributed with this source code.
6
 */
7
8
namespace Phinx\Db\Plan;
9
10
use ArrayObject;
11
use Phinx\Db\Action\AddColumn;
12
use Phinx\Db\Action\AddForeignKey;
13
use Phinx\Db\Action\AddIndex;
14
use Phinx\Db\Action\ChangeColumn;
15
use Phinx\Db\Action\ChangeComment;
16
use Phinx\Db\Action\ChangePrimaryKey;
17
use Phinx\Db\Action\CreateTable;
18
use Phinx\Db\Action\DropForeignKey;
19
use Phinx\Db\Action\DropIndex;
20
use Phinx\Db\Action\DropTable;
21
use Phinx\Db\Action\RemoveColumn;
22
use Phinx\Db\Action\RenameColumn;
23
use Phinx\Db\Action\RenameTable;
24
use Phinx\Db\Adapter\AdapterInterface;
25
use Phinx\Db\Plan\Solver\ActionSplitter;
26
use Phinx\Db\Table\Table;
27
28
/**
29
 * A Plan takes an Intent and transforms int into a sequence of
30
 * instructions that can be correctly executed by an AdapterInterface.
31
 *
32
 * The main focus of Plan is to arrange the actions in the most efficient
33
 * way possible for the database.
34
 */
35
class Plan
36
{
37
    /**
38
     * List of tables to be created
39
     *
40
     * @var \Phinx\Db\Plan\NewTable[]
41
     */
42
    protected $tableCreates = [];
43
44
    /**
45
     * List of table updates
46
     *
47
     * @var \Phinx\Db\Plan\AlterTable[]
48
     */
49
    protected $tableUpdates = [];
50
51
    /**
52
     * List of table removals or renames
53
     *
54
     * @var \Phinx\Db\Plan\AlterTable[]
55
     */
56
    protected $tableMoves = [];
57
58
    /**
59
     * List of index additions or removals
60
     *
61
     * @var \Phinx\Db\Plan\AlterTable[]
62
     */
63
    protected $indexes = [];
64
65
    /**
66
     * List of constraint additions or removals
67
     *
68
     * @var \Phinx\Db\Plan\AlterTable[]
69
     */
70
    protected $constraints = [];
71
72
    /**
73
     * List of dropped columns
74
     *
75
     * @var \Phinx\Db\Plan\AlterTable[]
76
     */
77
    protected $columnRemoves = [];
78
79
    /**
80
     * Constructor
81
     *
82
     * @param \Phinx\Db\Plan\Intent $intent All the actions that should be executed
83
     */
84
    public function __construct(Intent $intent)
85
    {
86
        $this->createPlan($intent->getActions());
87
    }
88
89
    /**
90
     * Parses the given Intent and creates the separate steps to execute
91
     *
92
     * @param \Phinx\Db\Action\Action[] $actions The actions to use for the plan
93
     *
94
     * @return void
95
     */
96
    protected function createPlan($actions)
97
    {
98
        $this->gatherCreates($actions);
99
        $this->gatherUpdates($actions);
100
        $this->gatherTableMoves($actions);
101
        $this->gatherIndexes($actions);
102
        $this->gatherConstraints($actions);
103
        $this->resolveConflicts();
104
    }
105
106
    /**
107
     * Returns a nested list of all the steps to execute
108
     *
109
     * @return \Phinx\Db\Plan\AlterTable[][]
110
     */
111 View Code Duplication
    protected function updatesSequence()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
112
    {
113
        return [
114
            $this->tableUpdates,
115
            $this->constraints,
116
            $this->indexes,
117
            $this->columnRemoves,
118
            $this->tableMoves,
119
        ];
120
    }
121
122
    /**
123
     * Returns a nested list of all the steps to execute in inverse order
124
     *
125
     * @return \Phinx\Db\Plan\AlterTable[][]
126
     */
127 View Code Duplication
    protected function inverseUpdatesSequence()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
128
    {
129
        return [
130
            $this->constraints,
131
            $this->tableMoves,
132
            $this->indexes,
133
            $this->columnRemoves,
134
            $this->tableUpdates,
135
        ];
136
    }
137
138
    /**
139
     * Executes this plan using the given AdapterInterface
140
     *
141
     * @param \Phinx\Db\Adapter\AdapterInterface $executor The executor object for the plan
142
     *
143
     * @return void
144
     */
145 View Code Duplication
    public function execute(AdapterInterface $executor)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
146
    {
147
        foreach ($this->tableCreates as $newTable) {
148
            $executor->createTable($newTable->getTable(), $newTable->getColumns(), $newTable->getIndexes());
149
        }
150
151
        foreach ($this->updatesSequence() as $updates) {
152
            foreach ($updates as $update) {
153
                $executor->executeActions($update->getTable(), $update->getActions());
154
            }
155
        }
156
    }
157
158
    /**
159
     * Executes the inverse plan (rollback the actions) with the given AdapterInterface:w
160
     *
161
     * @param \Phinx\Db\Adapter\AdapterInterface $executor The executor object for the plan
162
     *
163
     * @return void
164
     */
165 View Code Duplication
    public function executeInverse(AdapterInterface $executor)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
166
    {
167
        foreach ($this->inverseUpdatesSequence() as $updates) {
168
            foreach ($updates as $update) {
169
                $executor->executeActions($update->getTable(), $update->getActions());
170
            }
171
        }
172
173
        foreach ($this->tableCreates as $newTable) {
174
            $executor->createTable($newTable->getTable(), $newTable->getColumns(), $newTable->getIndexes());
175
        }
176
    }
177
178
    /**
179
     * Deletes certain actions from the plan if they are found to be conflicting or redundant.
180
     *
181
     * @return void
182
     */
183
    protected function resolveConflicts()
184
    {
185
        foreach ($this->tableMoves as $alterTable) {
186
            foreach ($alterTable->getActions() as $action) {
187
                if ($action instanceof DropTable) {
188
                    $this->tableUpdates = $this->forgetTable($action->getTable(), $this->tableUpdates);
189
                    $this->constraints = $this->forgetTable($action->getTable(), $this->constraints);
190
                    $this->indexes = $this->forgetTable($action->getTable(), $this->indexes);
191
                    $this->columnRemoves = $this->forgetTable($action->getTable(), $this->columnRemoves);
192
                }
193
            }
194
        }
195
196
        // Columns that are dropped will automatically cause the indexes to be dropped as well
197
        foreach ($this->columnRemoves as $columnRemove) {
198
            foreach ($columnRemove->getActions() as $action) {
199
                if ($action instanceof RemoveColumn) {
200
                    list($this->indexes) = $this->forgetDropIndex(
201
                        $action->getTable(),
202
                        [$action->getColumn()->getName()],
203
                        $this->indexes
204
                    );
205
                }
206
            }
207
        }
208
209
        // Renaming a column and then changing the renamed column is something people do,
210
        // but it is a conflicting action. Luckily solving the conflict can be done by moving
211
        // the ChangeColumn action to another AlterTable.
212
        $splitter = new ActionSplitter(
213
            RenameColumn::class,
214
            ChangeColumn::class,
215
            function (RenameColumn $a, ChangeColumn $b) {
216
                return $a->getNewName() === $b->getColumnName();
217
            }
218
        );
219
        $tableUpdates = [];
220
        foreach ($this->tableUpdates as $update) {
221
            $tableUpdates = array_merge($tableUpdates, $splitter($update));
222
        }
223
        $this->tableUpdates = $tableUpdates;
224
225
        // Dropping indexes used by foreign keys is a conflict, but one we can resolve
226
        // if the foreign key is also scheduled to be dropped. If we can find such a a case,
227
        // we force the execution of the index drop after the foreign key is dropped.
228
        // Changing constraint properties sometimes require dropping it and then
229
        // creating it again with the new stuff. Unfortunately, we have already bundled
230
        // everything together in as few AlterTable statements as we could, so we need to
231
        // resolve this conflict manually.
232
        $splitter = new ActionSplitter(
233
            DropForeignKey::class,
234
            AddForeignKey::class,
235
            function (DropForeignKey $a, AddForeignKey $b) {
236
                return $a->getForeignKey()->getColumns() === $b->getForeignKey()->getColumns();
237
            }
238
        );
239
        $constraints = [];
240
        foreach ($this->constraints as $constraint) {
241
            $constraints = array_merge(
242
                $constraints,
243
                $splitter($this->remapContraintAndIndexConflicts($constraint))
244
            );
245
        }
246
        $this->constraints = $constraints;
247
    }
248
249
    /**
250
     * Deletes all actions related to the given table and keeps the
251
     * rest
252
     *
253
     * @param \Phinx\Db\Table\Table $table The table to find in the list of actions
254
     * @param \Phinx\Db\Plan\AlterTable[] $actions The actions to transform
255
     *
256
     * @return \Phinx\Db\Plan\AlterTable[] The list of actions without actions for the given table
257
     */
258
    protected function forgetTable(Table $table, $actions)
259
    {
260
        $result = [];
261
        foreach ($actions as $action) {
262
            if ($action->getTable()->getName() === $table->getName()) {
263
                continue;
264
            }
265
            $result[] = $action;
266
        }
267
268
        return $result;
269
    }
270
271
    /**
272
     * Finds all DropForeignKey actions in an AlterTable and moves
273
     * all conflicting DropIndex action in `$this->indexes` into the
274
     * given AlterTable.
275
     *
276
     * @param \Phinx\Db\Plan\AlterTable $alter The collection of actions to inspect
277
     *
278
     * @return \Phinx\Db\Plan\AlterTable The updated AlterTable object. This function
279
     * has the side effect of changing the `$this->indexes` property.
280
     */
281
    protected function remapContraintAndIndexConflicts(AlterTable $alter)
282
    {
283
        $newAlter = new AlterTable($alter->getTable());
284
285
        foreach ($alter->getActions() as $action) {
286
            $newAlter->addAction($action);
287
            if ($action instanceof DropForeignKey) {
288
                list($this->indexes, $dropIndexActions) = $this->forgetDropIndex(
289
                    $action->getTable(),
290
                    $action->getForeignKey()->getColumns(),
291
                    $this->indexes
292
                );
293
                foreach ($dropIndexActions as $dropIndexAction) {
294
                    $newAlter->addAction($dropIndexAction);
295
                }
296
            }
297
        }
298
299
        return $newAlter;
300
    }
301
302
    /**
303
     * Deletes any DropIndex actions for the given table and exact columns
304
     *
305
     * @param \Phinx\Db\Table\Table $table The table to find in the list of actions
306
     * @param string[] $columns The column names to match
307
     * @param \Phinx\Db\Plan\AlterTable[] $actions The actions to transform
308
     *
309
     * @return array A tuple containing the list of actions without actions for dropping the index
310
     * and a list of drop index actions that were removed.
311
     */
312 View Code Duplication
    protected function forgetDropIndex(Table $table, array $columns, array $actions)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
313
    {
314
        $dropIndexActions = new ArrayObject();
315
        $indexes = array_map(function ($alter) use ($table, $columns, $dropIndexActions) {
316
            if ($alter->getTable()->getName() !== $table->getName()) {
317
                return $alter;
318
            }
319
320
            $newAlter = new AlterTable($table);
321
            foreach ($alter->getActions() as $action) {
322
                if ($action instanceof DropIndex && $action->getIndex()->getColumns() === $columns) {
323
                    $dropIndexActions->append($action);
324
                } else {
325
                    $newAlter->addAction($action);
326
                }
327
            }
328
329
            return $newAlter;
330
        }, $actions);
331
332
        return [$indexes, $dropIndexActions->getArrayCopy()];
333
    }
334
335
    /**
336
     * Deletes any RemoveColumn actions for the given table and exact columns
337
     *
338
     * @param \Phinx\Db\Table\Table $table The table to find in the list of actions
339
     * @param string[] $columns The column names to match
340
     * @param \Phinx\Db\Plan\AlterTable[] $actions The actions to transform
341
     *
342
     * @return array A tuple containing the list of actions without actions for removing the column
343
     * and a list of remove column actions that were removed.
344
     */
345 View Code Duplication
    protected function forgetRemoveColumn(Table $table, array $columns, array $actions)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
346
    {
347
        $removeColumnActions = new ArrayObject();
348
        $indexes = array_map(function ($alter) use ($table, $columns, $removeColumnActions) {
349
            if ($alter->getTable()->getName() !== $table->getName()) {
350
                return $alter;
351
            }
352
353
            $newAlter = new AlterTable($table);
354
            foreach ($alter->getActions() as $action) {
355
                if ($action instanceof RemoveColumn && in_array($action->getColumn()->getName(), $columns, true)) {
356
                    $removeColumnActions->append($action);
357
                } else {
358
                    $newAlter->addAction($action);
359
                }
360
            }
361
362
            return $newAlter;
363
        }, $actions);
364
365
        return [$indexes, $removeColumnActions->getArrayCopy()];
366
    }
367
368
    /**
369
     * Collects all table creation actions from the given intent
370
     *
371
     * @param \Phinx\Db\Action\Action[] $actions The actions to parse
372
     *
373
     * @return void
374
     */
375
    protected function gatherCreates($actions)
376
    {
377
        foreach ($actions as $action) {
378
            if ($action instanceof CreateTable) {
379
                $this->tableCreates[$action->getTable()->getName()] = new NewTable($action->getTable());
380
            }
381
        }
382
383
        foreach ($actions as $action) {
384
            if (
385
                ($action instanceof AddColumn || $action instanceof AddIndex)
386
                && isset($this->tableCreates[$action->getTable()->getName()])
387
            ) {
388
                $table = $action->getTable();
389
390
                if ($action instanceof AddColumn) {
391
                    $this->tableCreates[$table->getName()]->addColumn($action->getColumn());
392
                }
393
394
                if ($action instanceof AddIndex) {
395
                    $this->tableCreates[$table->getName()]->addIndex($action->getIndex());
396
                }
397
            }
398
        }
399
    }
400
401
    /**
402
     * Collects all alter table actions from the given intent
403
     *
404
     * @param \Phinx\Db\Action\Action[] $actions The actions to parse
405
     *
406
     * @return void
407
     */
408
    protected function gatherUpdates($actions)
409
    {
410
        foreach ($actions as $action) {
411
            if (
412
                !($action instanceof AddColumn)
413
                && !($action instanceof ChangeColumn)
414
                && !($action instanceof RemoveColumn)
415
                && !($action instanceof RenameColumn)
416
            ) {
417
                 continue;
418
            } elseif (isset($this->tableCreates[$action->getTable()->getName()])) {
419
                continue;
420
            }
421
            $table = $action->getTable();
422
            $name = $table->getName();
423
424
            if ($action instanceof RemoveColumn) {
425
                if (!isset($this->columnRemoves[$name])) {
426
                    $this->columnRemoves[$name] = new AlterTable($table);
427
                }
428
                $this->columnRemoves[$name]->addAction($action);
429
            } else {
430
                if (!isset($this->tableUpdates[$name])) {
431
                    $this->tableUpdates[$name] = new AlterTable($table);
432
                }
433
                $this->tableUpdates[$name]->addAction($action);
434
            }
435
        }
436
    }
437
438
    /**
439
     * Collects all alter table drop and renames from the given intent
440
     *
441
     * @param \Phinx\Db\Action\Action[] $actions The actions to parse
442
     * @return void
443
     */
444
    protected function gatherTableMoves($actions)
445
    {
446
        foreach ($actions as $action) {
447
            if (
448
                !($action instanceof DropTable)
449
                && !($action instanceof RenameTable)
450
                && !($action instanceof ChangePrimaryKey)
451
                && !($action instanceof ChangeComment)
452
            ) {
453
                continue;
454
            }
455
            $table = $action->getTable();
456
            $name = $table->getName();
457
458
            if (!isset($this->tableMoves[$name])) {
459
                $this->tableMoves[$name] = new AlterTable($table);
460
            }
461
462
            $this->tableMoves[$name]->addAction($action);
463
        }
464
    }
465
466
    /**
467
     * Collects all index creation and drops from the given intent
468
     *
469
     * @param \Phinx\Db\Action\Action[] $actions The actions to parse
470
     *
471
     * @return void
472
     */
473
    protected function gatherIndexes($actions)
474
    {
475
        foreach ($actions as $action) {
476
            if (!($action instanceof AddIndex) && !($action instanceof DropIndex)) {
477
                continue;
478
            } elseif (isset($this->tableCreates[$action->getTable()->getName()])) {
479
                continue;
480
            }
481
482
            $table = $action->getTable();
483
            $name = $table->getName();
484
485
            if (!isset($this->indexes[$name])) {
486
                $this->indexes[$name] = new AlterTable($table);
487
            }
488
489
            $this->indexes[$name]->addAction($action);
490
        }
491
    }
492
493
    /**
494
     * Collects all foreign key creation and drops from the given intent
495
     *
496
     * @param \Phinx\Db\Action\Action[] $actions The actions to parse
497
     *
498
     * @return void
499
     */
500
    protected function gatherConstraints($actions)
501
    {
502
        foreach ($actions as $action) {
503
            if (!($action instanceof AddForeignKey || $action instanceof DropForeignKey)) {
504
                continue;
505
            }
506
            $table = $action->getTable();
507
            $name = $table->getName();
508
509
            if (!isset($this->constraints[$name])) {
510
                $this->constraints[$name] = new AlterTable($table);
511
            }
512
513
            $this->constraints[$name]->addAction($action);
514
        }
515
    }
516
}
517