Completed
Pull Request — master (#1523)
by
unknown
03:21 queued 01:33
created

Plan::remapDropFkConflicts()   B

Complexity

Conditions 7
Paths 1

Size

Total Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 45
rs 8.2666
c 0
b 0
f 0
cc 7
nc 1
nop 1
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\Table\Table;
43
44
/**
45
 * A Plan takes an Intent and transforms int into a sequence of
46
 * instructions that can be correctly executed by an AdapterInterface.
47
 *
48
 * The main focus of Plan is to arrange the actions in the most efficient
49
 * way possible for the database.
50
 */
51
class Plan
52
{
53
54
    /**
55
     * List of tables to be created
56
     *
57
     * @var \Phinx\Db\Plan\NewTable[]
58
     */
59
    protected $tableCreates = [];
60
61
    /**
62
     * List of table updates
63
     *
64
     * @var \Phinx\Db\Plan\AlterTable[]
65
     */
66
    protected $tableUpdates = [];
67
68
    /**
69
     * List of table removals or renames
70
     *
71
     * @var \Phinx\Db\Plan\AlterTable[]
72
     */
73
    protected $tableMoves = [];
74
75
    /**
76
     * List of index additions or removals
77
     *
78
     * @var \Phinx\Db\Plan\AlterTable[]
79
     */
80
    protected $indexes = [];
81
82
    /**
83
     * List of constraint additions or removals
84
     *
85
     * @var \Phinx\Db\Plan\AlterTable[]
86
     */
87
    protected $constraints = [];
88
89
    /**
90
     * Constructor
91
     *
92
     * @param Intent $intent All the actions that should be executed
93
     */
94
    public function __construct(Intent $intent)
95
    {
96
        $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...
97
    }
98
99
    /**
100
     * Parses the given Intent and creates the separate steps to execute
101
     *
102
     * @param Intent $actions The actions to use for the plan
103
     * @return void
104
     */
105
    protected function createPlan($actions)
106
    {
107
        $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...
108
        $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...
109
        $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...
110
        $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...
111
        $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...
112
        $this->resolveConflicts();
113
    }
114
115
    /**
116
     * Returns a nested list of all the steps to execute
117
     *
118
     * @return AlterTable[][]
119
     */
120
    protected function updatesSequence()
121
    {
122
        return [
123
            $this->tableUpdates,
124
            $this->constraints,
125
            $this->indexes,
126
            $this->tableMoves,
127
        ];
128
    }
129
130
    /**
131
     * Executes this plan using the given AdapterInterface
132
     *
133
     * @param AdapterInterface $executor The executor object for the plan
134
     * @return void
135
     */
136 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...
137
    {
138
        foreach ($this->tableCreates as $newTable) {
139
            $executor->createTable($newTable->getTable(), $newTable->getColumns(), $newTable->getIndexes());
140
        }
141
142
        collection($this->updatesSequence())
143
            ->unfold()
144
            ->each(function ($updates) use ($executor) {
145
                $executor->executeActions($updates->getTable(), $updates->getActions());
146
            });
147
    }
148
149
    /**
150
     * Executes the inverse plan (rollback the actions) with the given AdapterInterface:w
151
     *
152
     * @param AdapterInterface $executor The executor object for the plan
153
     * @return void
154
     */
155 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...
156
    {
157
        collection(array_reverse($this->updatesSequence()))
158
            ->unfold()
159
            ->each(function ($updates) use ($executor) {
160
                $executor->executeActions($updates->getTable(), $updates->getActions());
161
            });
162
163
        foreach ($this->tableCreates as $newTable) {
164
            $executor->createTable($newTable->getTable(), $newTable->getColumns(), $newTable->getIndexes());
165
        }
166
    }
167
168
    /**
169
     * Deletes certain actions from the plan if they are found to be conflicting or redundant.
170
     *
171
     * @return void
172
     */
173
    protected function resolveConflicts()
174
    {
175
        $moveActions = collection($this->tableMoves)
176
            ->unfold(function ($move) {
177
                return $move->getActions();
178
            });
179
180
        foreach ($moveActions as $action) {
181
            if ($action instanceof DropTable) {
182
                $this->tableUpdates = $this->forgetTable($action->getTable(), $this->tableUpdates);
183
                $this->constraints = $this->forgetTable($action->getTable(), $this->constraints);
184
                $this->indexes = $this->forgetTable($action->getTable(), $this->indexes);
185
            }
186
        }
187
188
        $this->constraints = collection($this->constraints)
189
            ->map(function (AlterTable $alter) {
190
                // Dropping indexes used by foreign keys is a conflict, but one we can resolve
191
                // if the foreign key is also scheduled to be dropped. If we can find sucha a case,
192
                // we force the execution of the index drop after the foreign key is dropped.
193
                return $this->remapContraintAndIndexConflicts($alter);
194
            })
195
            ->unfold(function (AlterTable $alter) {
196
                // Changing constraint or column properties sometimes require dropping it and then
197
                // creating it again with the new stuff. Unfortunately, we have already bundled
198
                // everything together in as few AlterTable statements as we could, so we need to
199
                // resolve this conflict manually
200
                return $this->remapDropFkConflicts($alter);
201
            })
202
            ->toList();
203
    }
204
205
    /**
206
     * Deletes all actions related to the given table and keeps the
207
     * rest
208
     *
209
     * @param Table $table The table to find in the list of actions
210
     * @param AlterTable[] $actions The actions to transform
211
     * @return AlterTable[] The list of actions without actions for the given table
212
     */
213
    protected function forgetTable(Table $table, $actions)
214
    {
215
        $result = [];
216
        foreach ($actions as $action) {
217
            if ($action->getTable()->getName() === $table->getName()) {
218
                continue;
219
            }
220
            $result[] = $action;
221
        }
222
223
        return $result;
224
    }
225
226
    /**
227
     * Finds all DropForeignKey actions in an AlterTable and moves
228
     * all conflicting DropIndex action in `$this->indexes` into the
229
     * given AlterTable.
230
     *
231
     * @param AlterTable $alter The collection of actions to inspect
232
     * @return AlterTable The updated AlterTable object. This function
233
     * has the side effect of changing the `$this->indexes` property.
234
     */
235
    protected function remapContraintAndIndexConflicts(AlterTable $alter)
236
    {
237
        $newAlter = new AlterTable($alter->getTable());
238
        collection($alter->getActions())
239
            ->unfold(function ($action) {
240
                if (!$action instanceof DropForeignKey) {
241
                    return [$action];
242
                }
243
244
                list($this->indexes, $dropIndexActions) = $this->forgetDropIndex(
245
                    $action->getTable(),
246
                    $action->getForeignKey()->getColumns(),
247
                    $this->indexes
248
                );
249
250
                if (!empty($dropIndexActions)) {
251
                    return array_merge([$action], $dropIndexActions);
252
                }
253
254
                return [$action];
255
            })
256
            ->each(function ($action) use ($newAlter) {
257
                $newAlter->addAction($action);
258
            });
259
260
        return $newAlter;
261
    }
262
263
    /**
264
     * Deletes any DropIndex actions for the given table and exact columns
265
     *
266
     * @param Table $table The table to find in the list of actions
267
     * @param string[] $columns The column names to match
268
     * @param AlterTable[] $actions The actions to transform
269
     * @return array A tuple containing the list of actions without actions for dropping the index
270
     * and a list of drop index actions that were removed.
271
     */
272
    protected function forgetDropIndex(Table $table, array $columns, array $actions)
273
    {
274
        $dropIndexActions = new ArrayObject();
275
        $indexes = collection($actions)
276
            ->map(function ($alter) use ($table, $columns, $dropIndexActions) {
277
                if ($alter->getTable()->getName() !== $table->getName()) {
278
                    return $alter;
279
                }
280
281
                $newAlter = new AlterTable($table);
282
                collection($alter->getActions())
283
                    ->map(function ($action) use ($columns) {
284
                        if (!$action instanceof DropIndex) {
285
                            return [$action, null];
286
                        }
287
                        if ($action->getIndex()->getColumns() === $columns) {
288
                            return [null, $action];
289
                        }
290
291
                        return [$action, null];
292
                    })
293
                    ->each(function ($tuple) use ($newAlter, $dropIndexActions) {
294
                        list($action, $dropIndex) = $tuple;
295
                        if ($action) {
296
                            $newAlter->addAction($action);
297
                        }
298
                        if ($dropIndex) {
299
                            $dropIndexActions->append($dropIndex);
300
                        }
301
                    });
302
303
                return $newAlter;
304
            })
305
            ->toArray();
306
307
        return [$indexes, $dropIndexActions->getArrayCopy()];
308
    }
309
310
    /**
311
     * Finds conflixting DropForeignKey/AddForeignKey and returns a list
312
     * of non conflicting AlterTable instructions.
313
     *
314
     * @param AlterTable $alter The collection of actions to inspect
315
     * @return AlterTable[] A list of AlterTable that can be executed without
316
     * this type of conflict
317
     */
318
    protected function remapDropFkConflicts(AlterTable $alter)
319
    {
320
        $actions = collection($alter->getActions());
321
        $dropActions = $actions
322
            ->filter(function ($action) {
323
                return $action instanceof DropForeignKey;
324
            })
325
            ->toList();
326
327
        $originalAlter = new AlterTable($alter->getTable());
328
        $newAlter = new AlterTable($alter->getTable());
329
330
        $actions
331
            ->map(function ($action) use ($dropActions) {
332
                if (!$action instanceof AddForeignKey) {
333
                    return [$action, null];
334
                }
335
336
                $columns = $action->getForeignKey()->getColumns();
337
                $found = false;
338
                foreach ($dropActions as $da) {
339
                    if ($da->getForeignKey()->getColumns() === $columns) {
340
                        $found = true;
341
                        break;
342
                    }
343
                }
344
345
                if ($found) {
346
                    return [null, $action];
347
                }
348
349
                return [$action, null];
350
            })
351
            ->each(function ($pair) use ($originalAlter, $newAlter) {
352
                list($original, $new) = $pair;
353
                if ($original) {
354
                    $originalAlter->addAction($original);
355
                }
356
                if ($new) {
357
                    $newAlter->addAction($new);
358
                }
359
            });
360
361
        return [$originalAlter, $newAlter];
362
    }
363
364
    /**
365
     * Collects all table creation actions from the given intent
366
     *
367
     * @param \Phinx\Db\Action\Action[] $actions The actions to parse
368
     * @return void
369
     */
370
    protected function gatherCreates($actions)
371
    {
372
        collection($actions)
373
            ->filter(function ($action) {
374
                return $action instanceof CreateTable;
375
            })
376
            ->map(function ($action) {
377
                return [$action->getTable()->getName(), new NewTable($action->getTable())];
378
            })
379
            ->each(function ($step) {
380
                $this->tableCreates[$step[0]] = $step[1];
381
            });
382
383
        collection($actions)
384
            ->filter(function ($action) {
385
                return $action instanceof AddColumn
386
                    || $action instanceof AddIndex;
387
            })
388
            ->filter(function ($action) {
389
                return isset($this->tableCreates[$action->getTable()->getName()]);
390
            })
391
            ->each(function ($action) {
392
                $table = $action->getTable();
393
394
                if ($action instanceof AddColumn) {
395
                    $this->tableCreates[$table->getName()]->addColumn($action->getColumn());
396
                }
397
398
                if ($action instanceof AddIndex) {
399
                    $this->tableCreates[$table->getName()]->addIndex($action->getIndex());
400
                }
401
            });
402
    }
403
404
    /**
405
     * Collects all alter table actions from the given intent
406
     *
407
     * @param \Phinx\Db\Action\Action[] $actions The actions to parse
408
     * @return void
409
     */
410
    protected function gatherUpdates($actions)
411
    {
412
        collection($actions)
413
            ->filter(function ($action) {
414
                return $action instanceof AddColumn
415
                    || $action instanceof ChangeColumn
416
                    || $action instanceof RemoveColumn
417
                    || $action instanceof RenameColumn;
418
            })
419
            // We are only concerned with table changes
420
            ->reject(function ($action) {
421
                return isset($this->tableCreates[$action->getTable()->getName()]);
422
            })
423 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...
424
                $table = $action->getTable();
425
                $name = $table->getName();
426
427
                if (!isset($this->tableUpdates[$name])) {
428
                    $this->tableUpdates[$name] = new AlterTable($table);
429
                }
430
431
                $this->tableUpdates[$name]->addAction($action);
432
            });
433
    }
