Completed
Pull Request — master (#1523)
by José
02:50
created

Plan::remapContraintAndIndexConflicts()   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 27
rs 9.488
c 0
b 0
f 0
cc 3
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
        // Dropping indexes used by foreign keys is a conflict, but one we can resolve
189
        // if the foreign key is also scheduled to be dropped. If we can find sucha a case,
190
        // we force the execution of the index drop after the foreign key is dropped.
191
        $this->constraints = collection($this->constraints)
192
             ->map(function ($alter) {
193
                 return $this->remapContraintAndIndexConflicts($alter);
194
             })
195
            ->toArray();
196
    }
197
198
    /**
199
     * Deletes all actions related to the given table and keeps the
200
     * rest
201
     *
202
     * @param Table $table The table to find in the list of actions
203
     * @param AlterTable[] $actions The actions to transform
204
     * @return AlterTable[] The list of actions without actions for the given table
205
     */
206
    protected function forgetTable(Table $table, $actions)
207
    {
208
        $result = [];
209
        foreach ($actions as $action) {
210
            if ($action->getTable()->getName() === $table->getName()) {
211
                continue;
212
            }
213
            $result[] = $action;
214
        }
215
216
        return $result;
217
    }
218
219
    /**
220
     * Finds all DropForeignKey actions in an AlterTable and moves
221
     * all conflicting DropIndex action in `$this->indexes` into the
222
     * given AlterTable.
223
     *
224
     * @param AlterTable The collection of actions to inspect
225
     * @return AlterTable The updated AlterTable object. This function
226
     * has the side effect of changing the `$this->indexes` property.
227
     */
228
    protected function remapContraintAndIndexConflicts(AlterTable $alter)
229
    {
230
        $newAlter = new AlterTable($alter->getTable());
231
        collection($alter->getActions())
232
            ->unfold(function ($action) {
233
                if (!$action instanceof DropForeignKey) {
234
                    return [$action];
235
                }
236
237
                list($this->indexes, $dropIndexActions) = $this->forgetDropIndex(
238
                    $action->getTable(),
239
                    $action->getForeignKey()->getColumns(),
240
                    $this->indexes
241
                );
242
243
                if (!empty($dropIndexActions)) {
244
                    return array_merge([$action], $dropIndexActions);
245
                }
246
247
                return [$action];
248
            })
249
            ->each(function ($action) use ($newAlter) {
250
                $newAlter->addAction($action);
251
            });
252
253
        return $newAlter;
254
    }
255
256
    /**
257
     * Deletes any DropIndex actions for the given table and exact columns
258
     *
259
     * @param Table $table The table to find in the list of actions
260
     * @param string[] $columns The column names to match
261
     * @param AlterTable[] $actions The actions to transform
262
     * @return array A tuple containing the list of actions without actions for dropping the index
263
     * and a list of drop index actions that were removed.
264
     */
265
    protected function forgetDropIndex(Table $table, array $columns, array $actions)
266
    {
267
        $dropIndexActions = new ArrayObject();
268
        $indexes = collection($actions)
269
            ->map(function ($alter) use ($table, $columns, $dropIndexActions) {
270
                if ($alter->getTable()->getName() !== $table->getName()) {
271
                    return $alter;
272
                }
273
274
                $newAlter = new AlterTable($table);
275
                collection($alter->getActions())
276
                    ->map(function ($action) use ($columns) {
277
                        if (!$action instanceof DropIndex) {
278
                            return [$action, null];
279
                        }
280
                        if ($action->getIndex()->getColumns() === $columns) {
281
                            return [null, $action];
282
                        }
283
284
                        return [$action, null];
285
                    })
286
                    ->each(function ($tuple) use ($newAlter, $dropIndexActions) {
287
                        list($action, $dropIndex) = $tuple;
288
                        if ($action) {
289
                            $newAlter->addAction($action);
290
                        }
291
                        if ($dropIndex) {
292
                            $dropIndexActions->append($dropIndex);
293
                        }
294
                    });
295
296
                return $newAlter;
297
            })
298
            ->toArray();
299
300
        return [$indexes, $dropIndexActions->getArrayCopy()];
301
    }
302
303
    /**
304
     * Collects all table creation actions from the given intent
305
     *
306
     * @param \Phinx\Db\Action\Action[] $actions The actions to parse
307
     * @return void
308
     */
309
    protected function gatherCreates($actions)
