Test Failed
Branch master (1fbe3c)
by Aleksandr
02:14
created

MigrationTrait::getForeignKey()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 8
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 2
1
<?php
2
3
4
namespace carono\yii2migrate\traits;
5
6
use carono\yii2migrate\ForeignKeyColumn;
7
use carono\yii2migrate\helpers\SchemaHelper;
8
use carono\yii2migrate\IndexColumn;
9
use carono\yii2migrate\PivotColumn;
10
use yii\db\ColumnSchema;
11
use yii\db\ColumnSchemaBuilder;
12
use yii\db\Migration;
13
use yii\db\Schema;
14
use yii\helpers\ArrayHelper;
15
16
/**
17
 * Trait MigrationTrait
18
 *
19
 * @package carono\yii2migrate\traits
20
 * @mixin Migration
21
 */
22
trait MigrationTrait
23
{
24
    private static $tableOptions = '@tableOptions';
25
26
    /**
27
     * @param      $refTable
28
     * @param null $refColumn
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $refColumn is correct as it would always require null to be passed?
Loading history...
29
     *
30
     * @param string $type
31
     * @param null $length
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $length is correct as it would always require null to be passed?
Loading history...
32
     * @return ForeignKeyColumn
33
     */
34
    public function foreignKey($refTable = null, $refColumn = null, $type = Schema::TYPE_INTEGER, $length = null)
35
    {
36
        return (new ForeignKeyColumn($type, $length))->refTable($refTable)->refColumn($refColumn)->setMigrate($this);
37
    }
38
39
    /**
40
     * @param array $columns
41
     * @param bool $isUnique
42
     * @return IndexColumn
43
     */
44
    public function index($columns = [], $isUnique = false)
45
    {
46
        return (new IndexColumn())->setMigrate($this)->columns($columns)->unique($isUnique);
47
    }
48
49
    /**
50
     * @param null $refTable
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $refTable is correct as it would always require null to be passed?
Loading history...
51
     * @param null $refColumn
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $refColumn is correct as it would always require null to be passed?
Loading history...
52
     *
53
     * @return PivotColumn
54
     */
55
    public function pivot($refTable = null, $refColumn = null)
56
    {
57
        return (new PivotColumn())->refTable($refTable)->refColumn($refColumn)->setMigrate($this);
58
    }
59
60
    /**
61
     * @return array
62
     */
63
    public function tableOptions()
64
    {
65
        return [];
66
    }
67
68
    /**
69
     * @param string $name
70
     * @param string $table
71
     * @param array|string $columns
72
     * @param bool $unique
73
     */
74
    public function createIndex($name, $table, $columns, $unique = false)
75
    {
76
        $suffix = $unique ? '_unq' : '_idx';
77
        if ($name === null) {
0 ignored issues
show
introduced by
The condition $name === null is always false.
Loading history...
78
            $name = self::formIndexName($table, $columns, $suffix, $this->db->tablePrefix);
79
            $name = $this->expandTablePrefix($name);
80
        }
81
        $name = SchemaHelper::truncateIndexName($name, 64, $suffix);
82
        parent::createIndex($name, $table, $columns, $unique);
83
    }
84
85
    /**
86
     * @param ColumnSchema $column
87
     * @return $this|ColumnSchemaBuilder
88
     * @throws \Exception
89
     */
90
    public function columnSchemaToBuilder(ColumnSchema $column)
91
    {
92
        $size = $column->size;
93
        $precision = $column->precision;
94
        $default = $column->defaultValue;
95
        $scale = $column->scale;
96
        if ($column->isPrimaryKey && $column->autoIncrement) {
97
            return $this->primaryKey();
0 ignored issues
show
Bug introduced by
It seems like primaryKey() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

97
            return $this->/** @scrutinizer ignore-call */ primaryKey();
Loading history...
98
        }
99
        switch ($column->type) {
100
            case 'string':
101
                $builder = $this->string($size);
0 ignored issues
show
Bug introduced by
It seems like string() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

101
                /** @scrutinizer ignore-call */ 
102
                $builder = $this->string($size);
Loading history...
102
                break;
103
            case 'integer':
104
                $builder = $this->integer($size);
0 ignored issues
show
Bug introduced by
It seems like integer() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

104
                /** @scrutinizer ignore-call */ 
105
                $builder = $this->integer($size);
Loading history...
105
                break;
106
            case 'datetime':
107
                $builder = $this->dateTime($precision);
0 ignored issues
show
Bug introduced by
It seems like dateTime() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

107
                /** @scrutinizer ignore-call */ 
108
                $builder = $this->dateTime($precision);
Loading history...
108
                break;
109
            case 'text':
110
                $builder = $this->text();
0 ignored issues
show
Bug introduced by
It seems like text() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

110
                /** @scrutinizer ignore-call */ 
111
                $builder = $this->text();
Loading history...
111
                break;
112
            case 'smallint':
113
                if ($size === 1) {
114
                    $default = (boolean)$default;
115
                    $builder = $this->boolean();
0 ignored issues
show
Bug introduced by
It seems like boolean() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

115
                    /** @scrutinizer ignore-call */ 
116
                    $builder = $this->boolean();
Loading history...
116
                } else {
117
                    $builder = $this->smallInteger($size);
0 ignored issues
show
Bug introduced by
It seems like smallInteger() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

117
                    /** @scrutinizer ignore-call */ 
118
                    $builder = $this->smallInteger($size);
Loading history...
118
                }
119
                break;
120
            case 'binary':
121
                $builder = $this->binary()->defaultValue($default);
0 ignored issues
show
Bug introduced by
It seems like binary() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

121
                $builder = $this->/** @scrutinizer ignore-call */ binary()->defaultValue($default);
Loading history...
122
                break;
123
            case 'decimal':
124
                $builder = $this->decimal($precision, $scale);
0 ignored issues
show
Bug introduced by
It seems like decimal() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

124
                /** @scrutinizer ignore-call */ 
125
                $builder = $this->decimal($precision, $scale);
Loading history...
125
                break;
126
            case 'double':
127
                $builder = $this->double($precision)->defaultValue($default);
0 ignored issues
show
Bug introduced by
It seems like double() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

127
                $builder = $this->/** @scrutinizer ignore-call */ double($precision)->defaultValue($default);
Loading history...
128
                break;
129
            default:
130
                throw new \Exception("Column ($column->name) type '$column->type' not recognized");
131
        }
132
        $builder->defaultValue($default);
133
        if (!$column->allowNull) {
134
            $builder->notNull();
135
        }
136
        $builder->comment($column->comment);
137
        return $builder;
138
    }
139
140
    /**
141
     * @param $name
142
     * @return mixed
143
     */
144
    public function expandTablePrefix($name)
145
    {
146
        return SchemaHelper::expandTablePrefix($name, $this->db->tablePrefix);
147
    }
148
149
    /**
150
     * @param string $table
151
     * @param array $columns
152
     * @param null $options
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $options is correct as it would always require null to be passed?
Loading history...
153
     * @throws \Exception
154
     */
155
    public function createTable($table, $columns, $options = null)
156
    {
157
        /**
158
         * @var PivotColumn[] $pvs
159
         * @var ForeignKeyColumn[] $fks
160
         */
161
        echo "    > create table $table ...";
162
        $time = microtime(true);
163
        $pvs = [];
164
        $fks = [];
165
        $pks = [];
166
167
        $options = $this->getTableOptionsFromArray(ArrayHelper::remove($columns, self::$tableOptions, []), $options);
168
169
        foreach ($columns as $column => &$type) {
170
171
            if ($type instanceof ColumnSchema) {
172
                $column = is_numeric($column) ? $type->name : $column;
173
                $type = $this->columnSchemaToBuilder($type);
174
            }
175
            if ((string)$type === (string)$this->primaryKey()) {
176
                $pks[] = $column;
177
            }
178
            if ($type instanceof ForeignKeyColumn) {
179
                $type->sourceTable($table)->sourceColumn($column);
180
                $fks[] = $type;
181
            }
182
183
            if ($type instanceof PivotColumn) {
184
                $type->setName($column)->sourceTable($table);
185
                $pvs[] = $type;
186
                unset($columns[$column]);
187
            }
188
        }
189
        if (count($pks) > 1) {
190
            foreach ($columns as $column => &$type) {
191
                $type = $this->integer();
192
            }
193
        }
194
        $this->db->createCommand()->createTable($table, $columns, $options)->execute();
195
        foreach ($columns as $column => $type) {
196
            if ($type instanceof ColumnSchemaBuilder && $type->comment !== null) {
197
                $this->db->createCommand()->addCommentOnColumn($table, $column, $type->comment)->execute();
198
            }
199
        }
200
        if ($fks) {
201
            echo "\n";
202
        }
203
        foreach ($fks as $fk) {
204
            echo '  ';
205
            $fk->apply();
206
        }
207
        if ($fks) {
208
            echo "\n";
209
        }
210
        if (count($pks) > 1) {
211
            echo '  ';
212
            $this->addPrimaryKey(null, $table, $pks);
213
        }
214
        if ($pvs) {
215
            echo "\n";
216
        }
217
        foreach ($pvs as $pv) {
218
            echo '  ';
219
            $pv->apply();
220
        }
221
        echo ' done (time: ' . sprintf('%.3f', microtime(true) - $time) . "s)\n";
222
    }
223
224
    /**
225
     * @param string $name
226
     * @param string $table
227
     * @param array|string $columns
228
     * @param string $refTable
229
     * @param array|string $refColumns
230
     * @param null $delete
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $delete is correct as it would always require null to be passed?
Loading history...
231
     * @param null $update
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $update is correct as it would always require null to be passed?
Loading history...
232
     */
233
    public function addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete = null, $update = null)
