Passed
Push — master ( df5659...59487d )
by Bruno
04:26 queued 01:12
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 21
        $extra = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $extra is dead and can be removed.
Loading history...
129
130
        // TODO: scalars
131
132 21
        if ($field->type instanceof NonNull) {
133 21
            $type = $field->type->getWrappedType();
134
        } else {
135 1
            $type = $field->type;
136
        }
137
138 21
        $codeFragment = new MigrationCodeFragment();
139
140 21
        if ($type instanceof IDType) {
141 21
            $codeFragment->appendBase('$table->bigIncrements("id")');
142 16
        } elseif ($type instanceof StringType) {
143 16
            $codeFragment->appendBase('$table->string("' . $fieldName . '")');
144 4
        } elseif ($type instanceof IntType) {
145 1
            $codeFragment->appendBase('$table->integer("' . $fieldName . '")');
146 4
        } elseif ($type instanceof BooleanType) {
147 1
            $codeFragment->appendBase('$table->boolean("' . $fieldName . '")');
148 4
        } elseif ($type instanceof FloatType) {
149 1
            $codeFragment->appendBase('$table->float("' . $fieldName . '")');
150 3
        } elseif ($type instanceof EnumType) {
151
            $ourType = $this->parser->getScalarType($type->name);
152
            $parsedValues = $type->config['values'];
153
154
            if (!$ourType) {
155
                $parsedKeys = array_keys($parsedValues);
156
                $enumValues = array_combine($parsedKeys, $parsedKeys);
157
158
                // let's create this for the user
159
                $code = DatatypeFactory::generate(
160
                    $type->name,
161
                    'enum',
162
                    'App\\Datatypes',
163
                    'Tests\\Unit',
164
                    function (ClassType $enumClass) use ($enumValues) {
165
                        $enumClass->addConstant('CHOICES', $enumValues);
166
                        $enumClass->getMethod('__construct')->addBody('$this->choices = self::CHOICES;');
167
                    }
168
                );
169
        
170
                $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

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

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