310
    {
311
        collection($actions)
312
            ->filter(function ($action) {
313
                return $action instanceof CreateTable;
314
            })
315
            ->map(function ($action) {
316
                return [$action->getTable()->getName(), new NewTable($action->getTable())];
317
            })
318
            ->each(function ($step) {
319
                $this->tableCreates[$step[0]] = $step[1];
320
            });
321
322
        collection($actions)
323
            ->filter(function ($action) {
324
                return $action instanceof AddColumn
325
                    || $action instanceof AddIndex;
326
            })
327
            ->filter(function ($action) {
328
                return isset($this->tableCreates[$action->getTable()->getName()]);
329
            })
330
            ->each(function ($action) {
331
                $table = $action->getTable();
332
333
                if ($action instanceof AddColumn) {
334
                    $this->tableCreates[$table->getName()]->addColumn($action->getColumn());
335
                }
336
337
                if ($action instanceof AddIndex) {
338
                    $this->tableCreates[$table->getName()]->addIndex($action->getIndex());
339
                }
340
            });
341
    }
342
343
    /**
344
     * Collects all alter table actions from the given intent
345
     *
346
     * @param \Phinx\Db\Action\Action[] $actions The actions to parse
347
     * @return void
348
     */
349
    protected function gatherUpdates($actions)
350
    {
351
        collection($actions)
352
            ->filter(function ($action) {
353
                return $action instanceof AddColumn
354
                    || $action instanceof ChangeColumn
355
                    || $action instanceof RemoveColumn
356
                    || $action instanceof RenameColumn;
357
            })
358
            // We are only concerned with table changes
359
            ->reject(function ($action) {
360
                return isset($this->tableCreates[$action->getTable()->getName()]);
361
            })
362 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...
363
                $table = $action->getTable();
364
                $name = $table->getName();
365
366
                if (!isset($this->tableUpdates[$name])) {
367
                    $this->tableUpdates[$name] = new AlterTable($table);
368
                }
369
370
                $this->tableUpdates[$name]->addAction($action);
371
            });
372
    }
373
374
    /**
375
     * Collects all alter table drop and renames from the given intent
376
     *
377
     * @param \Phinx\Db\Action\Action[] $actions The actions to parse
378
     * @return void
379
     */
380
    protected function gatherTableMoves($actions)
381
    {
382
        collection($actions)
383
            ->filter(function ($action) {
384
                return $action instanceof DropTable
385
                    || $action instanceof RenameTable
386
                    || $action instanceof ChangePrimaryKey
387
                    || $action instanceof ChangeComment;
388
            })
389 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...
390
                $table = $action->getTable();
391
                $name = $table->getName();
392
393
                if (!isset($this->tableMoves[$name])) {
394
                    $this->tableMoves[$name] = new AlterTable($table);
395
                }
396
397
                $this->tableMoves[$name]->addAction($action);
398
            });
399
    }
400
401
    /**
402
     * Collects all index creation and drops from the given intent
403
     *
404
     * @param \Phinx\Db\Action\Action[] $actions The actions to parse
405
     * @return void
406
     */
407
    protected function gatherIndexes($actions)
408
    {
409
        collection($actions)
410
            ->filter(function ($action) {
411
                return $action instanceof AddIndex
412
                    || $action instanceof DropIndex;
413
            })
414
            ->reject(function ($action) {
415
                // Indexes for new tables are created inline
416
                // so we don't wan't them here too
417
                return isset($this->tableCreates[$action->getTable()->getName()]);
418
            })
419 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...
420
                $table = $action->getTable();
421
                $name = $table->getName();
422
423
                if (!isset($this->indexes[$name])) {
424
                    $this->indexes[$name] = new AlterTable($table);
425
                }
426
427
                $this->indexes[$name]->addAction($action);
428
            });
429
    }
430
431
    /**
432
     * Collects all foreign key creation and drops from the given intent
433
     *
434
     * @param \Phinx\Db\Action\Action[] $actions The actions to parse
435
     * @return void
436
     */
437
    protected function gatherConstraints($actions)
438
    {
439
        collection($actions)
440
            ->filter(function ($action) {
441
                return $action instanceof AddForeignKey
442
                    || $action instanceof DropForeignKey;
443
            })
444 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...
445
                $table = $action->getTable();
446
                $name = $table->getName();
447
448
                if (!isset($this->constraints[$name])) {
449
                    $this->constraints[$name] = new AlterTable($table);
450
                }
451
452
                $this->constraints[$name]->addAction($action);
453
            });
454
    }
455
}
456