234
    {
235
        if ($name === null) {
0 ignored issues
show
introduced by
The condition $name === null is always false.
Loading history...
236
            $name = $this->formFkName($table, $columns, $refTable, $refColumns);
237
            $name = $this->expandTablePrefix($name);
238
        }
239
        $name = SchemaHelper::truncateIndexName($name, 64, '_fk');
240
        parent::addForeignKey($name, $table, $columns, $refTable, $refColumns, $delete, $update);
241
    }
242
243
    /**
244
     * @inheritdoc
245
     */
246
    public function alterColumn($table, $column, $type)
247
    {
248
        if ($type instanceof ForeignKeyColumn) {
249
            $type->sourceTable($table);
250
            $type->sourceColumn($column);
251
            $type->apply();
252
        } else {
253
            parent::alterColumn($table, $column, $type);
254
        }
255
    }
256
257
    /**
258
     * @inheritdoc
259
     */
260
    public function addColumn($table, $column, $type)
261
    {
262
        if ($type instanceof ForeignKeyColumn) {
263
            parent::addColumn($table, $column, $type);
264
            $type->sourceTable($table);
265
            $type->sourceColumn($column);
266
            $type->apply();
267
        } else {
268
            parent::addColumn($table, $column, $type);
269
        }
270
    }
271
272
    /**
273
     * @inheritdoc
274
     */
