Passed
Push — master ( 4e6115...77d3fa )
by Bruno
10:05 queued 05:29
created

MigrationGenerator::processRelationship()   F

Complexity

Conditions 26
Paths 445

Size

Total Lines 135
Code Lines 83

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 72
CRAP Score 26.927

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 26
eloc 83
c 1
b 1
f 0
nc 445
nop 2
dl 0
loc 135
ccs 72
cts 81
cp 0.8889
crap 26.927
rs 0.7708

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php declare(strict_types=1);
2
3
namespace Modelarium\Laravel\Targets;
4
5
use Formularium\Datatype\Datatype_enum;
6
use Formularium\Exception\ClassNotFoundException;
7
use Formularium\Factory\DatatypeFactory;
8
use Illuminate\Support\Str;
9
use GraphQL\Language\AST\DirectiveNode;
10
use GraphQL\Type\Definition\BooleanType;
11
use GraphQL\Type\Definition\CustomScalarType;
12
use GraphQL\Type\Definition\Directive;
13
use GraphQL\Type\Definition\EnumType;
14
use GraphQL\Type\Definition\FloatType;
15
use GraphQL\Type\Definition\IDType;
16
use GraphQL\Type\Definition\IntType;
17
use GraphQL\Type\Definition\ListOfType;
18
use GraphQL\Type\Definition\NonNull;
19
use GraphQL\Type\Definition\ObjectType;
20
use GraphQL\Type\Definition\StringType;
21
use GraphQL\Type\Definition\UnionType;
22
use Modelarium\BaseGenerator;
23
use Modelarium\Exception\Exception;
24
use Modelarium\Exception\ScalarNotFoundException;
25
use Modelarium\GeneratedCollection;
26
use Modelarium\GeneratedItem;
27
use Modelarium\Parser;
28
use Modelarium\Types\FormulariumScalarType;
29
use Nette\PhpGenerator\ClassType;
30
31
use function Safe\array_combine;
32
use function Safe\rsort;
33
use function Safe\date;
34
35
function getStringBetween(string $string, string $start, string $end): string
36
{
37
    $ini = mb_strpos($string, $start);
38
    if ($ini === false) {
39
        return '';
40
    }
41
    $ini += mb_strlen($start);
42
    $len = mb_strpos($string, $end, $ini) - $ini;
43
    return mb_substr($string, $ini, $len);
44
}
45
46
function endsWith(string $haystack, string $needle): bool
47
{
48
    return substr_compare($haystack, $needle, -strlen($needle)) === 0;
49
}
50
class MigrationGenerator extends BaseGenerator
51
{
52
    /**
53
     * @var string
54
     */
55
    protected $stubDir = __DIR__ . "/stubs/";
56
57
    protected const MODE_CREATE = 'create';
58
    protected const MODE_PATCH = 'patch';
59
    protected const MODE_NO_CHANGE = 'nochange';
60
61
    /**
62
     * Unique counter
63
     *
64
     * @var integer
65
     */
66
    public static $counter = 0;
67
68
    /**
69
     * @var ObjectType
70
     */
71
    protected $type = null;
72
73
    /**
74
     * @var GeneratedCollection
75
     */
76
    protected $collection = null;
77
78
    /**
79
     * Code used in the create() call
80
     *
81
     * @var string[]
82
     */
83
    public $createCode = [];
84
85
    /**
86
     * Code used post the create() call
87
     *
88
     * @var string[]
89
     */
90
    public $postCreateCode = [];
91
92
    /**
93
     * 'create' or 'patch'
94
     *
95
     * @var string
96
     */
97
    protected $mode = self::MODE_CREATE;
98
99
    /**
100
     * Code used in the create() call
101
     *
102
     * @var string
103
     */
104
    protected $currentModel = '';
105
106 8
    public function generate(): GeneratedCollection
107
    {
108 8
        $this->collection = new GeneratedCollection();
109 8
        $this->currentModel = \GraphQL\Language\Printer::doPrint($this->type->astNode);
110 8
        $filename = $this->generateFilename($this->lowerName);
111
112 8
        if ($this->mode !== self::MODE_NO_CHANGE) {
113 8
            $item = new GeneratedItem(
114 8
                GeneratedItem::TYPE_MIGRATION,
115 8
                $this->generateString(),
116
                $filename
117
            );
118 8
            $this->collection->prepend($item);
119
        }
120 8
        return $this->collection;
121
    }
122
123 21
    protected function processBasetype(
124
        \GraphQL\Type\Definition\FieldDefinition $field,
125
        \GraphQL\Language\AST\NodeList $directives
126
    ): void {
127 21
        $fieldName = $field->name;
128
129 21
        if ($field->type instanceof NonNull) {
130 21
            $type = $field->type->getWrappedType();
131
        } else {
132 1
            $type = $field->type;
133
        }
134
135 21
        $codeFragment = new MigrationCodeFragment();
136
137 21
        if ($type instanceof IDType) {
138 21
            $codeFragment->appendBase('$table->bigIncrements("id")');
139 16
        } elseif ($type instanceof StringType) {
140 16
            $codeFragment->appendBase('$table->string("' . $fieldName . '")');
141 4
        } elseif ($type instanceof IntType) {
142 1
            $codeFragment->appendBase('$table->integer("' . $fieldName . '")');
143 4
        } elseif ($type instanceof BooleanType) {
144 1
            $codeFragment->appendBase('$table->boolean("' . $fieldName . '")');
145 4
        } elseif ($type instanceof FloatType) {
146 1
            $codeFragment->appendBase('$table->float("' . $fieldName . '")');
147 3
        } elseif ($type instanceof EnumType) {
148
            $this->processEnum($type, $codeFragment);
149 3
        } elseif ($type instanceof UnionType) {
150
            return;
151 3
        } elseif ($type instanceof CustomScalarType) {
152 3
            $ourType = $this->parser->getScalarType($type->name);
153 3
            if (!$ourType) {
154
                throw new Exception("Invalid extended scalar type: " . get_class($type));
155
            }
156 3
            $options = []; // TODO: from directives
157 3
            $codeFragment->appendBase('$table->' . $ourType->getLaravelSQLType($fieldName, $options));
158
        } elseif ($type instanceof ListOfType) {
159
            throw new Exception("Invalid field type: " . get_class($type));
160
        } else {
161
            throw new Exception("Invalid field type: " . get_class($type));
162
        }
163
164 21
        if (!($field->type instanceof NonNull)) {
165 1
            $codeFragment->appendBase('->nullable()');
166
        }
167
168 21
        foreach ($directives as $directive) {
169 2
            $name = $directive->name->value;
170 2
            if ($name === 'migrationSkip') { // special case
171
                return;
172
            }
173
174 2
            $className = $this->getDirectiveClass($name);
175 2
            if ($className) {
176 1
                $methodName = "$className::processMigrationFieldDirective";
177
                /** @phpstan-ignore-next-line */
178 1
                $methodName(
179 1
                    $this,
180
                    $field,
181
                    $directive,
182
                    $codeFragment
183
                );
184
            }
185
        }
186
187 21
        $this->createCode[] = $codeFragment->base . ';';
188 21
        $this->createCode = array_merge($this->createCode, $codeFragment->extraLines);
189 21
    }
190
191
    protected function processEnum(
192
        EnumType $type,
193
        MigrationCodeFragment $codeFragment
194
    ) {
195
        $ourType = $this->parser->getScalarType($type->name);
196
        $parsedValues = $type->config['values'];
197
198
        if (!$ourType) {
199
            $parsedKeys = array_keys($parsedValues);
200
            $enumValues = array_combine($parsedKeys, $parsedKeys);
201
202
            // let's create this for the user
203
            $code = DatatypeFactory::generate(
204
                $type->name,
205
                'enum',
206
                'App\\Datatypes',
207
                'Tests\\Unit',
208
                function (ClassType $enumClass) use ($enumValues) {
209
                    $enumClass->addConstant('CHOICES', $enumValues);
210
                    $enumClass->getMethod('__construct')->addBody('$this->choices = self::CHOICES;');
211
                }
212
            );
213
    
214
            $path = base_path('app/Datatypes');
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

214
            $path = /** @scrutinizer ignore-call */ base_path('app/Datatypes');
Loading history...
215
            $lowerTypeName = mb_strtolower($type->name);
216
217
            $retval = DatatypeFactory::generateFile(
218
                $code,
219
                $path,
220
                base_path('tests/Unit/')
221
            );
222
223
            $php = \Modelarium\Util::generateLighthouseTypeFile($lowerTypeName, 'App\\Datatypes\\Types');
224
            $filename = $path . "/Types/Datatype_{$lowerTypeName}.php";
225
            if (!is_dir($path . "/Types")) {
226
                \Safe\mkdir($path . "/Types", 0777, true);
227
            }
228
            \Safe\file_put_contents($filename, $php);
229
    
230
            // recreate scalars
231
            \Modelarium\Util::generateScalarFile('App\\Datatypes', base_path('graphql/types.graphql'));
232
233
            // load php files that were just created
234
            require_once($retval['filename']);
235
            require_once($filename);
236
            $this->parser->appendScalar($type->name, 'App\\Datatypes\\Types\\Datatype_' . $lowerTypeName);
237
            $ourType = $this->parser->getScalarType($type->name);
238
        }
239
        if (!($ourType instanceof FormulariumScalarType)) {
240
            throw new Exception("Enum {$type->name} {$fieldName} is not a FormulariumScalarType");
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $fieldName seems to be never defined.
Loading history...
241
        }
242
243
        /**
244
         * @var FormulariumScalarType $ourType
245
         */
246
        /**
247
         * @var Datatype_enum $ourDatatype
248
         */
249
        $ourDatatype = $ourType->getDatatype();
250
        $currentChoices = $ourDatatype->getChoices();
251
        if (array_diff_key($currentChoices, $parsedValues) || array_diff_key($parsedValues, $currentChoices)) {
252
            // TODO???
253
        }
254
255
        $options = []; // TODO: from directives
256
        $codeFragment->appendBase('$table->'  . $ourType->getLaravelSQLType($fieldName, $options));
257
    }
258
259 8
    protected function processRelationship(
260
        \GraphQL\Type\Definition\FieldDefinition $field,
261
        \GraphQL\Language\AST\NodeList $directives
262
    ): void {
263 8
        $lowerName = mb_strtolower($this->getInflector()->singularize($field->name));
264 8
        $lowerNamePlural = $this->getInflector()->pluralize($lowerName);
265 8
        $fieldName = $lowerName . '_id';
266
267 8
        list($type, $isRequired) = Parser::getUnwrappedType($field->type);
268 8
        $typeName = $type->name;
269 8
        $tableName = self::toTableName($typeName);
270
271 8
        $base = null;
272 8
        $extra = [];
273
274
        // special types that should be skipped.
275 8
        if ($typeName === 'Can') {
276
            return;
277
        }
278
279 8
        $isManyToMany = false;
280 8
        foreach ($directives as $directive) {
281 8
            $name = $directive->name->value;
282 8
            if ($name === 'migrationSkip') {
283
                return;
284
            }
285
286
            // TODO: separate classes
287
            // $className = $this->getDirectiveClass($name);
288
            // if ($className) {
289
            //     $methodName = "$className::processMigrationRelationshipDirective";
290
            //     /** @phpstan-igno re-next-line */
291
            //     $methodName(
292
            //         $this,
293
            //         $field,
294
            //         $directive
295
            //     );
296
            // }
297
298 8
            switch ($name) {
299 8
            case 'migrationUniqueIndex':
300
                $extra[] = '$table->unique("' . $fieldName . '");';
301
                break;
302 8
            case 'migrationIndex':
303
                $extra[] = '$table->index("' . $fieldName . '");';
304
                break;
305 8
            case 'belongsTo':
306 4
                $targetType = $this->parser->getType($typeName);
307 4
                if (!$targetType) {
308
                    throw new Exception("Cannot get type {$typeName} as a relationship to {$this->baseName}");
309 4
                } elseif (!($targetType instanceof ObjectType)) {
310
                    throw new Exception("{$typeName} is not a type for a relationship to {$this->baseName}");
311
                }
312
                // we don't know what is the reverse relationship name at this point. so let's guess all possibilities
313
                try {
314 4
                    $targetField = $targetType->getField($tableName);
315 4
                } catch (\GraphQL\Error\InvariantViolation $e) {
316
                    try {
317 4
                        $targetField = $targetType->getField($this->tableName);
318 3
                    } catch (\GraphQL\Error\InvariantViolation $e) {
319
                        // one to one
320 3
                        $targetField = $targetType->getField($this->lowerName);
321
                    }
322
                }
323
324 4
                $targetDirectives = $targetField->astNode->directives;
325 4
                foreach ($targetDirectives as $targetDirective) {
326 4
                    switch ($targetDirective->name->value) {
327 4
                    case 'hasOne':
328 1
                    case 'hasMany':
329 4
                        $base = '$table->unsignedBigInteger("' . $fieldName . '")';
330 4
                    break;
331
                    }
332
                }
333 4
                break;
334
335 8
            case 'belongsToMany':
336 1
                $type1 = $this->lowerName;
337 1
                $type2 = $lowerName;
338
339
                // we only generate once, so use a comparison for that
340 1
                $isManyToMany = true;
341 1
                if (strcasecmp($type1, $type2) < 0) {
342 1
                    $this->generateManyToManyTable($type1, $type2);
343
                }
344 1
                break;
345
346 7
            case 'morphTo':
347 2
                $relation = Parser::getDirectiveArgumentByName($directive, 'relation', $lowerName);
348 2
                $base = '$table->unsignedBigInteger("' . $relation . '_id")';
349 2
                $extra[] = '$table->string("' . $relation . '_type")' .
350 2
                    ($isRequired ? '' : '->nullable()') . ';';
351 2
                break;
352
353 7
            case 'morphedByMany':
354 1
                $isManyToMany = true;
355 1
                $relation = Parser::getDirectiveArgumentByName($directive, 'relation', $lowerName);
356 1
                $this->generateManyToManyMorphTable($this->lowerName, $relation);
357 1
                break;
358
            }
359
        }
360
361 8
        foreach ($directives as $directive) {
362 8
            $name = $directive->name->value;
363
            switch ($name) {
364 8
            case 'migrationForeign':
365
                
366 4
                if (!$isManyToMany) {
367 4
                    $arguments = array_merge(
368
                        [
369 4
                            'references' => 'id',
370 4
                            'on' => $lowerNamePlural
371
                        ],
372 4
                        Parser::getDirectiveArguments($directive)
373
                    );
374
    
375 4
                    $extra[] = '$table->foreign("' . $fieldName . '")' .
376 4
                        "->references(\"{$arguments['references']}\")" .
377 4
                        "->on(\"{$arguments['on']}\")" .
378 4
                        (($arguments['onDelete'] ?? '') ? "->onDelete(\"{$arguments['onDelete']}\")" : '') .
379 4
                        (($arguments['onUpdate'] ?? '') ? "->onUpdate(\"{$arguments['onUpdate']}\")" : '') .
380 4
                        ';';
381
                }
382 4
                break;
383
            }
384
        }
385
386 8
        if ($base) {
387 6
            if (!($field->type instanceof NonNull)) {
388
                $base .= '->nullable()';
389
            }
390 6
            $base .= ';';
391 6
            $this->createCode[] = $base;
392
        }
393 8
        $this->createCode = array_merge($this->createCode, $extra);
394 8
    }
395
396 21
    protected function processDirectives(
397
        \GraphQL\Language\AST\NodeList $directives
398
    ): void {
399 21
        foreach ($directives as $directive) {
400 6
            $name = $directive->name->value;
401 6
            $className = $this->getDirectiveClass($name);
402 6
            if ($className) {
403 6
                $methodName = "$className::processMigrationTypeDirective";
404
                /** @phpstan-ignore-next-line */
405 6
                $methodName(
406 6
                    $this,
407
                    $directive
408
                );
409
            }
410
        }
411 21
    }
412
413 21
    public function generateString(): string
414
    {
415 21
        foreach ($this->type->getFields() as $field) {
416 21
            $directives = $field->astNode->directives;
417
            if (
418 21
                ($field->type instanceof ObjectType) ||
419 21
                ($field->type instanceof ListOfType) ||
420 21
                ($field->type instanceof UnionType) ||
421 21
                ($field->type instanceof NonNull && (
422 21
                    ($field->type->getWrappedType() instanceof ObjectType) ||
423 21
                    ($field->type->getWrappedType() instanceof ListOfType) ||
424 21
                    ($field->type->getWrappedType() instanceof UnionType)
425
                ))
426
            ) {
427
                // relationship
428 8
                $this->processRelationship($field, $directives);
429
            } else {
430 21
                $this->processBasetype($field, $directives);
431
            }
432
        }
433
434
        assert($this->type->astNode !== null);
435
        /**
436
         * @var \GraphQL\Language\AST\NodeList|null
437
         */
438 21
        $directives = $this->type->astNode->directives;
439 21
        if ($directives) {
440 21
            $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

440
            $this->processDirectives(/** @scrutinizer ignore-type */ $directives);
Loading history...
441
        }
442
443
        $context = [
444 21
            'dummytablename' => $this->tableName,
445
            'modelSchemaCode' => "# start graphql\n" .
446 21
                $this->currentModel .
447 21
                "\n# end graphql",
448
        ];
449
450 21
        if ($this->mode === self::MODE_CREATE) {
451 21
            $context['className'] = 'Create' . $this->studlyName;
452 21
            $context['dummyCode'] = join("\n            ", $this->createCode);
453 21
            $context['dummyPostCreateCode'] = join("\n            ", $this->postCreateCode);
454
        } else {
455
            $context['className'] = 'Patch' . $this->studlyName . date('YmdHis');
456
            $context['dummyCode'] = '// TODO: write the patch please';
457
            $context['dummyPostCreateCode'] = '';
458
        }
459
460 21
        return $this->templateStub('migration', $context);
461
    }
462
463
    /**
464
     * creates a many-to-many morph relationship table
465
     *
466
     * @param string $name
467
     * @param string $relation
468
     * @return string The table name.
469
     */
470 1
    protected function generateManyToManyMorphTable(string $name, string $relation): string
471
    {
472
        $dummyCode = <<<EOF
473
474 1
            \$table->unsignedBigInteger("{$name}_id");
475 1
            \$table->unsignedBigInteger("{$relation}_id");
476 1
            \$table->string("{$relation}_type");
477
EOF;
478
        $context = [
479 1
            'dummyCode' => $dummyCode,
480 1
            'dummytablename' => $this->getInflector()->pluralize($relation), // TODO: check, toTableName()?
481 1
            'modelSchemaCode' => ''
482
        ];
483 1
        $contents = $this->templateStub('migration', $context);
484
485 1
        $item = new GeneratedItem(
486 1
            GeneratedItem::TYPE_MIGRATION,
487
            $contents,
488 1
            $this->getBasePath(
489
                'database/migrations/' .
490 1
                date('Y_m_d_His') .
491 1
                str_pad((string)(static::$counter++), 3, "0", STR_PAD_LEFT) . // so we keep the same order of types in schema
492 1
                '_' . $this->mode . '_' .
493 1
                $relation .
494 1
                '_table.php'
495
            )
496
        );
497 1
        $this->collection->push($item);
498
499 1
        return $context['dummytablename'];
500
    }
501
502
    /**
503
     * creates a many-to-many relationship table
504
     *
505
     * @param string $type1
506
     * @param string $type2
507
     * @return string The table name.
508
     */
509 1
    protected function generateManyToManyTable(string $type1, string $type2): string
510
    {
511
        $dummyCode = <<<EOF
512
513
            \$table->increments("id");
514 1
            \$table->unsignedBigInteger("{$type1}_id")->references('id')->on('{$type1}');
515 1
            \$table->unsignedBigInteger("{$type2}_id")->references('id')->on('{$type2}');
516
EOF;
517
        $context = [
518 1
            'dummyCode' => $dummyCode,
519 1
            'dummytablename' => "{$type1}_{$type2}",
520 1
            'className' => Str::studly($this->mode) . Str::studly($type1) . Str::studly($type2),
521 1
            'modelSchemaCode' => ''
522
        ];
523 1
        $contents = $this->templateStub('migration', $context);
524
525 1
        $item = new GeneratedItem(
526 1
            GeneratedItem::TYPE_MIGRATION,
527
            $contents,
528 1
            $this->getBasePath(
529
                'database/migrations/' .
530 1
                date('Y_m_d_His') .
531 1
                str_pad((string)(static::$counter++), 3, "0", STR_PAD_LEFT) . // so we keep the same order of types in schema
532 1
                '_' . $this->mode . '_' .
533 1
                $type1 . '_' . $type2 .
534 1
                '_table.php'
535
            )
536
        );
537 1
        $this->collection->push($item);
538
539 1
        return $context['dummytablename'];
540
    }
541
542 8
    protected function generateFilename(string $basename): string
543
    {
544 8
        $this->mode = self::MODE_CREATE;
545 8
        $match = '_' . $basename . '_table.php';
546
547 8
        $basepath = $this->getBasePath('database/migrations/');
548 8
        if (is_dir($basepath)) {
549
            $migrationFiles = \Safe\scandir($basepath);
550
            rsort($migrationFiles);
551
            foreach ($migrationFiles as $m) {
552
                if (!endsWith($m, $match)) {
553
                    continue;
554
                }
555
556
                // get source
557
                $data = \Safe\file_get_contents($basepath . '/' . $m);
558
559
                // compare with this source
560
                $model = trim(getStringBetween($data, '# start graphql', '# end graphql'));
561
562
                // if equal ignore and don't output file
563
                if ($model === $this->currentModel) {
564
                    $this->mode = self::MODE_NO_CHANGE;
565
                } else {
566
                    // else we'll generate a diff and patch
567
                    $this->mode = self::MODE_PATCH;
568
                }
569
                break;
570
            }
571
        }
572
573 8
        return $this->getBasePath(
574
            'database/migrations/' .
575 8
            date('Y_m_d_His') .
576 8
            str_pad((string)(static::$counter++), 3, "0", STR_PAD_LEFT) . // so we keep the same order of types in schema
577 8
            '_' . $this->mode . '_' .
578 8
            $basename . '_table.php'
579
        );
580
    }
581
}
582