Passed
Push — master ( 864347...46e361 )
by Bruno
09:55
created

MigrationGenerator::generateFilename()   A

Complexity

Conditions 5
Paths 2

Size

Total Lines 35
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
eloc 20
c 0
b 0
f 0
dl 0
loc 35
ccs 0
cts 0
cp 0
rs 9.2888
cc 5
nc 2
nop 1
crap 30
1
<?php declare(strict_types=1);
2
3
namespace Modelarium\Laravel\Targets;
4
5
use GraphQL\Type\Definition\BooleanType;
6
use GraphQL\Type\Definition\CustomScalarType;
7
use GraphQL\Type\Definition\EnumType;
8
use GraphQL\Type\Definition\FloatType;
9
use GraphQL\Type\Definition\IDType;
10
use GraphQL\Type\Definition\IntType;
11
use GraphQL\Type\Definition\ListOfType;
12
use GraphQL\Type\Definition\NonNull;
13
use GraphQL\Type\Definition\ObjectType;
14
use GraphQL\Type\Definition\StringType;
15
use Modelarium\Exception\Exception;
16
use Modelarium\GeneratedCollection;
17
use Modelarium\GeneratedItem;
18
use Modelarium\ScalarType as ModelariumScalarType;
0 ignored issues
show
Bug introduced by
The type Modelarium\ScalarType was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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

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