275
    public function addPrimaryKey($name, $table, $columns)
276
    {
277
        if ($name === null) {
278
            $name = self::formIndexName($table, $columns, '_pk', $this->db->tablePrefix);
279
            $name = $this->expandTablePrefix($name);
280
        }
281
        $name = SchemaHelper::truncateIndexName($name, 64, '_pk');
282
        parent::addPrimaryKey($name, $table, $columns);
283
    }
284
285
    /**
286
     * @return array
287
     */
288
    public function newColumns()
289
    {
290
        return [];
291
    }
292
293
    /**
294
     * @param array $array
295
     */
296
    public function downNewColumns($array = [])
297
    {
298
        $this->_applyNewColumns($array ?: $this->newColumns(), true);
299
    }
300
301
    /**
302
     * @param array $array
303
     */
304
    public function upNewColumns($array = [])
305
    {
306
        $this->_applyNewColumns($array ?: $this->newColumns(), false);
307
    }
308
309
    /**
310
     * @param array $columns
311
     * @param bool $revert
312
     */
313
    protected function _applyNewColumns($columns = [], $revert = false)
314
    {
315
        $columns = $revert ? array_reverse($columns) : $columns;
316
317
        $result = [];
318
        foreach ($columns as $key => $column) {
319
            if (is_numeric($key)) {
320
                $result[] = $column;
321
            } else {
322
                foreach ($column as $columnName => $value) {
323
                    $result[] = [$key, $columnName, $value];
324
                }
325
            }
326
        }
327
328
        foreach ($result as $column) {
329
            if ($column[2] instanceof PivotColumn) {
330
                $column[2]->setName($column[1])->sourceTable($column[0]);
331
            }
332
            if ($revert) {
333
                if ($column[2] instanceof PivotColumn) {
334
                    $column[2]->remove();
335
                    continue;
336
                }
337
                $this->dropColumn($column[0], $column[1]);
338
            } else {
339
                if ($column[2] instanceof PivotColumn) {
340
                    $column[2]->apply();
341
                    continue;
342
                }
343
                $this->addColumn($column[0], $column[1], $column[2]);
344
            }
345
        }
346
    }
