Passed
Push — master ( 1645cc...d148de )
by Bruno
06:50
created

MigrationGenerator::processRelationship()   D

Complexity

Conditions 22
Paths 64

Size

Total Lines 103
Code Lines 73

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 61
CRAP Score 22.1125

Importance

Changes 0
Metric Value
eloc 73
c 0
b 0
f 0
dl 0
loc 103
rs 4.1666
ccs 61
cts 65
cp 0.9385
cc 22
nc 64
nop 2
crap 22.1125

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
        foreach ($directives as $directive) {
207 4
            $name = $directive->name->value;
208 4
            switch ($name) {
209 4
            case 'migrationSkip':
210
                return;
211
            case 'migrationUniqueIndex':
212
                $extra[] = '$table->unique("' . $fieldName . '");';
213 5
                break;
214 1
            case 'migrationIndex':
215
                $extra[] = '$table->index("' . $fieldName . '");';
216
                break;
217 5
            case 'belongsTo':
218 4
                $targetType = $this->parser->getType($typeName);
219 4
                if (!$targetType) {
220
                    throw new Exception("Cannot get type {$typeName} as a relationship to {$this->name}");
221
                } elseif (!($targetType instanceof ObjectType)) {
222 5
                    throw new Exception("{$typeName} is not a type for a relationship to {$this->name}");
223
                }
224
                try {
225 15
                    $targetField = $targetType->getField($this->lowerName); // TODO: might have another name than lowerName
226
                } catch (\GraphQL\Error\InvariantViolation $e) {
227
                    $targetField = $targetType->getField($this->lowerNamePlural);
228 15
                }
229
230 15
                $targetDirectives = $targetField->astNode->directives;
231 5
                foreach ($targetDirectives as $targetDirective) {
232 5
                    switch ($targetDirective->name->value) {
233 5
                    case 'hasOne':
234 3
                    case 'hasMany':
235 3
                        $base = '$table->unsignedBigInteger("' . $fieldName . '")';
236 5
                    break;
237 1
                    }
238
                }
239 1
                break;
240 1
241 1
            case 'belongsToMany':
242
                $type1 = $this->lowerName;
243 1
                $type2 = $lowerName;
244 1
245 4
                // we only generate once, so use a comparison for that
246 1
                if (strcasecmp($type1, $type2) < 0) {
247 1
                    $item = $this->generateManyToManyTable($type1, $type2);
248 3
                    $this->collection->push($item);
249 3
                }
250 3
                break;
251 3
252 3
            case 'morphTo':
253 3
                $relation = Parser::getDirectiveArgumentByName($directive, 'relation', $lowerName);
254
                $base = '$table->unsignedBigInteger("' . $relation . '_id")';
255
                $extra[] = '$table->string("' . $relation . '_type")';
256
                break;
257
258 15
            case 'morphedByMany':
259
                $relation = Parser::getDirectiveArgumentByName($directive, 'relation', $lowerName);
260
                $item = $this->generateManyToManyMorphTable($this->lowerName, $relation);
261 15
                $this->collection->push($item);
262
                break;
263
264 15
            case 'migrationForeign':
265
                $arguments = array_merge(
266 15
                    [
267 15
                        'references' => 'id',
268
                        'on' => $lowerNamePlural
269 15
                    ],
270 15
                    Parser::getDirectiveArguments($directive)
271 15
                );
272 15
                $extra[] = '$table->foreign("' . $fieldName . '")' .
273
                    "->references(\"{$arguments['references']}\")" .
274
                    "->on(\"{$arguments['on']}\")" .
275
                    ($arguments['onDelete'] ? "->onDelete(\"{$arguments['onDelete']}\")" : '') .
276 5
                    ($arguments['onUpdate'] ? "->onUpdate(\"{$arguments['onUpdate']}\")" : '') .
277
                    ';';
278 15
                break;
279
            }
280
        }
281
282 15
        if ($base) {
283
            if (!($field->type instanceof NonNull)) {
284
                $base .= '->nullable()';
285
            }
286 15
            $base .= ';';
287 15
            $this->createCode[] = $base;
288 15
        }
289
        $this->createCode = array_merge($this->createCode, $extra);
290
    }
291 15
292 15
    protected function processDirectives(
293 15
        \GraphQL\Language\AST\NodeList $directives
294 15
    ): void {
295
        foreach ($directives as $directive) {
296
            $name = $directive->name->value;
297 15
            switch ($name) {
298 15
            case 'migrationSoftDeletes':
299 15
                $this->createCode[] ='$table->softDeletes();';
300 15
                break;
301
            case 'migrationPrimaryIndex':
302
                // TODO
303 15
                throw new Exception("Primary index is not implemented yet");
304 15
            case 'migrationIndex':
305 15
                $values = $directive->arguments[0]->value->values;
306 15
307
                $indexFields = [];
308 15
                foreach ($values as $value) {
309 15
                    $indexFields[] = $value->value;
310
                }
311
                if (!count($indexFields)) {
312 1
                    throw new Exception("You must provide at least one field to an index");
313
                }
314
                $this->createCode[] ='$table->index("' . implode('", "', $indexFields) .'");';
315
                break;
316
            case 'migrationSpatialIndex':
317
                $this->createCode[] ='$table->spatialIndex("' . $directive->arguments[0]->value->value .'");';
318 1
                break;
319 1
320
            case 'migrationFulltextIndex':
321
                $values = $directive->arguments[0]->value->values;
322
323
                $indexFields = [];
324 1
                foreach ($values as $value) {
325 1
                    $indexFields[] = $value->value;
326 1
                }
327 1
328
                if (!count($indexFields)) {
329
                    throw new Exception("You must provide at least one field to a full text index");
330 1
                }
331 1
                $this->postCreateCode[] = "DB::statement('ALTER TABLE " .
332 1
                    $this->lowerNamePlural .
333 1
                    " ADD FULLTEXT fulltext_index (\"" .
334
                    implode('", "', $indexFields) .
335
                    "\")');";
336 1
                break;
337 1
            case 'migrationRememberToken':
338 1
                $this->createCode[] ='$table->rememberToken();';
339 1
                break;
340
            case 'migrationTimestamps':
341 1
                $this->createCode[] ='$table->timestamps();';
342 1
                break;
343
            default:
344 1
            }
345 1
        }
346 1
    }
347 1
348
    public function generateString(): string
349 1
    {
350
        foreach ($this->type->getFields() as $field) {
351
            $directives = $field->astNode->directives;
352 5
            if (
353
                ($field->type instanceof ObjectType) ||
354
                ($field->type instanceof ListOfType) ||
355
                ($field->type instanceof UnionType) ||
356 5
                ($field->type instanceof NonNull && (
357
                    ($field->type->getWrappedType() instanceof ObjectType) ||
358
                    ($field->type->getWrappedType() instanceof ListOfType) ||
359
                    ($field->type->getWrappedType() instanceof UnionType)
360
                ))
361
            ) {
362
                // relationship
363
                $this->processRelationship($field, $directives);
364
            } else {
365
                $this->processBasetype($field, $directives);
366
            }
367
        }
368
369
        assert($this->type->astNode !== null);
370
        /**
371
         * @var \GraphQL\Language\AST\NodeList|null
372
         */
373
        $directives = $this->type->astNode->directives;
374
        if ($directives) {
375
            $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

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