434
435
    /**
436
     * Collects all alter table drop and renames from the given intent
437
     *
438
     * @param \Phinx\Db\Action\Action[] $actions The actions to parse
439
     * @return void
440
     */
441
    protected function gatherTableMoves($actions)
442
    {
443
        collection($actions)
444
            ->filter(function ($action) {
445
                return $action instanceof DropTable
446
                    || $action instanceof RenameTable
447
                    || $action instanceof ChangePrimaryKey
448
                    || $action instanceof ChangeComment;
449
            })
450 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...
451
                $table = $action->getTable();
452
                $name = $table->getName();
453
454
                if (!isset($this->tableMoves[$name])) {
455
                    $this->tableMoves[$name] = new AlterTable($table);
456
                }
457
458
                $this->tableMoves[$name]->addAction($action);
459
            });
460
    }
461
462
    /**
463
     * Collects all index creation and drops from the given intent
464
     *
465
     * @param \Phinx\Db\Action\Action[] $actions The actions to parse
466
     * @return void
467
     */
468
    protected function gatherIndexes($actions)
469
    {
470
        collection($actions)
471
            ->filter(function ($action) {
472
                return $action instanceof AddIndex
473
                    || $action instanceof DropIndex;
474
            })
475
            ->reject(function ($action) {
476
                // Indexes for new tables are created inline
477
                // so we don't wan't them here too
478
                return isset($this->tableCreates[$action->getTable()->getName()]);
479
            })
480 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...
481
                $table = $action->getTable();
482
                $name = $table->getName();
483
484
                if (!isset($this->indexes[$name])) {
485
                    $this->indexes[$name] = new AlterTable($table);
486
                }
487
488
                $this->indexes[$name]->addAction($action);
489
            });
490
    }
491
492
    /**
493
     * Collects all foreign key creation and drops from the given intent
494
     *
495
     * @param \Phinx\Db\Action\Action[] $actions The actions to parse
496
     * @return void
497
     */
498
    protected function gatherConstraints($actions)
499
    {
500
        collection($actions)
501
            ->filter(function ($action) {
502
                return $action instanceof AddForeignKey
503
                    || $action instanceof DropForeignKey;
504
            })
505 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...
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