347
348
    /**
349
     * @inheritdoc
350
     */
351
    public function dropColumn($table, $column)
352
    {
353
        $foreignKeys = SchemaHelper::findTableForeignKeys($this->db, $table);
354
        foreach ($foreignKeys as $key => $foreignKey) {
355
            if ($foreignKey->columnNames === [$column]) {
356
                $this->dropForeignKey($key, $table);
0 ignored issues
show
Bug introduced by
The method dropForeignKey() does not exist on carono\yii2migrate\traits\MigrationTrait. Did you maybe mean dropForeignKeyByColumn()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

356
                $this->/** @scrutinizer ignore-call */ 
357
                       dropForeignKey($key, $table);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
357
            }
358
        }
359
        return parent::dropColumn($table, $column);
360
    }
361
362
    /**
363
     * @return array
364
     */
365
    public function newTables()
366
    {
367
        return [];
368
    }
369
370
    /**
371
     * @param array $array
372
     * @param null $tableOptions
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $tableOptions is correct as it would always require null to be passed?
Loading history...
373
     */
374
    public function upNewTables($array = [], $tableOptions = null)
375
    {
376
        $this->_applyNewTables($array ?: $this->newTables(), false, $tableOptions);
377
    }
378
379
    /**
380
     * @param array $array
381
     */
382
    public function upNewIndex($array = [])
383
    {
384
        $this->_applyNewIndex($array ?: $this->newIndex());
385
    }
386
387
    /**
388
     * @param array $array
389
     */
390
    public function downNewIndex($array = [])
391
    {
392
        $this->_applyNewIndex($array ?: $this->newIndex(), true);
393
    }
394
395
    /**
396
     * @return array
397
     */
398
    public function newIndex()
399
    {
400
        return [];
401
    }
402
403
    /**
404
     * @param array $array
405
     */
406
    public function downNewTables($array = [])
407
    {
408
        $this->_applyNewTables($array ?: $this->newTables(), true);
409
    }
410
411
    /**
412
     * @param $indexes
413
     * @param bool $revert
414
     */
415
    protected function _applyNewIndex($indexes, $revert = false)
416
    {
417
        /**
418
         * @var IndexColumn $index
419
         */
420
        $indexes = $revert ? array_reverse($indexes) : $indexes;
421
        foreach ($indexes as $key => $data) {
422
            if (!is_numeric($key)) {
423
                foreach ($data as $index) {
424
                    if ($index instanceof IndexColumn) {
425
                        $index->table($key);
426
                        if ($revert) {
427
                            $index->remove();
428
                        } else {
429
                            $index->apply();
430
                        }
431
                    }
432
                }
433
                continue;
434
            }
435
436
            // Old style
437
            $unq = isset($data[2]) && $data[2];
438
            $columns = is_array($data[1]) ? $data[1] : explode(',', $data[1]);
439
            $index = $this->index($columns, $unq)->table($data[0]);
440
441
            if ($revert) {
442
                $index->remove();
443
            } else {
444
                $index->apply();
445
            }
446
        }
447
    }
448
449
    /**
450
     * @param array|string $items
451
     * @param string|array $default
452
     * @return array|mixed|string
453
     */
454
    private function getTableOptionsFromArray($items, $default = '')
