Passed
Pull Request — 4.x (#25)
by
unknown
65:04
created

MigrateGenerateCommand   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 495
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 170
dl 0
loc 495
ccs 0
cts 186
cp 0
rs 8.5599
c 3
b 0
f 0
wmc 48

18 Methods

Rating   Name   Duplication   Size   Complexity  
A setup() 0 7 1
A getUpDown() 0 5 1
A askNumeric() 0 18 6
A getTemplateData() 0 32 2
B mergeMigrationsIntoSingle() 0 73 6
A askYn() 0 8 2
B generate() 0 17 7
A __construct() 0 11 1
A generateTablesAndIndices() 0 11 2
A handle() 0 22 3
A generateForeignKeys() 0 9 2
A askIfLogMigrationTable() 0 23 6
A generateMigrationFiles() 0 11 1
A getFileGenerationPath() 0 6 1
A getTemplatePath() 0 3 1
A filterTables() 0 11 3
A getExcludedTables() 0 12 2
A filterAndExcludeTables() 0 6 1

How to fix   Complexity   

Complex Class

Complex classes like MigrateGenerateCommand often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MigrateGenerateCommand, and based on these observations, apply Extract Interface, too.

1
<?php namespace KitLoong\MigrationsGenerator;
2
3
use Illuminate\Database\Migrations\MigrationRepositoryInterface;
4
use Illuminate\Support\Facades\Config;
5
use Illuminate\Support\Str;
6
use KitLoong\MigrationsGenerator\Generators\Decorator;
7
use KitLoong\MigrationsGenerator\Generators\SchemaGenerator;
8
use Way\Generators\Commands\GeneratorCommand;
9
use Way\Generators\Generator;
10
use Xethron\MigrationsGenerator\Syntax\AddForeignKeysToTable;
11
use Xethron\MigrationsGenerator\Syntax\AddToTable;
12
use Xethron\MigrationsGenerator\Syntax\DroppedTable;
13
use Xethron\MigrationsGenerator\Syntax\RemoveForeignKeysFromTable;
14
15
class MigrateGenerateCommand extends GeneratorCommand
16
{
17
18
    /**
19
     * The name and signature of the console command.
20
     * @var string
21
     */
22
    protected $signature = 'migrate:generate
23
                {tables? : A list of Tables you wish to Generate Migrations for separated by a comma: users,posts,comments}
24
                {--c|connection= : The database connection to use}
25
                {--t|tables= : A list of Tables you wish to Generate Migrations for separated by a comma: users,posts,comments}
26
                {--i|ignore= : A list of Tables you wish to ignore, separated by a comma: users,posts,comments}
27
                {--p|path= : Where should the file be created?}
28
                {--s|single : Generate all migrations into a single file}
29
                {--tp|templatePath= : The location of the template for this generator}
30
                {--defaultIndexNames : Don\'t use db index names for migrations}
31
                {--defaultFKNames : Don\'t use db foreign key names for migrations}';
32
33
    /**
34
     * The console command description.
35
     */
36
    protected $description = 'Generate a migration from an existing table structure.';
37
38
    /**
39
     * @var MigrationRepositoryInterface $repository
40
     */
41
    protected $repository;
42
43
    /**
44
     * @var SchemaGenerator
45
     */
46
    protected $schemaGenerator;
47
48
    /**
49
     * Array of Fields to create in a new Migration
50
     * Namely: Columns, Indexes and Foreign Keys
51
     */
52
    protected $fields = array();
53
54
    /**
55
     * List of Migrations that has been done
56
     */
57
    protected $migrations = array();
58
59
    protected $log = false;
60
61
    /**
62
     * @var int
63
     */
64
    protected $batch;
65
66
    /**
67
     * Filename date prefix (Y_m_d_His)
68
     * @var string
69
     */
70
    protected $datePrefix;
71
72
    /**
73
     * @var string
74
     */
75
    protected $migrationName;
76
77
    /**
78
     * @var string
79
     */
80
    protected $method;
81
82
    /**
83
     * @var string
84
     */
85
    protected $table;
86
87
    /**
88
     * Will append connection method if not default connection
89
     * @var string
90
     */
91
    protected $connection;
92
93
    protected $decorator;
94
95
96
    /**
97
     * Single file functionality
98
     * @var bool;
99
     */
100
    protected $single;
101
    /* @var array */
102
    protected $single_creates;
103
    /* @var array */
104
    protected $single_keys;
105
106
    public function __construct(
107
        Generator $generator,
108
        SchemaGenerator $schemaGenerator,
109
        MigrationRepositoryInterface $repository,
110
        Decorator $decorator
111
    ) {
112
        $this->schemaGenerator = $schemaGenerator;
113
        $this->repository = $repository;
114
        $this->decorator = $decorator;
115
116
        parent::__construct($generator);
117
    }
118
119
    /**
120
     * Execute the console command. Added for Laravel 5.5
121
     *
122
     * @return void
123
     * @throws \Doctrine\DBAL\DBALException
124
     */
125
    public function handle()
126
    {
127
        $this->setup($this->connection = $this->option('connection') ?: Config::get('database.default'));
128
129
        $this->info('Using connection: '.$this->connection."\n");
130
131
        $this->schemaGenerator->initialize();
132
133
        $tables = $this->filterTables();
134
        $this->info('Generating migrations for: '.implode(', ', $tables));
135
136
        $this->askIfLogMigrationTable();
137
138
        $this->single = $this->option('single');
0 ignored issues
show
Documentation Bug introduced by
The property $single was declared of type boolean, but $this->option('single') is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
139
140
        $this->generateMigrationFiles($tables);
141
142
        if ($this->single) {
143
            $this->mergeMigrationsIntoSingle();
144
        }
145
146
        $this->info("\nFinished!\n");
147
    }
148
149
    /**
150
     * Merge all generated migrations into a single file
151
     */
152
    protected function mergeMigrationsIntoSingle()
153
    {
154
        $creates = [];
155
        $keys = [];
156
157
        foreach ($this->single_creates as $table => $file) {
158
            $code = file_get_contents("database/migrations/${file}.php");
159
            list($up, $down) = $this->getUpDown($code);
160
            $creates[$table] = [ 'up' => $up, 'down' => $down, ];
161
            unlink("database/migrations/${file}.php");
162
        }
163
164
        foreach ($this->single_keys as $table => $file) {
165
            $code = file_get_contents("database/migrations/${file}.php");
166
            list($up, $down) = $this->getUpDown($code);
167
            $keys[$table] = [ 'up' => $up, 'down' => $down, ];
168
            unlink("database/migrations/${file}.php");
169
        }
170
171
        $pre = '
172
<?php
173
use Illuminate\Support\Facades\Schema;
174
use Illuminate\Database\Schema\Blueprint;
175
use Illuminate\Database\Migrations\Migration;
176
177
class CreateInitialTablesAndKeys extends Migration
178
{
179
    /**
180
    * Run the migrations.
181
    *
182
    * @return void
183
    */
184
    public function up()
185
    {
186
';
187
188
        $mid = '
189
   }
190
191
    /**
192
    * Reverse the migrations.
193
    *
194
    * @return void
195
    */
196
    public function down()
197
    {
198
        ';
199
200
        $end = '
201
    }
202
}';
203
        $tables = array_keys($creates);
204
        sort($tables);
205
206
        $ups = '';
207
        $downs = '';
208
209
        foreach ($tables as $table) {
210
            if (isset($creates[$table])) {
211
                $ups .= $creates[$table]['up'];
212
                $downs .= $creates[$table]['down'];
213
            }
214
            if (isset($keys[$table])) {
215
                $ups .= $keys[$table]['up'];
216
                $downs .= $keys[$table]['down'];
217
            }
218
        }
219
220
        $file = $pre . $ups . $mid . $downs . $end;
221
        $filename = "database/migrations/{$this->datePrefix}_create_initial_tables_and_keys.php";
222
        file_put_contents($filename, $file);
223
224
        $this->info("\nSingle file specified, migrations deleted and merged into ${filename}\n");
225
    }
226
227
    /**
228
     * Extract the up and php code
229
     * @param $code
230
     * @return array
231
     */
232
    protected function getUpDown($code)
233
    {
234
        preg_match('/public function up\(\)\n\s+{((?:[^}]*(?:}[^}]+)*)}\);)\n\s+}\n\n/m', $code, $upMatches);
235
        preg_match('/public function down\(\)\n\s+{((?:[^}]*(?:}[^}]+)*))}\n}/m', $code, $downMatches);
236
        return [ $upMatches[1], $downMatches[1] ];
237
    }
238
239
    protected function setup(string $connection): void
240
    {
241
        /** @var MigrationsGeneratorSetting $setting */
242
        $setting = app(MigrationsGeneratorSetting::class);
243
        $setting->setConnection($connection);
244
        $setting->setIgnoreIndexNames($this->option('defaultIndexNames'));
0 ignored issues
show
Bug introduced by
$this->option('defaultIndexNames') of type string is incompatible with the type boolean expected by parameter $ignoreIndexNames of KitLoong\MigrationsGener...::setIgnoreIndexNames(). ( Ignorable by Annotation )

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

244
        $setting->setIgnoreIndexNames(/** @scrutinizer ignore-type */ $this->option('defaultIndexNames'));
Loading history...
245
        $setting->setIgnoreForeignKeyNames($this->option('defaultFKNames'));
0 ignored issues
show
Bug introduced by
$this->option('defaultFKNames') of type string is incompatible with the type boolean expected by parameter $ignoreForeignKeyNames of KitLoong\MigrationsGener...IgnoreForeignKeyNames(). ( Ignorable by Annotation )

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

245
        $setting->setIgnoreForeignKeyNames(/** @scrutinizer ignore-type */ $this->option('defaultFKNames'));
Loading history...
246
    }
247
248
    /**
249
     * Get all tables from schema or return table list provided in option.
250
     * Then filter and exclude tables in --ignore option if any.
251
     * Also exclude migrations table
252
     *
253
     * @return string[]
254
     */
255
    protected function filterTables()
256
    {
257
        if ($tableArg = (string) $this->argument('tables')) {
258
            $tables = explode(',', $tableArg);
259
        } elseif ($tableOpt = (string) $this->option('tables')) {
260
            $tables = explode(',', $tableOpt);
261
        } else {
262
            $tables = $this->schemaGenerator->getTables();
263
        }
264
265
        return $this->filterAndExcludeTables($tables);
266
    }
267
268
    protected function askIfLogMigrationTable(): void
269
    {
270
        if (!$this->option('no-interaction')) {
271
            $this->log = $this->askYn('Do you want to log these migrations in the migrations table?');
272
        }
273
274
        if ($this->log) {
275
            $migrationSource = $this->connection;
276
277
            if ($migrationSource !== Config::get('database.default')) {
278
                if (!$this->askYn('Log into current connection: '.$this->connection.'? [Y = '.$this->connection.', n = '.Config::get('database.default').' (default connection)]')) {
279
                    $migrationSource = Config::get('database.default');
280
                }
281
            }
282
283
            $this->repository->setSource($migrationSource);
284
            if (!$this->repository->repositoryExists()) {
285
                $options = array('--database' => $migrationSource);
286
                $this->call('migrate:install', $options);
287
            }
288
            $this->batch = $this->askNumeric(
289
                'Next Batch Number is: '.$this->repository->getNextBatchNumber().'. We recommend using Batch Number 0 so that it becomes the "first" migration',
290
                0
291
            );
292
        }
293
    }
294
295
    protected function generateMigrationFiles(array $tables): void
296
    {
297
        $this->info("Setting up Tables and Index Migrations");
298
        $this->datePrefix = date('Y_m_d_His');
299
        $this->generateTablesAndIndices($tables);
300
301
        $this->info("\nSetting up Foreign Key Migrations\n");
302
303
        // Plus 1 second to have foreign key migrations generate after table migrations generated
304
        $this->datePrefix = date('Y_m_d_His', strtotime('+1 second'));
305
        $this->generateForeignKeys($tables);
306
    }
307
308
    /**
309
     * Ask for user input: Yes/No.
310
     *
311
     * @param  string  $question  Question to ask
312
     * @return boolean          Answer from user
313
     */
314
    protected function askYn(string $question): bool
315
    {
316
        $answer = $this->ask($question.' [Y/n] ') ?? 'y';
317
318
        while (!in_array(strtolower($answer), ['y', 'n', 'yes', 'no'])) {
319
            $answer = $this->ask('Please choose either yes or no. [Y/n]') ?? 'y';
320
        }
321
        return in_array(strtolower($answer), ['y', 'yes']);
322
    }
323
324
    /**
325
     * Ask user for a Numeric Value, or blank for default.
326
     *
327
     * @param  string  $question  Question to ask
328
     * @param  int|null  $default  Default Value (optional)
329
     * @return int           Answer
330
     */
331
    protected function askNumeric(string $question, $default = null): int
332
    {
333
        $ask = 'Your answer needs to be a numeric value';
334
335
        if (!is_null($default)) {
336
            $question .= ' [Default: '.$default.'] ';
337
            $ask .= ' or blank for default';
338
        }
339
340
        $answer = $this->ask($question);
341
342
        while (!is_numeric($answer) and !($answer == '' and !is_null($default))) {
343
            $answer = $this->ask($ask.'. ');
344
        }
345
        if ($answer == '') {
346
            $answer = $default;
347
        }
348
        return $answer;
349
    }
350
351
    /**
352
     * Generate tables and index migrations.
353
     *
354
     * @param  string[]  $tables  List of tables to create migrations for
355
     * @return void
356
     */
357
    protected function generateTablesAndIndices($tables)
358
    {
359
        $this->method = 'create';
360
361
        foreach ($tables as $tableName) {
362
            $this->table = $tableName;
363
            $this->migrationName = 'create_'.$this->decorator->tableUsedInFilename($tableName).'_table';
364
            $indexes = $this->schemaGenerator->getIndexes($tableName);
365
            $fields = $this->schemaGenerator->getFields($tableName, $indexes['single']);
366
            $this->fields = array_merge($fields, $indexes['multi']->toArray());
367
            $this->generate('creates');
368
        }
369
    }
370
371
    /**
372
     * Generate foreign key migrations.
373
     *
374
     * @param  array  $tables  List of tables to create migrations for
375
     * @return void
376
     */
377
    protected function generateForeignKeys(array $tables)
378
    {
379
        $this->method = 'table';
380
381
        foreach ($tables as $tableName) {
382
            $this->table = $tableName;
383
            $this->migrationName = 'add_foreign_keys_to_'.$this->decorator->tableUsedInFilename($tableName).'_table';
384
            $this->fields = $this->schemaGenerator->getForeignKeyConstraints($tableName);
385
            $this->generate('keys');
386
        }
387
    }
388
389
    /**
390
     * Generate Migration for the current table.
391
     *
392
     * @return void
393
     */
394
    protected function generate($type = 'creates')
395
    {
396
        if (!empty($this->fields)) {
397
            $this->create();
398
399
            $file = $this->datePrefix.'_'.$this->migrationName;
400
401
            if ($this->log) {
402
                $this->repository->log($file, $this->batch);
403
            }
404
405
            if ($this->single && $type === 'creates') {
406
                $this->single_creates[$this->table] = $file;
407
            }
408
409
            if ($this->single && $type === 'keys') {
410
                $this->single_keys[$this->table] = $file;
411
            }
412
        }
413
    }
414
415
    /**
416
     * The path where the file will be created.
417
     *
418
     * @return string
419
     */
420
    protected function getFileGenerationPath(): string
421
    {
422
        $path = $this->getPathByOptionOrConfig('path', 'migration_target_path');
423
        $fileName = $this->datePrefix.'_'.$this->migrationName.'.php';
424
425
        return "{$path}/{$fileName}";
426
    }
427
428
    /**
429
     * Fetch the template data.
430
     *
431
     * @return array
432
     */
433
    protected function getTemplateData(): array
434
    {
435
        if ($this->method == 'create') {
436
            $up = app(AddToTable::class)->run(
437
                $this->fields,
438
                $this->table,
439
                $this->connection,
440
                'create'
441
            );
442
            $down = app(DroppedTable::class)->run(
443
                $this->fields,
444
                $this->table,
445
                $this->connection,
446
                'drop'
447
            );
448
        } else {
449
            $up = app(AddForeignKeysToTable::class)->run(
450
                $this->fields,
451
                $this->table,
452
                $this->connection
453
            );
454
            $down = app(RemoveForeignKeysFromTable::class)->run(
455
                $this->fields,
456
                $this->table,
457
                $this->connection
458
            );
459
        }
460
461
        return [
462
            'CLASS' => ucwords(Str::camel($this->migrationName)),
463
            'UP' => $up,
464
            'DOWN' => $down
465
        ];
466
    }
467
468
    /**
469
     * Get path to template for generator.
470
     *
471
     * @return string
472
     */
473
    protected function getTemplatePath(): string
474
    {
475
        return $this->getPathByOptionOrConfig('templatePath', 'migration_template_path');
476
    }
477
478
    /**
479
     * Remove all the tables to exclude from the array of tables.
480
     *
481
     * @param  string[]  $tables
482
     *
483
     * @return string[]
484
     */
485
    protected function filterAndExcludeTables($tables)
486
    {
487
        $excludes = $this->getExcludedTables();
488
        $tables = array_diff($tables, $excludes);
489
490
        return $tables;
491
    }
492
493
    /**
494
     * Get a list of tables to be excluded.
495
     *
496
     * @return string[]
497
     */
498
    protected function getExcludedTables()
499
    {
500
        /** @var MigrationsGeneratorSetting $setting */
501
        $setting = app(MigrationsGeneratorSetting::class);
502
503
        $excludes = [$setting->getConnection()->getTablePrefix().Config::get('database.migrations')];
504
        $ignore = (string) $this->option('ignore');
505
        if (!empty($ignore)) {
506
            return array_merge($excludes, explode(',', $ignore));
507
        }
508
509
        return $excludes;
510
    }
511
}
512