Completed
Pull Request — master (#1753)
by
unknown
07:27
created

Plan::gatherTableMoves()   B

Complexity

Conditions 7
Paths 4

Size

Total Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 22
rs 8.6346
c 0
b 0
f 0
cc 7
nc 4
nop 1
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 $tableUpdate) {
221
            array_push($tableUpdates, ...$splitter->split($tableUpdate));
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
            $constraint = $this->remapContraintAndIndexConflicts($constraint);
242
            array_push($constraints, ...$splitter->split($constraint));
243
        }
244
        $this->constraints = $constraints;
245
    }
246
247
    /**
248
     * Deletes all actions related to the given table and keeps the
249
     * rest
250
     *
251
     * @param \Phinx\Db\Table\Table $table The table to find in the list of actions
252
     * @param \Phinx\Db\Plan\AlterTable[] $actions The actions to transform
253
     *
254
     * @return \Phinx\Db\Plan\AlterTable[] The list of actions without actions for the given table
255
     */
256
    protected function forgetTable(Table $table, $actions)
257
    {
258
        $result = [];
259
        foreach ($actions as $action) {
260
            if ($action->getTable()->getName() === $table->getName()) {
261
                continue;
262
            }
263
            $result[] = $action;
264
        }
265
266
        return $result;
267
    }
268
269
    /**
270
     * Finds all DropForeignKey actions in an AlterTable and moves
271
     * all conflicting DropIndex action in `$this->indexes` into the
272
     * given AlterTable.
273
     *
274
     * @param \Phinx\Db\Plan\AlterTable $alter The collection of actions to inspect
275
     *
276
     * @return \Phinx\Db\Plan\AlterTable The updated AlterTable object. This function
277
     * has the side effect of changing the `$this->indexes` property.
278
     */
279
    protected function remapContraintAndIndexConflicts(AlterTable $alter)
280
    {
281
        $newAlter = new AlterTable($alter->getTable());
282
283
        foreach ($alter->getActions() as $action) {
284
            $newAlter->addAction($action);
285
            if ($action instanceof DropForeignKey) {
286
                list($this->indexes, $dropIndexActions) = $this->forgetDropIndex(
287
                    $action->getTable(),
288
                    $action->getForeignKey()->getColumns(),
289
                    $this->indexes
290
                );
291
                foreach ($dropIndexActions as $dropIndexAction) {
292
                    $newAlter->addAction($dropIndexAction);
293
                }
294
            }
295
        }
296
297
        return $newAlter;
298
    }
299
300
    /**
301
     * Deletes any DropIndex actions for the given table and exact columns
302
     *
303
     * @param \Phinx\Db\Table\Table $table The table to find in the list of actions
304
     * @param string[] $columns The column names to match
305
     * @param \Phinx\Db\Plan\AlterTable[] $actions The actions to transform
306
     *
307
     * @return array A tuple containing the list of actions without actions for dropping the index
308
     * and a list of drop index actions that were removed.
309
     */
310 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...
311
    {
312
        $dropIndexActions = new ArrayObject();
313
        $indexes = array_map(function ($alter) use ($table, $columns, $dropIndexActions) {
314
            if ($alter->getTable()->getName() !== $table->getName()) {
315
                return $alter;
316
            }
317
318
            $newAlter = new AlterTable($table);
319
            foreach ($alter->getActions() as $action) {
320
                if ($action instanceof DropIndex && $action->getIndex()->getColumns() === $columns) {
321
                    $dropIndexActions->append($action);
322
                } else {
323
                    $newAlter->addAction($action);
324
                }
325
            }
326
327
            return $newAlter;
328
        }, $actions);
329
330
        return [$indexes, $dropIndexActions->getArrayCopy()];
331
    }
332
333
    /**
334
     * Deletes any RemoveColumn actions for the given table and exact columns
335
     *
336
     * @param \Phinx\Db\Table\Table $table The table to find in the list of actions
337
     * @param string[] $columns The column names to match
338
     * @param \Phinx\Db\Plan\AlterTable[] $actions The actions to transform
339
     *
340
     * @return array A tuple containing the list of actions without actions for removing the column
341
     * and a list of remove column actions that were removed.
342
     */
