Passed
Push — master ( d148de...2e74be )
by Bruno
09:35
created

MigrationGenerator::processRelationship()   F

Complexity

Conditions 24
Paths 340

Size

Total Lines 113
Code Lines 78

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 68
CRAP Score 24.099

Importance

Changes 0
Metric Value
eloc 78
c 0
b 0
f 0
dl 0
loc 113
ccs 68
cts 72
cp 0.9444
rs 1.5833
cc 24
nc 340
nop 2
crap 24.099

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 5
26
function getStringBetween(string $string, string $start, string $end): string
27 5
{
28 5
    $ini = mb_strpos($string, $start);
29 5
    if ($ini === false) {
30 5
        return '';
31 5
    }
32
    $ini += mb_strlen($start);
33 5
    $len = mb_strpos($string, $end, $ini) - $ini;
34 5
    return mb_substr($string, $ini, $len);
35
}
36
37 15
function endsWith(string $haystack, string $needle): bool
38
{
39
    return substr_compare($haystack, $needle, -strlen($needle)) === 0;
40
}
41 15
class MigrationGenerator extends BaseGenerator
42 15
{
43
    /**
44
     * @var string
45
     */
46 15
    protected $stubDir = __DIR__ . "/stubs/";
47 15
48
    protected const MODE_CREATE = 'create';
49 1
    protected const MODE_PATCH = 'patch';
50
    protected const MODE_NO_CHANGE = 'nochange';
51
52 15
    /**
53
     * Unique counter
54 15
     *
55
     * @var integer
56 15
     */
57 15
    public static $counter = 0;
58 15
59 10
    /**
60 10
     * @var ObjectType
61 10
     */
62 2
    protected $type = null;
63 2
64 2
    /**
65 1
     * @var GeneratedCollection
66 1
     */
67 1
    protected $collection = null;
68 1
69 1
    /**
70 1
     * Code used in the create() call
71
     *
72
     * @var string[]
73
     */
74
    protected $createCode = [];
75
76
    /**
77
     * Code used post the create() call
78
     *
79
     * @var string[]
80
     */
81
    protected $postCreateCode = [];
82 15
83 1
    /**
84
     * 'create' or 'patch'
85
     *
86 15
     * @var string
87 2
     */
88 2
    protected $mode = self::MODE_CREATE;
89 2
90 1
    /**
91 1
     * Code used in the create() call
92 1
     *
93
     * @var string
94
     */
95 1
    protected $currentModel = '';
96 1
97 1
    public function generate(): GeneratedCollection
98
    {
99
        $this->collection = new GeneratedCollection();
100
        $this->currentModel = \GraphQL\Language\Printer::doPrint($this->type->astNode);
101
        $filename = $this->generateFilename($this->lowerName);
102
103
        if ($this->mode !== self::MODE_NO_CHANGE) {
104 15
            $item = new GeneratedItem(
105
                GeneratedItem::TYPE_MIGRATION,
106 15
                $this->generateString(),
107 15
                $filename
108
            );
109
            $this->collection->prepend($item);
110 5
        }
111
        return $this->collection;
112
    }
113
114 5
    protected function processBasetype(
115 5
        \GraphQL\Type\Definition\FieldDefinition $field,
116
        \GraphQL\Language\AST\NodeList $directives
117 5
    ): void {
118 5
        $fieldName = $field->name;
119
        $extra = [];
120 1
121
        // TODO: scalars
122
123 5
        if ($field->type instanceof NonNull) {
124
            $type = $field->type->getWrappedType();
125
        } else {
126
            $type = $field->type;
127 5
        }
128
129 5
        $base = '';
130
        if ($type instanceof IDType) {
131 5
            $base = '$table->bigIncrements("id")';
132 5
        } elseif ($type instanceof StringType) {
133
            $base = '$table->string("' . $fieldName . '")';
134 5
        } elseif ($type instanceof IntType) {
135 5
            $base = '$table->integer("' . $fieldName . '")';
136 5
        } elseif ($type instanceof BooleanType) {
137 5
            $base = '$table->bool("' . $fieldName . '")';
138
        } elseif ($type instanceof FloatType) {
139
            $base = '$table->float("' . $fieldName . '")';
140 5
        } elseif ($type instanceof EnumType) {
141
            throw new Exception("Enum is not supported here as a type field");
142
        } elseif ($type instanceof UnionType) {
143 5
            return;
144 4
        } elseif ($type instanceof CustomScalarType) {
145 4
            $ourType = $this->parser->getScalarType($type->name);
146
            if (!$ourType) {
147 4
                throw new Exception("Invalid extended scalar type: " . get_class($type));
148
            }
149
            $options = []; // TODO: from directives
150
            $base = '$table->' . $ourType->getLaravelSQLType($fieldName, $options);
151 4
        } elseif ($type instanceof ListOfType) {
152 1
            throw new Exception("Invalid field type: " . get_class($type));
153 1
        } else {
154
            throw new Exception("Invalid field type: " . get_class($type));
155
        }
156 4
157 4
        if (!($field->type instanceof NonNull)) {
158 4
            $base .= '->nullable()';
159 4
        }
160 1
161 4
        foreach ($directives as $directive) {
162 4
            /**
163
             * @var DirectiveNode $directive
164
             */
165 4
            $name = $directive->name->value;
166 5
            switch ($name) {
167 1
            case 'migrationUniqueIndex':
168 1
                $extra[] = '$table->unique("' . $fieldName . '");';
169
                break;
170
            case 'migrationIndex':
171 1
                $extra[] = '$table->index("' . $fieldName . '");';
172 1
                break;
173 1
            case 'migrationDefaultValue':
174
                $x = ''; // TODO
175 1
                Parser::getDirectiveArgumentByName($directive, 'value');
176 4
                $base .= '->default(' . $x . ')';
177 4
                throw new Exception('Default value not implemented yet');
178 4
                // break;
179 4
            }
180 4
        }
181 4
        $base .= ';';
182
183
        $this->createCode[] = $base;
184
        $this->createCode = array_merge($this->createCode, $extra);
185
    }
186 4
187
    protected function processRelationship(
188 4
        \GraphQL\Type\Definition\FieldDefinition $field,
189 4
        \GraphQL\Language\AST\NodeList $directives
190
    ): void {
191
        $lowerName = mb_strtolower($this->getInflector()->singularize($field->name));
192 4
        $lowerNamePlural = $this->getInflector()->pluralize($lowerName);
193
        $fieldName = $lowerName . '_id';
194
195 4
        list($type, $isRequired) = Parser::getUnwrappedType($field->type);
196 4
        $typeName = $type->name; /** @phpstan-ignore-line */
197 4
198 4
        $base = null;
199 4
        $extra = [];
200 4
201
        // special types that should be skipped.
202
        if ($typeName === 'Can') {
203 4
            return;
204 4
        }
205 4
206 4
        $isManytoMany = false;
207 4
        foreach ($directives as $directive) {
208 4
            $name = $directive->name->value;
209 4
            switch ($name) {
210
            case 'migrationSkip':
211
                return;
212
            case 'migrationUniqueIndex':
213 5
                $extra[] = '$table->unique("' . $fieldName . '");';
214 1
                break;
215
            case 'migrationIndex':
216
                $extra[] = '$table->index("' . $fieldName . '");';
217 5
                break;
218 4
            case 'belongsTo':
219 4
                $targetType = $this->parser->getType($typeName);
220
                if (!$targetType) {
221
                    throw new Exception("Cannot get type {$typeName} as a relationship to {$this->name}");
222 5
                } elseif (!($targetType instanceof ObjectType)) {
223
                    throw new Exception("{$typeName} is not a type for a relationship to {$this->name}");
224
                }
225 15
                try {
226
                    $targetField = $targetType->getField($this->lowerName); // TODO: might have another name than lowerName
227
                } catch (\GraphQL\Error\InvariantViolation $e) {
228 15
                    $targetField = $targetType->getField($this->lowerNamePlural);
229
                }
230 15
231 5
                $targetDirectives = $targetField->astNode->directives;
232 5
                foreach ($targetDirectives as $targetDirective) {
233 5
                    switch ($targetDirective->name->value) {
234 3
                    case 'hasOne':
235 3
                    case 'hasMany':
236 5
                        $base = '$table->unsignedBigInteger("' . $fieldName . '")';
237 1
                    break;
238
                    }
239 1
                }
240 1
                break;
241 1
242
            case 'belongsToMany':
243 1
                $type1 = $this->lowerName;
244 1
                $type2 = $lowerName;
245 4
246 1
                // we only generate once, so use a comparison for that
247 1
                $isManytoMany = true;
248 3
                if (strcasecmp($type1, $type2) < 0) {
249 3
                    $this->generateManyToManyTable($type1, $type2);
250 3
                }
251 3
                break;
252 3
253 3
            case 'morphTo':
254
                $relation = Parser::getDirectiveArgumentByName($directive, 'relation', $lowerName);
255
                $base = '$table->unsignedBigInteger("' . $relation . '_id")';
256
                $extra[] = '$table->string("' . $relation . '_type")';
257
                break;
258 15
259
            case 'morphedByMany':
260
                $isManytoMany = true;
261 15
                $relation = Parser::getDirectiveArgumentByName($directive, 'relation', $lowerName);
262
                $this->generateManyToManyMorphTable($this->lowerName, $relation);
263
                break;
264 15
            }
265
        }
266 15
267 15
        foreach ($directives as $directive) {
268
            $name = $directive->name->value;
269 15
            switch ($name) {
270 15
            case 'migrationForeign':
271 15
                
272 15
                if (!$isManytoMany) {
273
                    $arguments = array_merge(
274
                        [
275
                            'references' => 'id',
276 5
                            'on' => $lowerNamePlural
277
                        ],
278 15
                        Parser::getDirectiveArguments($directive)
279
                    );
280
    
281
                    $extra[] = '$table->foreign("' . $fieldName . '")' .
282 15
                        "->references(\"{$arguments['references']}\")" .
283
                        "->on(\"{$arguments['on']}\")" .
284
                        ($arguments['onDelete'] ? "->onDelete(\"{$arguments['onDelete']}\")" : '') .
285
                        ($arguments['onUpdate'] ? "->onUpdate(\"{$arguments['onUpdate']}\")" : '') .
286 15
                        ';';
287 15
                }
288 15
                break;
289
            }
290
        }
291 15
292 15
        if ($base) {
293 15
            if (!($field->type instanceof NonNull)) {
294 15
                $base .= '->nullable()';
295
            }
296
            $base .= ';';
297 15
            $this->createCode[] = $base;
298 15
        }
299 15
        $this->createCode = array_merge($this->createCode, $extra);
300 15
    }
301
302
    protected function processDirectives(
303 15
        \GraphQL\Language\AST\NodeList $directives
304 15
    ): void {
305 15
        foreach ($directives as $directive) {
306 15
            $name = $directive->name->value;
307
            switch ($name) {
308 15
            case 'migrationSoftDeletes':
309 15
                $this->createCode[] ='$table->softDeletes();';
310
                break;
311
            case 'migrationPrimaryIndex':
312 1
                // TODO
313
                throw new Exception("Primary index is not implemented yet");
314
            case 'migrationIndex':
315
                $values = $directive->arguments[0]->value->values;
316
317
                $indexFields = [];
318 1
                foreach ($values as $value) {
319 1
                    $indexFields[] = $value->value;
320
                }
321
                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 1
            case 'migrationSpatialIndex':
327 1
                $this->createCode[] ='$table->spatialIndex("' . $directive->arguments[0]->value->value .'");';
328
                break;
329
330 1
            case 'migrationFulltextIndex':
331 1
                $values = $directive->arguments[0]->value->values;
332 1
333 1
                $indexFields = [];
334
                foreach ($values as $value) {
335
                    $indexFields[] = $value->value;
336 1
                }
337 1
338 1
                if (!count($indexFields)) {
339 1
                    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
                    " ADD FULLTEXT fulltext_index (\"" .
344 1
                    implode('", "', $indexFields) .
345 1
                    "\")');";
346 1
                break;
347 1
            case 'migrationRememberToken':
348
                $this->createCode[] ='$table->rememberToken();';
349 1
                break;
350
            case 'migrationTimestamps':
351
                $this->createCode[] ='$table->timestamps();';
352 5
                break;
353
            default:
354
            }
355
        }
356 5
    }
357
358
    public function generateString(): string
359
    {
360
        foreach ($this->type->getFields() as $field) {
361
            $directives = $field->astNode->directives;
362
            if (
363
                ($field->type instanceof ObjectType) ||
364
                ($field->type instanceof ListOfType) ||
365
                ($field->type instanceof UnionType) ||
366
                ($field->type instanceof NonNull && (
367
                    ($field->type->getWrappedType() instanceof ObjectType) ||
368
                    ($field->type->getWrappedType() instanceof ListOfType) ||
369
                    ($field->type->getWrappedType() instanceof UnionType)
370
                ))
371
            ) {
372
                // relationship
373
                $this->processRelationship($field, $directives);
374
            } else {
375
                $this->processBasetype($field, $directives);
376
            }
377
        }
378
379
        assert($this->type->astNode !== null);
380
        /**
381
         * @var \GraphQL\Language\AST\NodeList|null
382
         */
383
        $directives = $this->type->astNode->directives;
384
        if ($directives) {
385
            $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
            'dummytablename' => $this->lowerNamePlural,
390
            'modelSchemaCode' => "# start graphql\n" .
391
                $this->currentModel .
392
                "\n# end graphql",
393
        ];
394
395
        if ($this->mode === self::MODE_CREATE) {
396
            $context['dummyCode'] = join("\n            ", $this->createCode);
397
            $context['dummyPostCreateCode'] = join("\n            ", $this->postCreateCode);
398
        } else {
399
            $context['dummyCode'] = '// TODO: write the patch please';
400
            $context['dummyPostCreateCode'] = '';
401
        }
402
403
        return $this->templateStub('migration', $context);
404
    }
405
406
    /**
407
     * creates a many-to-many morph relationship table
408
     *
409
     * @param string $type1
410
     * @param string $type2
411
     * @return string The table name.
412
     */
413
    protected function generateManyToManyMorphTable(string $name, string $relation): string
414
    {
415
        $dummyCode = <<<EOF
416
417
            \$table->unsignedBigInteger("{$name}_id");
418
            \$table->unsignedBigInteger("{$relation}_id");
419
            \$table->string("{$relation}_type");
420
EOF;
421
        $context = [
422
            'dummyCode' => $dummyCode,
423
            'dummytablename' => $this->getInflector()->pluralize($relation),
424
            'modelSchemaCode' => ''
425
        ];
426
        $contents = $this->templateStub('migration', $context);
427
428
        $item = new GeneratedItem(
429
            GeneratedItem::TYPE_MIGRATION,
430
            $contents,
431
            $this->getBasePath(
432
                'database/migrations/' .
433
                date('Y_m_d_His') .
434
                static::$counter++ . // so we keep the same order of types in schema
435
                '_' . $this->mode . '_' .
436
                $relation .
437
                '_table.php'
438
            )
439
        );
440
        $this->collection->push($item);
441
442
        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
    protected function generateManyToManyTable(string $type1, string $type2): string
453
    {
454
        $dummyCode = <<<EOF
455
456
            \$table->increments("id");
457
            \$table->unsignedBigInteger("{$type1}_id")->references('id')->on('{$type1}');
458
            \$table->unsignedBigInteger("{$type2}_id")->references('id')->on('{$type2}');
459
EOF;
460
        $context = [
461
            'dummyCode' => $dummyCode,
462
            'dummytablename' => "{$type1}_{$type2}",
463
            'modelSchemaCode' => ''
464
        ];
465
        $contents = $this->templateStub('migration', $context);
466
467
        $item = new GeneratedItem(
468
            GeneratedItem::TYPE_MIGRATION,
469
            $contents,
470
            $this->getBasePath(
471
                'database/migrations/' .
472
                date('Y_m_d_His') .
473
                static::$counter++ . // so we keep the same order of types in schema
474
                '_' . $this->mode . '_' .
475
                $type1 . '_' . $type2 .
476
                '_table.php'
477
            )
478
        );
479
        $this->collection->push($item);
480
481
        return $context['dummytablename'];
482
    }
483
484
    protected function generateFilename(string $basename): string
485
    {
486
        $this->mode = self::MODE_CREATE;
487
        $match = '_' . $basename . '_table.php';
488
489
        $basepath = $this->getBasePath('database/migrations/');
490
        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
        return $this->getBasePath(
516
            'database/migrations/' .
517
            date('Y_m_d_His') .
518
            static::$counter++ . // so we keep the same order of types in schema
519
            '_' . $this->mode . '_' .
520
            $basename . '_table.php'
521
        );
522
    }
523
}
524