Passed
Push — master ( 9503af...0b2a3d )
by Bruno
05:22
created

MigrationGenerator   F

Complexity

Total Complexity 73

Size/Duplication

Total Lines 515
Duplicated Lines 0 %

Test Coverage

Coverage 88.29%

Importance

Changes 0
Metric Value
wmc 73
eloc 319
c 0
b 0
f 0
dl 0
loc 515
rs 2.56
ccs 181
cts 205
cp 0.8829

8 Methods

Rating   Name   Duplication   Size   Complexity  
A generateManyToManyTable() 0 44 1
C processDirectives() 0 52 13
B generateString() 0 71 11
A generate() 0 15 2
F processRelationship() 0 108 23
D processBasetype() 0 71 17
A generateFilename() 0 37 5
A generateManyToManyMorphTable() 0 44 1

How to fix   Complexity   

Complex Class

Complex classes like MigrationGenerator 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 MigrationGenerator, and based on these observations, apply Extract Interface, too.

1
<?php declare(strict_types=1);
2
3
namespace Modelarium\Laravel\Targets;
4
5
use GraphQL\Language\AST\DirectiveNode;
6
use GraphQL\Type\Definition\BooleanType;
7
use GraphQL\Type\Definition\CustomScalarType;
8
use GraphQL\Type\Definition\Directive;
9
use GraphQL\Type\Definition\EnumType;
10
use GraphQL\Type\Definition\FloatType;
11
use GraphQL\Type\Definition\IDType;
12
use GraphQL\Type\Definition\IntType;
13
use GraphQL\Type\Definition\ListOfType;
14
use GraphQL\Type\Definition\NonNull;
15
use GraphQL\Type\Definition\ObjectType;
16
use GraphQL\Type\Definition\StringType;
17
use GraphQL\Type\Definition\UnionType;
18
use Modelarium\BaseGenerator;
19
use Modelarium\Exception\Exception;
20
use Modelarium\GeneratedCollection;
21
use Modelarium\GeneratedItem;
22
use Modelarium\Parser;
23
24
use function Safe\rsort;
25 5
26
function getStringBetween(string $string, string $start, string $end): string
27 5
{
28 5
    $ini = mb_strpos($string, $start);
29 5
    if ($ini === false) {
30 5
        return '';
31 5
    }
32
    $ini += mb_strlen($start);
33 5
    $len = mb_strpos($string, $end, $ini) - $ini;
34 5
    return mb_substr($string, $ini, $len);
35
}
36
37 15
function endsWith(string $haystack, string $needle): bool
38
{
39
    return substr_compare($haystack, $needle, -strlen($needle)) === 0;
40
}
41 15
class MigrationGenerator extends BaseGenerator
42 15
{
43
    /**
44
     * @var string
45
     */
46 15
    protected $stubDir = __DIR__ . "/stubs/";
47 15
48
    protected const MODE_CREATE = 'create';
49 1
    protected const MODE_PATCH = 'patch';
50
    protected const MODE_NO_CHANGE = 'nochange';
51
52 15
    /**
53
     * Unique counter
54 15
     *
55
     * @var integer
56 15
     */
57 15
    public static $counter = 0;
58 15
59 10
    /**
60 10
     * @var ObjectType
61 10
     */
62 2
    protected $type = null;
63 2
64 2
    /**
65 1
     * @var GeneratedCollection
66 1
     */
67 1
    protected $collection = null;
68 1
69 1
    /**
70 1
     * Code used in the create() call
71
     *
72
     * @var string[]
73
     */
74
    protected $createCode = [];
75
76
    /**
77
     * Code used post the create() call
78
     *
79
     * @var string[]
80
     */
81
    protected $postCreateCode = [];
82 15
83 1
    /**
84
     * 'create' or 'patch'
85
     *
86 15
     * @var string
87 2
     */
88 2
    protected $mode = self::MODE_CREATE;
89 2
90 1
    /**
91 1
     * Code used in the create() call
92 1
     *
93
     * @var string
94
     */
95 1
    protected $currentModel = '';
96 1
97 1
    public function generate(): GeneratedCollection
98
    {
99
        $this->collection = new GeneratedCollection();
100
        $this->currentModel = \GraphQL\Language\Printer::doPrint($this->type->astNode);
101
        $filename = $this->generateFilename($this->lowerName);
102
103
        if ($this->mode !== self::MODE_NO_CHANGE) {
104 15
            $item = new GeneratedItem(
105
                GeneratedItem::TYPE_MIGRATION,
106 15
                $this->generateString(),
107 15
                $filename
108
            );
109
            $this->collection->prepend($item);
110 5
        }
111
        return $this->collection;
112
    }
113
114 5
    protected function processBasetype(
115 5
        \GraphQL\Type\Definition\FieldDefinition $field,
116
        \GraphQL\Language\AST\NodeList $directives
117 5
    ): void {
118 5
        $fieldName = $field->name;
119
        $extra = [];
120 1
121
        // TODO: scalars
122
123 5
        if ($field->type instanceof NonNull) {
124
            $type = $field->type->getWrappedType();
125
        } else {
126
            $type = $field->type;
127 5
        }
128
129 5
        $base = '';
130
        if ($type instanceof IDType) {
131 5
            $base = '$table->bigIncrements("id")';
132 5
        } elseif ($type instanceof StringType) {
133
            $base = '$table->string("' . $fieldName . '")';
134 5
        } elseif ($type instanceof IntType) {
135 5
            $base = '$table->integer("' . $fieldName . '")';
136 5
        } elseif ($type instanceof BooleanType) {
137 5
            $base = '$table->bool("' . $fieldName . '")';
138
        } elseif ($type instanceof FloatType) {
139
            $base = '$table->float("' . $fieldName . '")';
140 5
        } elseif ($type instanceof EnumType) {
141
            throw new Exception("Enum is not supported here as a type field");
142
        } elseif ($type instanceof UnionType) {
143 5
            return;
144 4
        } elseif ($type instanceof CustomScalarType) {
145 4
            $ourType = $this->parser->getScalarType($type->name);
146
            if (!$ourType) {
147 4
                throw new Exception("Invalid extended scalar type: " . get_class($type));
148
            }
149
            $options = []; // TODO: from directives
150
            $base = '$table->' . $ourType->getLaravelSQLType($fieldName, $options);
151 4
        } elseif ($type instanceof ListOfType) {
152 1
            throw new Exception("Invalid field type: " . get_class($type));
153 1
        } else {
154
            throw new Exception("Invalid field type: " . get_class($type));
155
        }
156 4
157 4
        if (!($field->type instanceof NonNull)) {
158 4
            $base .= '->nullable()';
159 4
        }
160 1
161 4
        foreach ($directives as $directive) {
162 4
            /**
163
             * @var DirectiveNode $directive
164
             */
165 4
            $name = $directive->name->value;
166 5
            switch ($name) {
167 1
            case 'migrationUniqueIndex':
168 1
                $extra[] = '$table->unique("' . $fieldName . '");';
169
                break;
170
            case 'migrationIndex':
171 1
                $extra[] = '$table->index("' . $fieldName . '");';
172 1
                break;
173 1
            case 'migrationDefaultValue':
174
                $x = ''; // TODO
175 1
                Parser::getDirectiveArgumentByName($directive, 'value');
176 4
                $base .= '->default(' . $x . ')';
177 4
                throw new Exception('Default value not implemented yet');
178 4
                // break;
179 4
            }
180 4
        }
181 4
        $base .= ';';
182
183
        $this->createCode[] = $base;
184
        $this->createCode = array_merge($this->createCode, $extra);
185
    }
186 4
187
    protected function processRelationship(
188 4
        \GraphQL\Type\Definition\FieldDefinition $field,
189 4
        \GraphQL\Language\AST\NodeList $directives
190
    ): void {
191
        $lowerName = mb_strtolower($this->getInflector()->singularize($field->name));
192 4
        $lowerNamePlural = $this->getInflector()->pluralize($lowerName);
193
194
        if ($field->type instanceof NonNull) {
195 4
            $type = $field->type->getWrappedType();
196 4
        } else {
197 4
            $type = $field->type;
198 4
        }
199 4
200 4
        if ($field->type instanceof ListOfType) {
201
            $type = $field->type->getWrappedType();
202
        }
203 4
204 4
        $typeName = $type->name; /** @phpstan-ignore-line */
205 4
206 4
        $fieldName = $lowerName . '_id';
207 4
208 4
        $base = null;
209 4
        $extra = [];
210
211
        foreach ($directives as $directive) {
212
            $name = $directive->name->value;
213 5
            switch ($name) {
214 1
            case 'migrationSkip':
215
                return;
216
            case 'migrationUniqueIndex':
217 5
                $extra[] = '$table->unique("' . $fieldName . '");';
218 4
                break;
219 4
            case 'migrationIndex':
220
                $extra[] = '$table->index("' . $fieldName . '");';
221
                break;
222 5
            case 'belongsTo':
223
                $targetType = $this->parser->getType($typeName);
224
                if (!$targetType) {
225 15
                    throw new Exception("Cannot get type {$typeName} as a relationship to {$this->name}");
226
                } elseif (!($targetType instanceof ObjectType)) {
227
                    throw new Exception("{$typeName} is not a type for a relationship to {$this->name}");
228 15
                }
229
                try {
230 15
                    $targetField = $targetType->getField($this->lowerName); // TODO: might have another name than lowerName
231 5
                } catch (\GraphQL\Error\InvariantViolation $e) {
232 5
                    $targetField = $targetType->getField($this->lowerNamePlural);
233 5
                }
234 3
235 3
                $targetDirectives = $targetField->astNode->directives;
236 5
                foreach ($targetDirectives as $targetDirective) {
237 1
                    switch ($targetDirective->name->value) {
238
                    case 'hasOne':
239 1
                    case 'hasMany':
240 1
                        $base = '$table->unsignedBigInteger("' . $fieldName . '")';
241 1
                    break;
242
                    }
243 1
                }
244 1
                break;
245 4
246 1
            case 'belongsToMany':
247 1
                $type1 = $this->lowerName;
248 3
                $type2 = $lowerName;
249 3
250 3
                // we only generate once, so use a comparison for that
251 3
                if (strcasecmp($type1, $type2) < 0) {
252 3
                    $item = $this->generateManyToManyTable($type1, $type2);
253 3
                    $this->collection->push($item);
254
                }
255
                break;
256
257
            case 'morphTo':
258 15
                $relation = Parser::getDirectiveArgumentByName($directive, 'relation', $lowerName);
259
                $base = '$table->unsignedBigInteger("' . $relation . '_id")';
260
                $extra[] = '$table->string("' . $relation . '_type")';
261 15
                break;
262
263
            case 'morphedByMany':
264 15
                $relation = Parser::getDirectiveArgumentByName($directive, 'relation', $lowerName);
265
                $item = $this->generateManyToManyMorphTable($this->lowerName, $relation);
266 15
                $this->collection->push($item);
267 15
                break;
268
269 15
            case 'migrationForeign':
270 15
                $arguments = array_merge(
271 15
                    [
272 15
                        'references' => 'id',
273
                        'on' => $lowerNamePlural
274
                    ],
275
                    Parser::getDirectiveArguments($directive)
276 5
                );
277
                $extra[] = '$table->foreign("' . $fieldName . '")' .
278 15
                    "->references(\"{$arguments['references']}\")" .
279
                    "->on(\"{$arguments['on']}\")" .
280
                    ($arguments['onDelete'] ? "->onDelete(\"{$arguments['onDelete']}\")" : '') .
281
                    ($arguments['onUpdate'] ? "->onUpdate(\"{$arguments['onUpdate']}\")" : '') .
282 15
                    ';';
283
                break;
284
            }
285
        }
286 15
287 15
        if ($base) {
288 15
            if (!($field->type instanceof NonNull)) {
289
                $base .= '->nullable()';
290
            }
291 15
            $base .= ';';
292 15
            $this->createCode[] = $base;
293 15
        }
294 15
        $this->createCode = array_merge($this->createCode, $extra);
295
    }
296
297 15
    protected function processDirectives(
298 15
        \GraphQL\Language\AST\NodeList $directives
299 15
    ): void {
300 15
        foreach ($directives as $directive) {
301
            $name = $directive->name->value;
302
            switch ($name) {
303 15
            case 'migrationSoftDeletes':
304 15
                $this->createCode[] ='$table->softDeletes();';
305 15
                break;
306 15
            case 'migrationPrimaryIndex':
307
                // TODO
308 15
                throw new Exception("Primary index is not implemented yet");
309 15
            case 'migrationIndex':
310
                $values = $directive->arguments[0]->value->values;
311
312 1
                $indexFields = [];
313
                foreach ($values as $value) {
314
                    $indexFields[] = $value->value;
315
                }
316
                if (!count($indexFields)) {
317
                    throw new Exception("You must provide at least one field to an index");
318 1
                }
319 1
                $this->createCode[] ='$table->index("' . implode('", "', $indexFields) .'");';
320
                break;
321
            case 'migrationSpatialIndex':
322
                $this->createCode[] ='$table->spatialIndex("' . $directive->arguments[0]->value->value .'");';
323
                break;
324 1
325 1
            case 'migrationFulltextIndex':
326 1
                $values = $directive->arguments[0]->value->values;
327 1
328
                $indexFields = [];
329
                foreach ($values as $value) {
330 1
                    $indexFields[] = $value->value;
331 1
                }
332 1
333 1
                if (!count($indexFields)) {
334
                    throw new Exception("You must provide at least one field to a full text index");
335
                }
336 1
                $this->postCreateCode[] = "DB::statement('ALTER TABLE " .
337 1
                    $this->lowerNamePlural .
338 1
                    " ADD FULLTEXT fulltext_index (\"" .
339 1
                    implode('", "', $indexFields) .
340
                    "\")');";
341 1
                break;
342 1
            case 'migrationRememberToken':
343
                $this->createCode[] ='$table->rememberToken();';
344 1
                break;
345 1
            case 'migrationTimestamps':
346 1
                $this->createCode[] ='$table->timestamps();';
347 1
                break;
348
            default:
349 1
            }
350
        }
351
    }
352 5
353
    public function generateString(): string
354
    {
355
        return $this->stubToString('migration', function ($stub) {
356 5
            foreach ($this->type->getFields() as $field) {
357
                $directives = $field->astNode->directives;
358
                if (
359
                    ($field->type instanceof ObjectType) ||
360
                    ($field->type instanceof ListOfType) ||
361
                    ($field->type instanceof UnionType) ||
362
                    ($field->type instanceof NonNull && (
363
                        ($field->type->getWrappedType() instanceof ObjectType) ||
364
                        ($field->type->getWrappedType() instanceof ListOfType) ||
365
                        ($field->type->getWrappedType() instanceof UnionType)
366
                    ))
367
                ) {
368
                    // relationship
369
                    $this->processRelationship($field, $directives);
370
                } else {
371
                    $this->processBasetype($field, $directives);
372
                }
373
            }
374
375
            assert($this->type->astNode !== null);
376
            /**
377
             * @var \GraphQL\Language\AST\NodeList|null
378
             */
379
            $directives = $this->type->astNode->directives;
380
            if ($directives) {
381
                $this->processDirectives($directives);
0 ignored issues
show
Bug introduced by
$directives of type GraphQL\Language\AST\DirectiveNode[] is incompatible with the type GraphQL\Language\AST\NodeList expected by parameter $directives of Modelarium\Laravel\Targe...or::processDirectives(). ( Ignorable by Annotation )

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

381
                $this->processDirectives(/** @scrutinizer ignore-type */ $directives);
Loading history...
382
            }
383
384
            if ($this->mode === self::MODE_CREATE) {
385
                $stub = str_replace(
386
                    '// dummyCode',
387
                    join("\n            ", $this->createCode),
388
                    $stub
389
                );
390
391
                $stub = str_replace(
392
                    '// dummyPostCreateCode',
393
                    join("\n            ", $this->postCreateCode),
394
                    $stub
395
                );
396
            } else {
397
                $stub = str_replace(
398
                    '// dummyCode',
399
                    '// TODO: write the patch please',
400
                    $stub
401
                );
402
403
                $stub = str_replace(
404
                    '// dummyPostCreateCode',
405
                    '',
406
                    $stub
407
                );
408
            }
409
410
            $stub = str_replace(
411
                'dummytablename',
412
                $this->lowerNamePlural,
413
                $stub
414
            );
415
416
            $stub = str_replace(
417
                'modelSchemaCode',
418
                "# start graphql\n" .
419
                $this->currentModel .
420
                "\n# end graphql\n",
421
                $stub
422
            );
423
            return $stub;
424
        });
425
    }
426
427
    public function generateManyToManyMorphTable(string $name, string $relation): GeneratedItem
428
    {
429
        $contents = $this->stubToString('migration', function ($stub) use ($name, $relation) {
430
            $code = <<<EOF
431
432
            \$table->unsignedBigInteger("{$name}_id");
433
            \$table->unsignedBigInteger("{$relation}_id");
434
            \$table->string("{$relation}_type");
435
EOF;
436
437
            $stub = str_replace(
438
                '// dummyCode',
439
                $code,
440
                $stub
441
            );
442
443
            $stub = str_replace(
444
                'dummytablename',
445
                $this->getInflector()->pluralize($relation),
446
                $stub
447
            );
448
449
            $stub = str_replace(
450
                'modelSchemaCode',
451
                '',
452
                $stub
453
            );
454
            return $stub;
455
        });
456
457
        $item = new GeneratedItem(
458
            GeneratedItem::TYPE_MIGRATION,
459
            $contents,
460
            $this->getBasePath(
461
                'database/migrations/' .
462
                date('Y_m_d_His') .
463
                static::$counter++ . // so we keep the same order of types in schema
464
                '_' . $this->mode . '_' .
465
                $relation .
466
                '_table.php'
467
            )
468
        );
469
470
        return $item;
471
    }
472
473
    public function generateManyToManyTable(string $type1, string $type2): GeneratedItem
474
    {
475
        $contents = $this->stubToString('migration', function ($stub) use ($type1, $type2) {
476
            $code = <<<EOF
477
478
            \$table->increments("id");
479
            \$table->unsignedBigInteger("{$type1}_id");
480
            \$table->unsignedBigInteger("{$type2}_id");
481
EOF;
482
483
            $stub = str_replace(
484
                '// dummyCode',
485
                $code,
486
                $stub
487
            );
488
489
            $stub = str_replace(
490
                'dummytablename',
491
                "{$type1}_{$type2}",
492
                $stub
493
            );
494
495
            $stub = str_replace(
496
                'modelSchemaCode',
497
                '',
498
                $stub
499
            );
500
            return $stub;
501
        });
502
503
        $item = new GeneratedItem(
504
            GeneratedItem::TYPE_MIGRATION,
505
            $contents,
506
            $this->getBasePath(
507
                'database/migrations/' .
508
                date('Y_m_d_His') .
509
                static::$counter++ . // so we keep the same order of types in schema
510
                '_' . $this->mode . '_' .
511
                $type1 . '_' . $type2 .
512
                '_table.php'
513
            )
514
        );
515
516
        return $item;
517
    }
518
519
    protected function generateFilename(string $basename): string
520
    {
521
        $this->mode = self::MODE_CREATE;
522
        $match = '_' . $basename . '_table.php';
523
524
        $basepath = $this->getBasePath('database/migrations/');
525
        if (is_dir($basepath)) {
526
            $migrationFiles = \Safe\scandir($basepath);
527
            rsort($migrationFiles);
528
            foreach ($migrationFiles as $m) {
529
                if (!endsWith($m, $match)) {
530
                    continue;
531
                }
532
533
                // get source
534
                $data = \Safe\file_get_contents($basepath . '/' . $m);
535
536
                // compare with this source
537
                $model = trim(getStringBetween($data, '# start graphql', '# end graphql'));
538
539
                // if equal ignore and don't output file
540
                if ($model === $this->currentModel) {
541
                    $this->mode = self::MODE_NO_CHANGE;
542
                } else {
543
                    // else we'll generate a diff and patch
544
                    $this->mode = self::MODE_PATCH;
545
                }
546
                break;
547
            }
548
        }
549
550
        return $this->getBasePath(
551
            'database/migrations/' .
552
            date('Y_m_d_His') .
553
            static::$counter++ . // so we keep the same order of types in schema
554
            '_' . $this->mode . '_' .
555
            $basename . '_table.php'
556
        );
557
    }
558
}
559