343 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...
344
    {
345
        $removeColumnActions = new ArrayObject();
346
        $indexes = array_map(function ($alter) use ($table, $columns, $removeColumnActions) {
347
            if ($alter->getTable()->getName() !== $table->getName()) {
348
                return $alter;
349
            }
350
351
            $newAlter = new AlterTable($table);
352
            foreach ($alter->getActions() as $action) {
353
                if ($action instanceof RemoveColumn && in_array($action->getColumn()->getName(), $columns, true)) {
354
                    $removeColumnActions->append($action);
355
                } else {
356
                    $newAlter->addAction($action);
357
                }
358
            }
359
360
            return $newAlter;
361
        }, $actions);
362
363
        return [$indexes, $removeColumnActions->getArrayCopy()];
364
    }
365
366
    /**
367
     * Collects all table creation actions from the given intent
368
     *
369
     * @param \Phinx\Db\Action\Action[] $actions The actions to parse
370
     *
371
     * @return void
372
     */
373
    protected function gatherCreates($actions)
374
    {
375
        foreach ($actions as $action) {
376
            if ($action instanceof CreateTable) {
377
                $this->tableCreates[$action->getTable()->getName()] = new NewTable($action->getTable());
378
            }
379
        }
380
381
        foreach ($actions as $action) {
382
            if (
383
                ($action instanceof AddColumn || $action instanceof AddIndex)
384
                && isset($this->tableCreates[$action->getTable()->getName()])
385
            ) {
386
                $table = $action->getTable();
387
388
                if ($action instanceof AddColumn) {
389
                    $this->tableCreates[$table->getName()]->addColumn($action->getColumn());
390
                }
391
392
                if ($action instanceof AddIndex) {
393
                    $this->tableCreates[$table->getName()]->addIndex($action->getIndex());
394
                }
395
            }
396
        }
397
    }
398
399
    /**
400
     * Collects all alter table actions from the given intent
401
     *
402
     * @param \Phinx\Db\Action\Action[] $actions The actions to parse
403
     *
404
     * @return void
405
     */
406
    protected function gatherUpdates($actions)
407
    {
408
        foreach ($actions as $action) {
409
            if (
410
                !(
411
                    $action instanceof AddColumn
412
                    || $action instanceof ChangeColumn
413
                    || $action instanceof RemoveColumn
414
                    || $action instanceof RenameColumn
415
                )
416
                || isset($this->tableCreates[$action->getTable()->getName()])
417
            ) {
418
                 continue;
419
            }
420
            $table = $action->getTable();
421
            $name = $table->getName();
422
423
            if ($action instanceof RemoveColumn) {
424
                if (!isset($this->columnRemoves[$name])) {
425
                    $this->columnRemoves[$name] = new AlterTable($table);
426
                }
427
                $this->columnRemoves[$name]->addAction($action);
428
            } else {
429
                if (!isset($this->tableUpdates[$name])) {
430
                    $this->tableUpdates[$name] = new AlterTable($table);
431
                }
432
                $this->tableUpdates[$name]->addAction($action);
433
            }
434
        }
435
    }
436
437
    /**
438
     * Collects all alter table drop and renames from the given intent
439
     *
440
     * @param \Phinx\Db\Action\Action[] $actions The actions to parse
441
     * @return void
442
     */
443
    protected function gatherTableMoves($actions)
444
    {
445
        foreach ($actions as $action) {
446
            if (
447
                !(
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 (
477
                !(
478
                    $action instanceof AddIndex
479
                    || $action instanceof DropIndex
480
                )
481
                || isset($this->tableCreates[$action->getTable()->getName()])
482
            ) {
483
                continue;
484
            }
485
486
            $table = $action->getTable();
487
            $name = $table->getName();
488
489
            if (!isset($this->indexes[$name])) {
490
                $this->indexes[$name] = new AlterTable($table);
491
            }
492
493
            $this->indexes[$name]->addAction($action);
494
        }
495
    }
496
497
    /**
498
     * Collects all foreign key creation and drops from the given intent
499
     *
500
     * @param \Phinx\Db\Action\Action[] $actions The actions to parse
501
     *
502
     * @return void
503
     */
504
    protected function gatherConstraints($actions)
505
    {
506
        foreach ($actions as $action) {
507
            if (!($action instanceof AddForeignKey || $action instanceof DropForeignKey)) {
508
                continue;
509
            }
510
            $table = $action->getTable();
511
            $name = $table->getName();
512
513
            if (!isset($this->constraints[$name])) {
514
                $this->constraints[$name] = new AlterTable($table);
515
            }
516
517
            $this->constraints[$name]->addAction($action);
518
        }
519
    }
520
}
521