Completed
Pull Request — master (#1753)
by
unknown
05:23
created

Plan::gatherIndexes()   A

Complexity

Conditions 6
Paths 4

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 17
rs 9.0777
c 0
b 0
f 0
cc 6
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
        $splitter = new ActionSplitter(RenameColumn::class, ChangeColumn::class, function (RenameColumn $a, ChangeColumn $b) {
210
            return $a->getNewName() === $b->getColumnName();
211
        });
212
        $tableUpdates = [];
213
        foreach ($this->tableUpdates as $tableUpdate) {
214
            array_push($tableUpdates, ...$splitter->split($tableUpdate));
215
        }
216
        $this->tableUpdates = $tableUpdates;
217
218
        $splitter = new ActionSplitter(DropForeignKey::class, AddForeignKey::class, function (DropForeignKey $a, AddForeignKey $b) {
219
            return $a->getForeignKey()->getColumns() === $b->getForeignKey()->getColumns();
220
        });
221
        $constraints = [];
222
        foreach ($this->constraints as $constraint) {
223
            $constraint = $this->remapContraintAndIndexConflicts($constraint);
224
            array_push($constraints, ...$splitter->split($constraint));
225
        }
226
        $this->constraints = $constraints;
227
    }
228
229
    /**
230
     * Deletes all actions related to the given table and keeps the
231
     * rest
232
     *
233
     * @param \Phinx\Db\Table\Table $table The table to find in the list of actions
234
     * @param \Phinx\Db\Plan\AlterTable[] $actions The actions to transform
235
     *
236
     * @return \Phinx\Db\Plan\AlterTable[] The list of actions without actions for the given table
237
     */
238
    protected function forgetTable(Table $table, $actions)
239
    {
240
        $result = [];
241
        foreach ($actions as $action) {
242
            if ($action->getTable()->getName() === $table->getName()) {
243
                continue;
244
            }
245
            $result[] = $action;
246
        }
247
248
        return $result;
249
    }
250
251
    /**
252
     * Finds all DropForeignKey actions in an AlterTable and moves
253
     * all conflicting DropIndex action in `$this->indexes` into the
254
     * given AlterTable.
255
     *
256
     * @param \Phinx\Db\Plan\AlterTable $alter The collection of actions to inspect
257
     *
258
     * @return \Phinx\Db\Plan\AlterTable The updated AlterTable object. This function
259
     * has the side effect of changing the `$this->indexes` property.
260
     */
261
    protected function remapContraintAndIndexConflicts(AlterTable $alter)
262
    {
263
        $newAlter = new AlterTable($alter->getTable());
264
265
        foreach ($alter->getActions() as $action) {
266
            $newAlter->addAction($action);
267
            if ($action instanceof DropForeignKey) {
268
                list($this->indexes, $dropIndexActions) = $this->forgetDropIndex(
269
                    $action->getTable(),
270
                    $action->getForeignKey()->getColumns(),
271
                    $this->indexes
272
                );
273
                foreach ($dropIndexActions as $dropIndexAction) {
274
                    $newAlter->addAction($dropIndexAction);
275
                }
276
            }
277
        }
278
279
        return $newAlter;
280
    }
281
282
    /**
283
     * Deletes any DropIndex actions for the given table and exact columns
284
     *
285
     * @param \Phinx\Db\Table\Table $table The table to find in the list of actions
286
     * @param string[] $columns The column names to match
287
     * @param \Phinx\Db\Plan\AlterTable[] $actions The actions to transform
288
     *
289
     * @return array A tuple containing the list of actions without actions for dropping the index
290
     * and a list of drop index actions that were removed.
291
     */
292 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...
293
    {
294
        $dropIndexActions = new ArrayObject();
295
        $indexes = array_map(function ($alter) use ($table, $columns, $dropIndexActions) {
296
            if ($alter->getTable()->getName() !== $table->getName()) {
297
                return $alter;
298
            }
299
300
            $newAlter = new AlterTable($table);
301
            foreach ($alter->getActions() as $action) {
302
                if ($action instanceof DropIndex && $action->getIndex()->getColumns() === $columns) {
303
                    $dropIndexActions->append($action);
304
                } else {
305
                    $newAlter->addAction($action);
306
                }
307
            }
308
309
            return $newAlter;
310
        }, $actions);
311
312
        return [$indexes, $dropIndexActions->getArrayCopy()];
313
    }
314
315
    /**
316
     * Deletes any RemoveColumn actions for the given table and exact columns
317
     *
318
     * @param \Phinx\Db\Table\Table $table The table to find in the list of actions
319
     * @param string[] $columns The column names to match
320
     * @param \Phinx\Db\Plan\AlterTable[] $actions The actions to transform
321
     *
322
     * @return array A tuple containing the list of actions without actions for removing the column
323
     * and a list of remove column actions that were removed.
324
     */
