MigrationGenerator   F
last analyzed

Complexity

Total Complexity 79

Size/Duplication

Total Lines 577
Duplicated Lines 0 %

Test Coverage

Coverage 64.89%

Importance

Changes 21
Bugs 9 Features 0
Metric Value
eloc 313
c 21
b 9
f 0
dl 0
loc 577
rs 2.08
ccs 183
cts 282
cp 0.6489
wmc 79

9 Methods

Rating   Name   Duplication   Size   Complexity  
A generate() 0 19 3
A generateManyToManyTable() 0 33 1
D checkMigrationCodeChange() 0 47 19
C generateString() 0 59 12
B processRelationship() 0 41 7
F processBasetype() 0 108 21
B generateFilename() 0 46 7
A generateManyToManyMorphTable() 0 32 1
B processEnum() 0 73 8

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 Formularium\CodeGenerator\LaravelEloquent\CodeGenerator as LaravelCodeGenerator;
6
use Formularium\Datatype;
7
use Formularium\Datatype\Datatype_enum;
8
use Formularium\Exception\ClassNotFoundException;
9
use Formularium\Factory\DatatypeFactory;
10
use Formularium\Field;
11
use Illuminate\Support\Str;
12
use GraphQL\Language\AST\DirectiveNode;
13
use GraphQL\Type\Definition\BooleanType;
14
use GraphQL\Type\Definition\CustomScalarType;
15
use GraphQL\Type\Definition\Directive;
16
use GraphQL\Type\Definition\EnumType;
17
use GraphQL\Type\Definition\FloatType;
18
use GraphQL\Type\Definition\IDType;
19
use GraphQL\Type\Definition\IntType;
20
use GraphQL\Type\Definition\ListOfType;
21
use GraphQL\Type\Definition\NonNull;
22
use GraphQL\Type\Definition\ObjectType;
23
use GraphQL\Type\Definition\StringType;
24
use GraphQL\Type\Definition\UnionType;
25
use Modelarium\BaseGenerator;
26
use Modelarium\Exception\Exception;
27
use Modelarium\Exception\ScalarNotFoundException;
28
use Modelarium\GeneratedCollection;
29
use Modelarium\GeneratedItem;
30
use Modelarium\Parser;
31
use Modelarium\Types\FormulariumScalarType;
32
use Nette\PhpGenerator\ClassType;
33
34
use function Safe\array_combine;
35
use function Safe\rsort;
36
use function Safe\date;
37
use function Safe\preg_match;
38
39
function getStringBetween(string $string, string $start, string $end): string
40
{
41
    $ini = mb_strpos($string, $start);
42
    if ($ini === false) {
43
        return '';
44
    }
45
    $ini += mb_strlen($start);
46
    $len = mb_strpos($string, $end, $ini) - $ini;
47
    return mb_substr($string, $ini, $len);
48
}
49
50
function endsWith(string $haystack, string $needle): bool
51
{
52
    return substr_compare($haystack, $needle, -strlen($needle)) === 0;
53
}
54
class MigrationGenerator extends BaseGenerator
55
{
56
    /**
57
     * @var string
58
     */
59
    protected $stubDir = __DIR__ . "/stubs/";
60
61
    protected const MODE_CREATE = 'create';
62
    protected const MODE_PATCH = 'patch';
63
    protected const MODE_NO_CHANGE = 'nochange';
64
65
    /**
66
     * Unique counter
67
     *
68
     * @var integer
69
     */
70
    public static $counter = 0;
71
72
    /**
73
     * @var ObjectType
74
     */
75
    protected $type = null;
76
77
    /**
78
     * @var GeneratedCollection
79
     */
80
    protected $collection = null;
81
82
    /**
83
     * Code used in the create() call
84
     *
85
     * @var string[]
86
     */
87
    public $createCode = [];
88
89
    /**
90
     * Code used post the create() call
91
     *
92
     * @var string[]
93
     */
94
    public $postCreateCode = [];
95
96
    /**
97
     * 'create' or 'patch'
98
     *
99
     * @var string
100
     */
101
    protected $mode = self::MODE_CREATE;
102
103
    /**
104
     * Code used in the create() call
105
     *
106
     * @var string
107
     */
108
    protected $currentModel = '';
109
110
    /**
111
     * The last migration code
112
     *
113
     * @var string
114
     */
115
    protected $lastMigrationCode = null;
116
117
    /**
118
     * Time stamp
119
     *
120
     * @var string
121
     */
122
    protected $stamp = '';
123
124 8
    public function generate(): GeneratedCollection
125
    {
126 8
        $this->collection = new GeneratedCollection();
127 8
        $this->currentModel = \GraphQL\Language\Printer::doPrint($this->type->astNode);
128 8
        $this->stamp = date('Y_m_d_His');
129 8
        $filename = $this->generateFilename($this->lowerName);
130
131 8
        if ($this->mode !== self::MODE_NO_CHANGE) {
132 8
            $code = $this->generateString();
133 8
            if ($this->checkMigrationCodeChange($code)) {
134 8
                $item = new GeneratedItem(
135 8
                    GeneratedItem::TYPE_MIGRATION,
136
                    $code,
137
                    $filename
138
                );
139 8
                $this->collection->prepend($item);
140
            }
141
        }
142 8
        return $this->collection;
143
    }
144
145
    /**
146
     * @param \GraphQL\Type\Definition\FieldDefinition $field
147
     * @param \GraphQL\Language\AST\NodeList<\GraphQL\Language\AST\DirectiveNode> $directives
148
     * @return void
149
     */
150 22
    protected function processBasetype(
151
        \GraphQL\Type\Definition\FieldDefinition $field,
152
        \GraphQL\Language\AST\NodeList $directives
153
    ): void {
154 22
        $fieldName = $field->name;
155
156 22
        $required = false;
157 22
        if ($field->getType() instanceof NonNull) {
158 22
            $required = true;
159 22
            $type = $field->getType()->getWrappedType();
160
        } else {
161 1
            $type = $field->getType();
162
        }
163
164 22
        $codeFragment = new MigrationCodeFragment();
165 22
        $lcg = new LaravelCodeGenerator();
166 22
        $formulariumField = null;
167
168 22
        if ($type instanceof IDType) {
169 22
            $codeFragment->appendBase('$table->bigIncrements("id")');
170 17
        } elseif ($type instanceof StringType) {
171 17
            $formulariumField = new Field(
172 17
                $fieldName,
173 17
                'string',
174 17
                [],
175 17
                [ Datatype::REQUIRED => ['value' => $required ] ]
176
            );
177 4
        } elseif ($type instanceof IntType) {
178 1
            $formulariumField = new Field(
179 1
                $fieldName,
180 1
                'integer',
181 1
                [],
182 1
                [ Datatype::REQUIRED => ['value' => $required ] ]
183
            );
184 4
        } elseif ($type instanceof BooleanType) {
185 1
            $formulariumField = new Field(
186 1
                $fieldName,
187 1
                'boolean',
188 1
                [],
189 1
                [ Datatype::REQUIRED => ['value' => $required ] ]
190
            );
191 4
        } elseif ($type instanceof FloatType) {
192 1
            $formulariumField = new Field(
193 1
                $fieldName,
194 1
                'float',
195 1
                [],
196 1
                [ Datatype::REQUIRED => ['value' => $required ] ]
197
            );
198 3
        } elseif ($type instanceof EnumType) {
199
            $this->processEnum($field, $type, $codeFragment);
200 3
        } elseif ($type instanceof UnionType) {
201
            return;
202 3
        } elseif ($type instanceof CustomScalarType) {
203 3
            $ourType = $this->parser->getScalarType($type->name);
204 3
            if (!$ourType) {
205
                throw new Exception("Null scalar type: " . get_class($type));
206 3
            } elseif (!is_a($ourType, FormulariumScalarType::class) &&
207 3
                !is_a($ourType, \Modelarium\Types\ScalarType::class)
208
            ) {
209
                throw new Exception("Invalid extended scalar type: " . get_class($type));
210
            }
211
            /**
212
             * @var FormulariumScalarType $ourType
213
             */
214 3
            $formulariumField = new Field(
215 3
                $fieldName,
216 3
                $ourType->getDatatype(),
217 3
                [],
218 3
                [ Datatype::REQUIRED => ['value' => $required ] ]
219
            );
220
        } elseif ($type instanceof ListOfType) {
221
            throw new Exception("Invalid field type: " . get_class($type));
222
        } else {
223
            throw new Exception("Invalid field type: " . get_class($type));
224
        }
225
226 22
        if ($formulariumField) {
227 17
            $fieldList = $lcg->field($formulariumField);
228 17
            foreach (is_array($fieldList) ? $fieldList : [$fieldList] as $f) {
229 17
                $codeFragment->appendBase(
230 17
                    '$table->' . $f
231
                );
232
            }
233 22
        } elseif (!($field->getType() instanceof NonNull)) {
234
            $codeFragment->appendBase('->nullable()');
235
        }
236
237 22
        foreach ($directives as $directive) {
238 1
            $name = $directive->name->value;
239 1
            if ($name === 'migrationSkip') { // special case
240
                return;
241
            }
242
243 1
            $className = $this->getDirectiveClass($name);
244 1
            if ($className) {
245 1
                $methodName = "$className::processMigrationFieldDirective";
246
                /** @phpstan-ignore-next-line */
247 1
                $methodName(
248 1
                    $this,
249
                    $field,
250
                    $directive,
251
                    $codeFragment
252
                );
253
            }
254
        }
255
256 22
        $this->createCode[] = $codeFragment->base . ';';
257 22
        $this->createCode = array_merge($this->createCode, $codeFragment->extraLines);
258 22
    }
259
260
    protected function processEnum(
261
        \GraphQL\Type\Definition\FieldDefinition $field,
262
        EnumType $type,
263
        MigrationCodeFragment $codeFragment
264
    ): void {
265
        $fieldName = $field->name;
266
        $ourType = $this->parser->getScalarType($type->name);
267
        $parsedValues = $type->config['values'];
268
269
        if (!$ourType) {
270
            $parsedKeys = array_keys($parsedValues);
271
            $enumValues = array_combine($parsedKeys, $parsedKeys);
272
273
            // let's create this for the user
274
            $code = DatatypeFactory::generate(
275
                $type->name,
276
                'enum',
277
                'App\\Modelarium\\Datatype',
278
                'Tests\\Unit',
279
                function (ClassType $enumClass) use ($enumValues) {
280
                    $enumClass->addConstant('CHOICES', $enumValues);
281
                    $enumClass->getMethod('__construct')->addBody('$this->choices = self::CHOICES;');
282
                }
283
            );
284
285
            $path = base_path('app/Modelarium/Datatype');
0 ignored issues
show
Bug introduced by
The function base_path was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

285
            $path = /** @scrutinizer ignore-call */ base_path('app/Modelarium/Datatype');
Loading history...
286
287
            $retval = DatatypeFactory::generateFile(
288
                $code,
289
                $path,
290
                base_path('tests/Unit/')
291
            );
292
293
            $php = \Modelarium\Util::generateLighthouseTypeFile($type->name, 'App\\Modelarium\\Datatype\\Types');
294
            $filename = $path . "/Types/Datatype_{$type->name}.php";
295
            if (!is_dir($path . "/Types")) {
296
                \Safe\mkdir($path . "/Types", 0777, true);
297
            }
298
            \Safe\file_put_contents($filename, $php);
299
300
            // recreate scalars
301
            \Modelarium\Util::generateScalarFile('App\\Modelarium\\Datatype', base_path('graphql/types.graphql'));
302
303
            // load php files that were just created
304
            require_once($retval['filename']);
305
            require_once($filename);
306
            $this->parser->appendScalar($type->name, 'App\\Modelarium\\Datatype\\Types\\Datatype_' . $type->name);
307
            $ourType = $this->parser->getScalarType($type->name);
308
        }
309
        if (!($ourType instanceof FormulariumScalarType)) {
310
            throw new Exception("Enum {$type->name} {$fieldName} is not a FormulariumScalarType");
311
        }
312
313
        /**
314
         * @var FormulariumScalarType $ourType
315
         */
316
        /**
317
         * @var Datatype_enum $ourDatatype
318
         */
319
        $ourDatatype = $ourType->getDatatype();
320
        $currentChoices = $ourDatatype->getChoices();
321
        if (array_diff_key($currentChoices, $parsedValues) || array_diff_key($parsedValues, $currentChoices)) {
322
            // TODO???
323
            $this->warn('Enum had its possible values changed. Please review the datatype class.');
324
        }
325
326
        $lcg = new LaravelCodeGenerator();
327
        $fieldList = $lcg->field(
328
            new Field($fieldName, $ourType->getDatatype())
329
        );
330
        foreach (is_array($fieldList) ? $fieldList : [$fieldList] as $f) {
331
            $codeFragment->appendBase(
332
                '$table->' . $f
333
            );
334
        }
335
    }
336
337
    /**
338
     * @param \GraphQL\Type\Definition\FieldDefinition $field
339
     * @param \GraphQL\Language\AST\NodeList<\GraphQL\Language\AST\DirectiveNode> $directives
340
     * @return void
341
     */
342 8
    protected function processRelationship(
343
        \GraphQL\Type\Definition\FieldDefinition $field,
344
        \GraphQL\Language\AST\NodeList $directives
345
    ): void {
346 8
        list($type, $isRequired) = Parser::getUnwrappedType($field->getType());
347 8
        $typeName = $type->name;
348
349
        // special types that should be skipped.
350 8
        if ($typeName === 'Can') {
351
            return;
352
        }
353
354 8
        $codeFragment = new MigrationCodeFragment();
355
356 8
        foreach ($directives as $directive) {
357 8
            $name = $directive->name->value;
358 8
            if ($name === 'migrationSkip') {
359
                return;
360
            }
361
362 8
            $className = $this->getDirectiveClass($name);
363 8
            if ($className) {
364 8
                $methodName = "$className::processMigrationRelationshipDirective";
365
                /** @phpstan-ignore-next-line */
366 8
                $methodName(
367 8
                    $this,
368
                    $field,
369
                    $directive,
370
                    $codeFragment
371
                );
372
            }
373
        }
374
375 8
        if ($codeFragment->base) {
376 6
            if (!($field->getType() instanceof NonNull)) {
377
                $codeFragment->appendBase('->nullable()');
378
            }
379 6
            $this->createCode[] = '$table' . $codeFragment->base . ';';
380
        }
381
382 8
        $this->createCode = array_merge($this->createCode, $codeFragment->extraLines);
383 8
    }
384
385 22
    public function generateString(): string
386
    {
387 22
        foreach ($this->type->getFields() as $field) {
388 22
            $directives = $field->astNode->directives;
389 22
            $type = $field->getType();
390
            if (
391 22
                ($type instanceof ObjectType) ||
392 22
                ($type instanceof ListOfType) ||
393 22
                ($type instanceof UnionType) ||
394 22
                ($type instanceof NonNull && (
395 22
                    ($type->getWrappedType() instanceof ObjectType) ||
396 22
                    ($type->getWrappedType() instanceof ListOfType) ||
397 22
                    ($type->getWrappedType() instanceof UnionType)
398
                ))
399
            ) {
400
                // relationship
401 8
                $this->processRelationship($field, $directives);
402
            } else {
403 22
                $this->processBasetype($field, $directives);
404
            }
405
        }
406
407
        assert($this->type->astNode !== null);
408
        /**
409
         * @var \GraphQL\Language\AST\NodeList<\GraphQL\Language\AST\DirectiveNode>|null
410
         */
411 22
        $directives = $this->type->astNode->directives;
412 22
        if ($directives) {
0 ignored issues
show
introduced by
$directives is of type GraphQL\Language\AST\NodeList, thus it always evaluated to true.
Loading history...
413 22
            $this->processTypeDirectives($directives, 'Migration');
414
        }
415
416
        $context = [
417 22
            'dummytablename' => $this->tableName,
418
            'modelSchemaCode' => "# start graphql\n" .
419 22
                $this->currentModel .
420 22
                "\n# end graphql",
421
        ];
422
423 22
        if ($this->mode === self::MODE_CREATE) {
424 22
            if ($this->lowerName == 'user') {
425 20
                $context['className'] = 'CreateUsers';
426
            } else {
427 8
                $context['className'] = 'Create' . $this->studlyName . str_replace('_', '', $this->stamp);
428
            }
429 22
            $context['upOperation'] = 'create';
430 22
            $context['downOperation'] = 'dropIfExists';
431 22
            $context['dummyCode'] = join("\n            ", $this->createCode);
432 22
            $context['dummyInverseCode'] = null;
433 22
            $context['dummyPostCreateCode'] = join("\n            ", $this->postCreateCode);
434
        } else {
435
            $context['className'] = 'Patch' . $this->studlyName . str_replace('_', '', $this->stamp);
436
            $context['upOperation'] = 'table';
437
            $context['downOperation'] = 'table';
438
            $context['dummyCode'] = '// TODO: write the patch please';
439
            $context['dummyInverseCode'] = '// TODO: write the inverse patch please';
440
            $context['dummyPostCreateCode'] = '';
441
        }
442
443 22
        return $this->templateStub('migration', $context);
444
    }
445
446
    /**
447
     * creates a many-to-many morph relationship table
448
     *
449
     * @param string $name
450
     * @param string $relation
451
     * @return string The table name.
452
     */
453 1
    public function generateManyToManyMorphTable(string $name, string $relation): string
454
    {
455
        $dummyCode = <<<EOF
456
457 1
            \$table->unsignedBigInteger("{$name}_id");
458 1
            \$table->unsignedBigInteger("{$relation}_id");
459 1
            \$table->string("{$relation}_type");
460
EOF;
461
        $context = [
462 1
            'dummyCode' => $dummyCode,
463 1
            'upOperation' => 'create',
464 1
            'downOperation' => 'dropIfExists',
465 1
            'dummytablename' => $this->getInflector()->pluralize($relation), // TODO: check, toTableName()?
466 1
            'modelSchemaCode' => ''
467
        ];
468 1
        $contents = $this->templateStub('migration', $context);
469
470 1
        $item = new GeneratedItem(
471 1
            GeneratedItem::TYPE_MIGRATION,
472
            $contents,
473 1
            $this->getBasePath(
474
                'database/migrations/' .
475 1
                $this->stamp .
476 1
                str_pad((string)(static::$counter++), 3, "0", STR_PAD_LEFT) . // so we keep the same order of types in schema
477 1
                '_' . $this->mode . '_' .
478 1
                $relation .
479 1
                '_table.php'
480
            )
481
        );
482 1
        $this->collection->push($item);
483
484 1
        return $context['dummytablename'];
485
    }
486
487
    /**
488
     * creates a many-to-many relationship table
489
     *
490
     * @param string $type1
491
     * @param string $type2
492
     * @return string The table name.
493
     */
494 1
    public function generateManyToManyTable(string $type1, string $type2): string
495
    {
496
        $dummyCode = <<<EOF
497
498
            \$table->increments("id");
499 1
            \$table->unsignedBigInteger("{$type1}_id")->references('id')->on('{$type1}');
500 1
            \$table->unsignedBigInteger("{$type2}_id")->references('id')->on('{$type2}');
501
EOF;
502
        $context = [
503 1
            'dummyCode' => $dummyCode,
504 1
            'upOperation' => 'create',
505 1
            'downOperation' => 'dropIfExists',
506 1
            'dummytablename' => "{$type1}_{$type2}",
507 1
            'className' => Str::studly($this->mode) . Str::studly($type1) . Str::studly($type2),
508 1
            'modelSchemaCode' => ''
509
        ];
510 1
        $contents = $this->templateStub('migration', $context);
511
512 1
        $item = new GeneratedItem(
513 1
            GeneratedItem::TYPE_MIGRATION,
514
            $contents,
515 1
            $this->getBasePath(
516
                'database/migrations/' .
517 1
                $this->stamp .
518 1
                str_pad((string)(static::$counter++), 3, "0", STR_PAD_LEFT) . // so we keep the same order of types in schema
519 1
                '_' . $this->mode . '_' .
520 1
                $type1 . '_' . $type2 .
521 1
                '_table.php'
522
            )
523
        );
524 1
        $this->collection->push($item);
525
526 1
        return $context['dummytablename'];
527
    }
528
529 8
    protected function generateFilename(string $basename): string
530
    {
531 8
        $this->mode = self::MODE_CREATE;
532 8
        $match = '/(patch|create)_' . preg_quote($basename) . '_(table|[0-9])/';
533
534 8
        $basepath = $this->getBasePath('database/migrations/');
535 8
        if (is_dir($basepath)) {
536
            $migrationFiles = \Safe\scandir($basepath);
537
            rsort($migrationFiles);
538
            foreach ($migrationFiles as $m) {
539
                if (!preg_match($match, $m)) {
540
                    continue;
541
                }
542
543
                // get source
544
                $this->lastMigrationCode = \Safe\file_get_contents($basepath . '/' . $m);
545
546
                // compare with this source
547
                $model = trim(getStringBetween($this->lastMigrationCode, '# start graphql', '# end graphql'));
548
549
                // if equal ignore and don't output file
550
                if ($model === trim($this->currentModel)) {
551
                    $this->mode = self::MODE_NO_CHANGE;
552
                } else {
553
                    // else we'll generate a diff and patch
554
                    $this->mode = self::MODE_PATCH;
555
                }
556
                break;
557
            }
558
        }
559
560 8
        if ($this->mode === self::MODE_CREATE && $this->lowerName === 'user') {
561 6
            return $this->getBasePath(
562 6
                'database/migrations/2014_10_12_000000_create_users_table.php'
563
            );
564
        }
565
566 8
        return $this->getBasePath(
567
            'database/migrations/' .
568 8
            $this->stamp .
569 8
            str_pad((string)(static::$counter++), 3, "0", STR_PAD_LEFT) . // so we keep the same order of types in schema
570 8
            '_' . $this->mode . '_' .
571 8
            $basename . '_' .
572 8
            str_replace('_', '', $this->stamp) . '_' .
573 8
            'table' .
574 8
            '.php'
575
        );
576
    }
577
578
    /**
579
     * Compares with the latest migration
580
     *
581
     * @param string $newcode
582
     * @return boolean
583
     */
584 8
    protected function checkMigrationCodeChange(string $newcode): bool
585
    {
586 8
        if (!$this->lastMigrationCode) {
587 8
            return true;
588
        }
589
        $tokens = token_get_all($this->lastMigrationCode);
590
        for ($i=0, $z=count($tokens); $i < $z; $i++) {
591
            if (is_array($tokens[$i]) && $tokens[$i][0] === T_FUNCTION
592
                && is_array($tokens[$i+1]) && $tokens[$i+1][0] == T_WHITESPACE
593
                && is_array($tokens[$i+2]) && $tokens[$i+2][1] == 'up'
594
            ) {
595
                $accumulator = [];
596
                $braceDepth = 0;
597
                // collect tokens from function head through opening brace
598
                while ($tokens[$i] != '{' && ($i < $z)) {
599
                    $accumulator[] = is_array($tokens[$i]) ? $tokens[$i][1] : $tokens[$i];
600
                    $i++;
601
                }
602
                if ($i == $z) {
603
                    // handle error
604
                } else {
605
                    // note, accumulate, and position index past brace
606
                    $braceDepth = 1;
607
                    $accumulator[] = '{';
608
                    $i++;
609
                }
610
                while ($braceDepth > 0 && ($i < $z)) {
611
                    if (is_array($tokens[$i])) {
612
                        $accumulator[] = $tokens[$i][1];
613
                    } else {
614
                        $accumulator[] = $tokens[$i];
615
                        if ($tokens[$i] == '{') {
616
                            $braceDepth++;
617
                        } elseif ($tokens[$i] == '}') {
618
                            $braceDepth--;
619
                        }
620
                    }
621
                    $i++;
622
                }
623
                $functionSrc = implode("", $accumulator);
624
                if ($functionSrc == $newcode) {
625
                    return false;
626
                }
627
            }
628
        }
629
630
        return true;
631
    }
632
}
633