Plan::forgetRemoveColumn()   A
last analyzed

Complexity

Conditions 5
Paths 1

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 13
dl 0
loc 21
c 0
b 0
f 0
rs 9.5222
cc 5
nc 1
nop 3
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
     * @return void
94
     */
95
    protected function createPlan($actions)
96
    {
97
        $this->gatherCreates($actions);
98
        $this->gatherUpdates($actions);
99
        $this->gatherTableMoves($actions);
100
        $this->gatherIndexes($actions);
101
        $this->gatherConstraints($actions);
102
        $this->resolveConflicts();
103
    }
104
105
    /**
106
     * Returns a nested list of all the steps to execute
107
     *
108
     * @return \Phinx\Db\Plan\AlterTable[][]
109
     */
110
    protected function updatesSequence()
111
    {
112
        return [
113
            $this->tableUpdates,
114
            $this->constraints,
115
            $this->indexes,
116
            $this->columnRemoves,
117
            $this->tableMoves,
118
        ];
119
    }
120
121
    /**
122
     * Returns a nested list of all the steps to execute in inverse order
123
     *
124
     * @return \Phinx\Db\Plan\AlterTable[][]
125
     */
126
    protected function inverseUpdatesSequence()
127
    {
128
        return [
129
            $this->constraints,
130
            $this->tableMoves,
131
            $this->indexes,
132
            $this->columnRemoves,
133
            $this->tableUpdates,
134
        ];
135
    }
136
137
    /**
138
     * Executes this plan using the given AdapterInterface
139
     *
140
     * @param \Phinx\Db\Adapter\AdapterInterface $executor The executor object for the plan
141
     * @return void
142
     */
143
    public function execute(AdapterInterface $executor)
144
    {
145
        foreach ($this->tableCreates as $newTable) {
146
            $executor->createTable($newTable->getTable(), $newTable->getColumns(), $newTable->getIndexes());
147
        }
148
149
        foreach ($this->updatesSequence() as $updates) {
150
            foreach ($updates as $update) {
151
                $executor->executeActions($update->getTable(), $update->getActions());
152
            }
153
        }
154
    }
155
156
    /**
157
     * Executes the inverse plan (rollback the actions) with the given AdapterInterface:w
158
     *
159
     * @param \Phinx\Db\Adapter\AdapterInterface $executor The executor object for the plan
160
     * @return void
161
     */
162
    public function executeInverse(AdapterInterface $executor)
163
    {
164
        foreach ($this->inverseUpdatesSequence() as $updates) {
165
            foreach ($updates as $update) {
166
                $executor->executeActions($update->getTable(), $update->getActions());
167
            }
168
        }
169
170
        foreach ($this->tableCreates as $newTable) {
171
            $executor->createTable($newTable->getTable(), $newTable->getColumns(), $newTable->getIndexes());
172
        }
173
    }
174
175
    /**
176
     * Deletes certain actions from the plan if they are found to be conflicting or redundant.
177
     *
178
     * @return void
179
     */
180
    protected function resolveConflicts()
181
    {
182
        foreach ($this->tableMoves as $alterTable) {
183
            foreach ($alterTable->getActions() as $action) {
184
                if ($action instanceof DropTable) {
185
                    $this->tableUpdates = $this->forgetTable($action->getTable(), $this->tableUpdates);
186
                    $this->constraints = $this->forgetTable($action->getTable(), $this->constraints);
187
                    $this->indexes = $this->forgetTable($action->getTable(), $this->indexes);
188
                    $this->columnRemoves = $this->forgetTable($action->getTable(), $this->columnRemoves);
189
                }
190
            }
191
        }
192
193
        // Columns that are dropped will automatically cause the indexes to be dropped as well
194
        foreach ($this->columnRemoves as $columnRemove) {
195
            foreach ($columnRemove->getActions() as $action) {
196
                if ($action instanceof RemoveColumn) {
197
                    [$this->indexes] = $this->forgetDropIndex(
198
                        $action->getTable(),
199
                        [$action->getColumn()->getName()],
200
                        $this->indexes
201
                    );
202
                }
203
            }
204
        }
205
206
        // Renaming a column and then changing the renamed column is something people do,
207
        // but it is a conflicting action. Luckily solving the conflict can be done by moving
208
        // the ChangeColumn action to another AlterTable.
209
        $splitter = new ActionSplitter(
210
            RenameColumn::class,
211
            ChangeColumn::class,
212
            function (RenameColumn $a, ChangeColumn $b) {
213
                return $a->getNewName() === $b->getColumnName();
214
            }
215
        );
216
        $tableUpdates = [];
217
        foreach ($this->tableUpdates as $update) {
218
            $tableUpdates = array_merge($tableUpdates, $splitter($update));
219
        }
220
        $this->tableUpdates = $tableUpdates;
221
222
        // Dropping indexes used by foreign keys is a conflict, but one we can resolve
223
        // if the foreign key is also scheduled to be dropped. If we can find such a a case,
224
        // we force the execution of the index drop after the foreign key is dropped.
225
        // Changing constraint properties sometimes require dropping it and then
226
        // creating it again with the new stuff. Unfortunately, we have already bundled
227
        // everything together in as few AlterTable statements as we could, so we need to
228
        // resolve this conflict manually.
229
        $splitter = new ActionSplitter(
230
            DropForeignKey::class,
231
            AddForeignKey::class,
232
            function (DropForeignKey $a, AddForeignKey $b) {
233
                return $a->getForeignKey()->getColumns() === $b->getForeignKey()->getColumns();
234
            }
235
        );
236
        $constraints = [];
237
        foreach ($this->constraints as $constraint) {
238
            $constraints = array_merge(
239
                $constraints,
240
                $splitter($this->remapContraintAndIndexConflicts($constraint))
241
            );
242
        }
243
        $this->constraints = $constraints;
244
    }