325 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...
326
    {
327
        $removeColumnActions = new ArrayObject();
328
        $indexes = array_map(function ($alter) use ($table, $columns, $removeColumnActions) {
329
            if ($alter->getTable()->getName() !== $table->getName()) {
330
                return $alter;
331
            }
332
333
            $newAlter = new AlterTable($table);
334
            foreach ($alter->getActions() as $action) {
335
                if ($action instanceof RemoveColumn && in_array($action->getColumn()->getName(), $columns, true)) {
336
                    $removeColumnActions->append($action);
337
                } else {
338
                    $newAlter->addAction($action);
339
                }
340
            }
341
342
            return $newAlter;
343
        }, $actions);
344
345
        return [$indexes, $removeColumnActions->getArrayCopy()];
346
    }
347
348
    /**
349
     * Collects all table creation actions from the given intent
350
     *
351
     * @param \Phinx\Db\Action\Action[] $actions The actions to parse
352
     *
353
     * @return void
354
     */
355
    protected function gatherCreates($actions)
356
    {
357
        foreach ($actions as $action) {
358
            if ($action instanceof CreateTable) {
359
                $this->tableCreates[$action->getTable()->getName()] = new NewTable($action->getTable());
360
            }
361
        }
362
363
        foreach ($actions as $action) {
364
            if (($action instanceof AddColumn || $action instanceof AddIndex) && isset($this->tableCreates[$action->getTable()->getName()])) {
365
                $table = $action->getTable();
366
367
                if ($action instanceof AddColumn) {
368
                    $this->tableCreates[$table->getName()]->addColumn($action->getColumn());
369
                }
370
371
                if ($action instanceof AddIndex) {
372
                    $this->tableCreates[$table->getName()]->addIndex($action->getIndex());
373
                }
374
            }
375
        }
376
    }
377
378
    /**
379
     * Collects all alter table actions from the given intent
380
     *
381
     * @param \Phinx\Db\Action\Action[] $actions The actions to parse
382
     *
383
     * @return void
384
     */
385
    protected function gatherUpdates($actions)
386
    {
387
        foreach ($actions as $action) {
388
            if (
389
                !(
390
                    $action instanceof AddColumn
391
                    || $action instanceof ChangeColumn
392
                    || $action instanceof RemoveColumn
393
                    || $action instanceof RenameColumn
394
                )
395
                || isset($this->tableCreates[$action->getTable()->getName()])
396
            ) {
397
                 continue;
398
            }
399
            $table = $action->getTable();
400
            $name = $table->getName();
401
402
            if ($action instanceof RemoveColumn) {
403
                if (!isset($this->columnRemoves[$name])) {
404
                    $this->columnRemoves[$name] = new AlterTable($table);
405
                }
406
                $this->columnRemoves[$name]->addAction($action);
407
            } else {
408
                if (!isset($this->tableUpdates[$name])) {
409
                    $this->tableUpdates[$name] = new AlterTable($table);
410
                }
411
                $this->tableUpdates[$name]->addAction($action);
412
            }
413
        }
414
    }
415
416
    /**
417
     * Collects all alter table drop and renames from the given intent
418
     *
419
     * @param \Phinx\Db\Action\Action[] $actions The actions to parse
420
     *
421
     * @return void
422
     */
423
    protected function gatherTableMoves($actions)
424
    {
425
        foreach ($actions as $action) {
426
            if (!($action instanceof DropTable || $action instanceof RenameTable || $action instanceof ChangePrimaryKey || $action instanceof ChangeComment)) {
427
                continue;
428
            }
429
            $table = $action->getTable();
430
            $name = $table->getName();
431
432
            if (!isset($this->tableMoves[$name])) {
433
                $this->tableMoves[$name] = new AlterTable($table);
434
            }
435
436
            $this->tableMoves[$name]->addAction($action);
437
        }
438
    }
439
440
    /**
441
     * Collects all index creation and drops from the given intent
442
     *
443
     * @param \Phinx\Db\Action\Action[] $actions The actions to parse
444
     *
445
     * @return void
446
     */
447
    protected function gatherIndexes($actions)
448
    {
449
        foreach ($actions as $action) {
450
            if (!($action instanceof AddIndex || $action instanceof DropIndex) || isset($this->tableCreates[$action->getTable()->getName()])) {
451
                continue;
452
            }
453
454
            $table = $action->getTable();
455
            $name = $table->getName();
456
457
            if (!isset($this->indexes[$name])) {
458
                $this->indexes[$name] = new AlterTable($table);
459
            }
460
461
            $this->indexes[$name]->addAction($action);
462
        }
463
    }
464
465
    /**
466
     * Collects all foreign key creation and drops from the given intent
467
     *
468
     * @param \Phinx\Db\Action\Action[] $actions The actions to parse
469
     *
470
     * @return void
471
     */
472
    protected function gatherConstraints($actions)
473
    {
474
        foreach ($actions as $action) {
475
            if (!($action instanceof AddForeignKey || $action instanceof DropForeignKey)) {
476
                continue;
477
            }
478
            $table = $action->getTable();
479
            $name = $table->getName();
480
481
            if (!isset($this->constraints[$name])) {
482
                $this->constraints[$name] = new AlterTable($table);
483
            }
484
485
            $this->constraints[$name]->addAction($action);
486
        }
487
    }
488
}
489