Passed
Push — master ( 41a7c0...823f6c )
by Bruno
15:50 queued 07:18
created

MigrationGenerator::processDirectives()   C

Complexity

Conditions 13
Paths 15

Size

Total Lines 52
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 37
CRAP Score 13.0711

Importance

Changes 0
Metric Value
eloc 40
c 0
b 0
f 0
dl 0
loc 52
ccs 37
cts 40
cp 0.925
rs 6.6166
cc 13
nc 15
nop 1
crap 13.0711

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

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