245
246
    /**
247
     * Deletes all actions related to the given table and keeps the
248
     * rest
249
     *
250
     * @param \Phinx\Db\Table\Table $table The table to find in the list of actions
251
     * @param \Phinx\Db\Plan\AlterTable[] $actions The actions to transform
252
     * @return \Phinx\Db\Plan\AlterTable[] The list of actions without actions for the given table
253
     */
254
    protected function forgetTable(Table $table, $actions)
255
    {
256
        $result = [];
257
        foreach ($actions as $action) {
258
            if ($action->getTable()->getName() === $table->getName()) {
259
                continue;
260
            }
261
            $result[] = $action;
262
        }
263
264
        return $result;
265
    }
266
267
    /**
268
     * Finds all DropForeignKey actions in an AlterTable and moves
269
     * all conflicting DropIndex action in `$this->indexes` into the
270
     * given AlterTable.
271
     *
272
     * @param \Phinx\Db\Plan\AlterTable $alter The collection of actions to inspect
273
     * @return \Phinx\Db\Plan\AlterTable The updated AlterTable object. This function
274
     * has the side effect of changing the `$this->indexes` property.
275
     */
276
    protected function remapContraintAndIndexConflicts(AlterTable $alter)
277
    {
278
        $newAlter = new AlterTable($alter->getTable());
279
280
        foreach ($alter->getActions() as $action) {
281
            $newAlter->addAction($action);
282
            if ($action instanceof DropForeignKey) {
283
                [$this->indexes, $dropIndexActions] = $this->forgetDropIndex(
284
                    $action->getTable(),
285
                    $action->getForeignKey()->getColumns(),
286
                    $this->indexes
287
                );
288
                foreach ($dropIndexActions as $dropIndexAction) {
289
                    $newAlter->addAction($dropIndexAction);
290
                }
291
            }
292
        }
293
294
        return $newAlter;
295
    }
296
297
    /**
298
     * Deletes any DropIndex actions for the given table and exact columns
299
     *
300
     * @param \Phinx\Db\Table\Table $table The table to find in the list of actions
301
     * @param string[] $columns The column names to match
302
     * @param \Phinx\Db\Plan\AlterTable[] $actions The actions to transform
303
     * @return array A tuple containing the list of actions without actions for dropping the index
304
     * and a list of drop index actions that were removed.
305
     */
306
    protected function forgetDropIndex(Table $table, array $columns, array $actions)
307
    {
308
        $dropIndexActions = new ArrayObject();
309
        $indexes = array_map(function ($alter) use ($table, $columns, $dropIndexActions) {
310
            if ($alter->getTable()->getName() !== $table->getName()) {
311
                return $alter;
312
            }
313
314
            $newAlter = new AlterTable($table);
315
            foreach ($alter->getActions() as $action) {
316
                if ($action instanceof DropIndex && $action->getIndex()->getColumns() === $columns) {
317
                    $dropIndexActions->append($action);
318
                } else {
319
                    $newAlter->addAction($action);
320
                }
321
            }
322
323
            return $newAlter;
324
        }, $actions);
325
326
        return [$indexes, $dropIndexActions->getArrayCopy()];
327
    }
328
329
    /**
330
     * Deletes any RemoveColumn actions for the given table and exact columns
331
     *
332
     * @param \Phinx\Db\Table\Table $table The table to find in the list of actions
333
     * @param string[] $columns The column names to match
334
     * @param \Phinx\Db\Plan\AlterTable[] $actions The actions to transform
335
     * @return array A tuple containing the list of actions without actions for removing the column
336
     * and a list of remove column actions that were removed.
337
     */
338
    protected function forgetRemoveColumn(Table $table, array $columns, array $actions)
339
    {
340
        $removeColumnActions = new ArrayObject();
341
        $indexes = array_map(function ($alter) use ($table, $columns, $removeColumnActions) {
342
            if ($alter->getTable()->getName() !== $table->getName()) {
343
                return $alter;
344
            }
345
346
            $newAlter = new AlterTable($table);
347
            foreach ($alter->getActions() as $action) {
348
                if ($action instanceof RemoveColumn && in_array($action->getColumn()->getName(), $columns, true)) {
349
                    $removeColumnActions->append($action);
350
                } else {
351
                    $newAlter->addAction($action);
352
                }
353
            }
354
355
            return $newAlter;
356
        }, $actions);
357
358
        return [$indexes, $removeColumnActions->getArrayCopy()];
359
    }
