Completed
Pull Request — master (#1523)
by José
13:46 queued 17s
created

Plan::resolveConflicts()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 40
rs 9.28
c 0
b 0
f 0
cc 3
nc 3
nop 0
1
<?php
2
/**
3
 * Phinx
4
 *
5
 * (The MIT license)
6
 *
7
 * Permission is hereby granted, free of charge, to any person obtaining a copy
8
 * of this software and associated * documentation files (the "Software"), to
9
 * deal in the Software without restriction, including without limitation the
10
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
11
 * sell copies of the Software, and to permit persons to whom the Software is
12
 * furnished to do so, subject to the following conditions:
13
 *
14
 * The above copyright notice and this permission notice shall be included in
15
 * all copies or substantial portions of the Software.
16
 *
17
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
23
 * IN THE SOFTWARE.
24
 */
25
namespace Phinx\Db\Plan;
26
27
use ArrayObject;
28
use Phinx\Db\Action\AddColumn;
29
use Phinx\Db\Action\AddForeignKey;
30
use Phinx\Db\Action\AddIndex;
31
use Phinx\Db\Action\ChangeColumn;
32
use Phinx\Db\Action\ChangeComment;
33
use Phinx\Db\Action\ChangePrimaryKey;
34
use Phinx\Db\Action\CreateTable;
35
use Phinx\Db\Action\DropForeignKey;
36
use Phinx\Db\Action\DropIndex;
37
use Phinx\Db\Action\DropTable;
38
use Phinx\Db\Action\RemoveColumn;
39
use Phinx\Db\Action\RenameColumn;
40
use Phinx\Db\Action\RenameTable;
41
use Phinx\Db\Adapter\AdapterInterface;
42
use Phinx\Db\Plan\Solver\ActionSplitter;
43
use Phinx\Db\Table\Table;
44
45
/**
46
 * A Plan takes an Intent and transforms int into a sequence of
47
 * instructions that can be correctly executed by an AdapterInterface.
48
 *
49
 * The main focus of Plan is to arrange the actions in the most efficient
50
 * way possible for the database.
51
 */
52
class Plan
53
{
54
55
    /**
56
     * List of tables to be created
57
     *
58
     * @var \Phinx\Db\Plan\NewTable[]
59
     */
60
    protected $tableCreates = [];
61
62
    /**
63
     * List of table updates
64
     *
65
     * @var \Phinx\Db\Plan\AlterTable[]
66
     */
67
    protected $tableUpdates = [];
68
69
    /**
70
     * List of table removals or renames
71
     *
72
     * @var \Phinx\Db\Plan\AlterTable[]
73
     */
74
    protected $tableMoves = [];
75
76
    /**
77
     * List of index additions or removals
78
     *
79
     * @var \Phinx\Db\Plan\AlterTable[]
80
     */
81
    protected $indexes = [];
82
83
    /**
84
     * List of constraint additions or removals
85
     *
86
     * @var \Phinx\Db\Plan\AlterTable[]
87
     */
88
    protected $constraints = [];
89
90
    /**
91
     * Constructor
92
     *
93
     * @param Intent $intent All the actions that should be executed
94
     */
95
    public function __construct(Intent $intent)
96
    {
97
        $this->createPlan($intent->getActions());
0 ignored issues
show
Documentation introduced by
$intent->getActions() is of type array<integer,object<Phinx\Db\Action\Action>>, but the function expects a object<Phinx\Db\Plan\Intent>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
98
    }
99
100
    /**
101
     * Parses the given Intent and creates the separate steps to execute
102
     *
103
     * @param Intent $actions The actions to use for the plan
104
     * @return void
105
     */
106
    protected function createPlan($actions)
107
    {
108
        $this->gatherCreates($actions);
0 ignored issues
show
Documentation introduced by
$actions is of type object<Phinx\Db\Plan\Intent>, but the function expects a array<integer,object<Phinx\Db\Action\Action>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
109
        $this->gatherUpdates($actions);
0 ignored issues
show
Documentation introduced by
$actions is of type object<Phinx\Db\Plan\Intent>, but the function expects a array<integer,object<Phinx\Db\Action\Action>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
110
        $this->gatherTableMoves($actions);
0 ignored issues
show
Documentation introduced by
$actions is of type object<Phinx\Db\Plan\Intent>, but the function expects a array<integer,object<Phinx\Db\Action\Action>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
111
        $this->gatherIndexes($actions);
0 ignored issues
show
Documentation introduced by
$actions is of type object<Phinx\Db\Plan\Intent>, but the function expects a array<integer,object<Phinx\Db\Action\Action>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
112
        $this->gatherConstraints($actions);
0 ignored issues
show
Documentation introduced by
$actions is of type object<Phinx\Db\Plan\Intent>, but the function expects a array<integer,object<Phinx\Db\Action\Action>>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

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