455
    {
456
        if (is_array($default)) {
457
            $default = ArrayHelper::getValue($default, $this->db->driverName, '');
458
        }
459
460
        if (!$default) {
461
            $default = ArrayHelper::getValue($this->tableOptions(), $this->db->driverName, '');
462
        }
463
464
        if (is_array($items)) {
465
            return ArrayHelper::getValue($items, $this->db->driverName, $default);
466
        }
467
468
        if ($items && is_string($items)) {
469
            return $items;
470
        }
471
472
        return $default;
473
    }
474
475
    /**
476
     * @param $tables
477
     * @param bool $revert
478
     * @param null $tableOptions
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $tableOptions is correct as it would always require null to be passed?
Loading history...
479
     */
480
    protected function _applyNewTables($tables, $revert = false, $tableOptions = null)
481
    {
482
        $tables = $revert ? array_reverse($tables) : $tables;
483
        foreach ($tables as $table => $columns) {
484
            if ($revert) {
485
                foreach ($columns as $column => $type) {
486
                    if ($type instanceof PivotColumn) {
487
                        $type->setName($column)->sourceTable($table);
488
                        $type->remove();
489
                    }
490
                }
491
                $this->dropTable($table);
0 ignored issues
show
Bug introduced by
It seems like dropTable() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

491
                $this->/** @scrutinizer ignore-call */ 
492
                       dropTable($table);
Loading history...
492
            } else {
493
                $this->createTable($table, $columns, $tableOptions);
494
            }
495
        }
496
    }
497
498
    /**
499
     * @param $table
500
     * @param $columns
501
     * @param $refTable
502
     * @param $refColumns
503
     * @return string
504
     */
505
    public function formFkName($table, $columns, $refTable, $refColumns)
506
    {
507
        return $this->foreignKey($refTable, $refColumns)->sourceTable($table)->sourceColumn($columns)->formIndexName();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->foreignKey...lumns)->formIndexName() also could return the type string[] which is incompatible with the documented return type string.
Loading history...
508
    }
509
510
    /**
511
     * @param $table
512
     * @param $columns
513
     * @param string $suffix
514
     * @param string $tablePrefix
515
     * @return string
516
     */
517
    public static function formPkIndexName($table, $columns, $suffix = '_pk', $tablePrefix = '')
518
    {
519
        return self::formIndexName($table, $columns, $suffix, $tablePrefix);
520
    }
521
522
    /**
523
     * @param $table
524
     * @param $columns
525
     * @param string $suffix
526
     * @param string $tablePrefix
527
     * @return string
528
     */
529
    public static function formIndexName($table, $columns, $suffix = '_idx', $tablePrefix = '')
530
    {
531
        $table = SchemaHelper::expandTablePrefix($table, $tablePrefix);
532
        $table = SchemaHelper::removeSchema($table);
533
        $column = implode(':', array_map('trim', (array)$columns));
534
        return "{$table}:{$column}$suffix";
535
    }
536
537
    /**
538
     * @param $table
539
     * @param $column
540
     */
541
    public function dropIndexByColumn($table, $column)
542
    {
543
        /**
544
         * @var \yii\db\IndexConstraint $index
545
         */
546
        foreach (SchemaHelper::findNonUniqueIndexes($this->db, $this->expandTablePrefix($table)) as $index) {
547
            if ($index->columnNames === (array)$column) {
548
                $this->dropIndex($index->name, $table);
0 ignored issues
show
Bug introduced by
The method dropIndex() does not exist on carono\yii2migrate\traits\MigrationTrait. Did you maybe mean dropIndexByColumn()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

548
                $this->/** @scrutinizer ignore-call */ 
549
                       dropIndex($index->name, $table);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
549
            }
550
        }
551
    }
552
553
    /**
554
     * @param $table
555
     * @param $column
556
     * @throws \yii\db\Exception
557
     */
558
    public function dropForeignKeyByColumn($table, $column)
559
    {
560
        foreach (SchemaHelper::findTableForeignKeys($this->db, $table) as $key => $index) {
561
            if ($index->columnNames === (array)$column) {
562
                $this->dropForeignKey($key, $table);
563
            }
564
        }
565
    }
566
}