360
361
    /**
362
     * Collects all table creation actions from the given intent
363
     *
364
     * @param \Phinx\Db\Action\Action[] $actions The actions to parse
365
     * @return void
366
     */
367
    protected function gatherCreates($actions)
368
    {
369
        foreach ($actions as $action) {
370
            if ($action instanceof CreateTable) {
371
                $this->tableCreates[$action->getTable()->getName()] = new NewTable($action->getTable());
372
            }
373
        }
374
375
        foreach ($actions as $action) {
376
            if (
377
                ($action instanceof AddColumn || $action instanceof AddIndex)
378
                && isset($this->tableCreates[$action->getTable()->getName()])
379
            ) {
380
                $table = $action->getTable();
381
382
                if ($action instanceof AddColumn) {
383
                    $this->tableCreates[$table->getName()]->addColumn($action->getColumn());
384
                }
385
386
                if ($action instanceof AddIndex) {
387
                    $this->tableCreates[$table->getName()]->addIndex($action->getIndex());
388
                }
389
            }
390
        }
391
    }
392
393
    /**
394
     * Collects all alter table actions from the given intent
395
     *
396
     * @param \Phinx\Db\Action\Action[] $actions The actions to parse
397
     * @return void
398
     */
399
    protected function gatherUpdates($actions)
400
    {
401
        foreach ($actions as $action) {
402
            if (
403
                !($action instanceof AddColumn)
404
                && !($action instanceof ChangeColumn)
405
                && !($action instanceof RemoveColumn)
406
                && !($action instanceof RenameColumn)
407
            ) {
408
                 continue;
409
            } elseif (isset($this->tableCreates[$action->getTable()->getName()])) {
410
                continue;
411
            }
412
            $table = $action->getTable();
413
            $name = $table->getName();
414
415
            if ($action instanceof RemoveColumn) {
416
                if (!isset($this->columnRemoves[$name])) {
417
                    $this->columnRemoves[$name] = new AlterTable($table);
418
                }
419
                $this->columnRemoves[$name]->addAction($action);
420
            } else {
421
                if (!isset($this->tableUpdates[$name])) {
422
                    $this->tableUpdates[$name] = new AlterTable($table);
423
                }
424
                $this->tableUpdates[$name]->addAction($action);
425
            }
426
        }
427
    }
428
429
    /**
430
     * Collects all alter table drop and renames from the given intent
431
     *
432
     * @param \Phinx\Db\Action\Action[] $actions The actions to parse
433
     * @return void
434
     */
435
    protected function gatherTableMoves($actions)
436
    {
437
        foreach ($actions as $action) {
438
            if (
439
                !($action instanceof DropTable)
440
                && !($action instanceof RenameTable)
441
                && !($action instanceof ChangePrimaryKey)
442
                && !($action instanceof ChangeComment)
443
            ) {
444
                continue;
445
            }
446
            $table = $action->getTable();
447
            $name = $table->getName();
448
449
            if (!isset($this->tableMoves[$name])) {
450
                $this->tableMoves[$name] = new AlterTable($table);
451
            }
452
453
            $this->tableMoves[$name]->addAction($action);
454
        }
455
    }
456
457
    /**
458
     * Collects all index creation and drops from the given intent
459
     *
460
     * @param \Phinx\Db\Action\Action[] $actions The actions to parse
461
     * @return void
462
     */
463
    protected function gatherIndexes($actions)
464
    {
465
        foreach ($actions as $action) {
466
            if (!($action instanceof AddIndex) && !($action instanceof DropIndex)) {
467
                continue;
468
            } elseif (isset($this->tableCreates[$action->getTable()->getName()])) {
469
                continue;
470
            }
471
472
            $table = $action->getTable();
473
            $name = $table->getName();
474
475
            if (!isset($this->indexes[$name])) {
476
                $this->indexes[$name] = new AlterTable($table);
477
            }
478
479
            $this->indexes[$name]->addAction($action);
480
        }
481
    }
482
483
    /**
484
     * Collects all foreign key creation and drops from the given intent
485
     *
486
     * @param \Phinx\Db\Action\Action[] $actions The actions to parse
487
     * @return void
488
     */
489
    protected function gatherConstraints($actions)
490
    {
491
        foreach ($actions as $action) {
492
            if (!($action instanceof AddForeignKey || $action instanceof DropForeignKey)) {
493
                continue;
494
            }
495
            $table = $action->getTable();
496
            $name = $table->getName();
497
498
            if (!isset($this->constraints[$name])) {
499
                $this->constraints[$name] = new AlterTable($table);
500
            }
501
502
            $this->constraints[$name]->addAction($action);
503
        }
504
    }
505
}
506