Passed
Push — master ( 2d2e7e...f06b9e )
by Bruno
03:45
created

MigrationGenerator::generateString()   B

Complexity

Conditions 11
Paths 12

Size

Total Lines 48
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 11.1859

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 31
c 1
b 0
f 0
nc 12
nop 0
dl 0
loc 48
ccs 23
cts 26
cp 0.8846
crap 11.1859
rs 7.3166

How to fix   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 22
    protected function processBasetype(
124
        \GraphQL\Type\Definition\FieldDefinition $field,
125
        \GraphQL\Language\AST\NodeList $directives
126
    ): void {
127 22
        $fieldName = $field->name;
128
129 22
        if ($field->type instanceof NonNull) {
130 22
            $type = $field->type->getWrappedType();
131
        } else {
132 1
            $type = $field->type;
133
        }
134
135 22
        $codeFragment = new MigrationCodeFragment();
136
137 22
        if ($type instanceof IDType) {
138 22
            $codeFragment->appendBase('$table->bigIncrements("id")');
139 17
        } elseif ($type instanceof StringType) {
140 17
            $codeFragment->appendBase('$table->string("' . $fieldName . '")');
141 4
        } elseif ($type instanceof IntType) {
142 1
            $codeFragment->appendBase('$table->integer("' . $fieldName . '")');
143 4
        } elseif ($type instanceof BooleanType) {
144 1
            $codeFragment->appendBase('$table->boolean("' . $fieldName . '")');
145 4
        } elseif ($type instanceof FloatType) {
146 1
            $codeFragment->appendBase('$table->float("' . $fieldName . '")');
147 3
        } elseif ($type instanceof EnumType) {
148
            $this->processEnum($field, $type, $codeFragment);
149 3
        } elseif ($type instanceof UnionType) {
150
            return;
151 3
        } elseif ($type instanceof CustomScalarType) {
152 3
            $ourType = $this->parser->getScalarType($type->name);
153 3
            if (!$ourType) {
154
                throw new Exception("Invalid extended scalar type: " . get_class($type));
155
            }
156 3
            $options = []; // TODO: from directives
157 3
            $codeFragment->appendBase('$table->' . $ourType->getLaravelSQLType($fieldName, $options));
158
        } elseif ($type instanceof ListOfType) {
159
            throw new Exception("Invalid field type: " . get_class($type));
160
        } else {
161
            throw new Exception("Invalid field type: " . get_class($type));
162
        }
163
164 22
        if (!($field->type instanceof NonNull)) {
165 1
            $codeFragment->appendBase('->nullable()');
166
        }
167
168 22
        foreach ($directives as $directive) {
169 2
            $name = $directive->name->value;
170 2
            if ($name === 'migrationSkip') { // special case
171
                return;
172
            }
173
174 2
            $className = $this->getDirectiveClass($name);
175 2
            if ($className) {
176 1
                $methodName = "$className::processMigrationFieldDirective";
177
                /** @phpstan-ignore-next-line */
178 1
                $methodName(
179 1
                    $this,
180
                    $field,
181
                    $directive,
182
                    $codeFragment
183
                );
184
            }
185
        }
186
187 22
        $this->createCode[] = $codeFragment->base . ';';
188 22
        $this->createCode = array_merge($this->createCode, $codeFragment->extraLines);
189 22
    }
190
191
    protected function processEnum(
192
        \GraphQL\Type\Definition\FieldDefinition $field,
193
        EnumType $type,
194
        MigrationCodeFragment $codeFragment
195
    ): void {
196
        $fieldName = $field->name;
197
        $ourType = $this->parser->getScalarType($type->name);
198
        $parsedValues = $type->config['values'];
199
200
        if (!$ourType) {
201
            $parsedKeys = array_keys($parsedValues);
202
            $enumValues = array_combine($parsedKeys, $parsedKeys);
203
204
            // let's create this for the user
205
            $code = DatatypeFactory::generate(
206
                $type->name,
207
                'enum',
208
                'App\\Datatypes',
209
                'Tests\\Unit',
210
                function (ClassType $enumClass) use ($enumValues) {
211
                    $enumClass->addConstant('CHOICES', $enumValues);
212
                    $enumClass->getMethod('__construct')->addBody('$this->choices = self::CHOICES;');
213
                }
214
            );
215
    
216
            $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

216
            $path = /** @scrutinizer ignore-call */ base_path('app/Datatypes');
Loading history...
217
            $lowerTypeName = mb_strtolower($type->name);
218
219
            $retval = DatatypeFactory::generateFile(
220
                $code,
221
                $path,
222
                base_path('tests/Unit/')
223
            );
224
225
            $php = \Modelarium\Util::generateLighthouseTypeFile($lowerTypeName, 'App\\Datatypes\\Types');
226
            $filename = $path . "/Types/Datatype_{$lowerTypeName}.php";
227
            if (!is_dir($path . "/Types")) {
228
                \Safe\mkdir($path . "/Types", 0777, true);
229
            }
230
            \Safe\file_put_contents($filename, $php);
231
    
232
            // recreate scalars
233
            \Modelarium\Util::generateScalarFile('App\\Datatypes', base_path('graphql/types.graphql'));
234
235
            // load php files that were just created
236
            require_once($retval['filename']);
237
            require_once($filename);
238
            $this->parser->appendScalar($type->name, 'App\\Datatypes\\Types\\Datatype_' . $lowerTypeName);
239
            $ourType = $this->parser->getScalarType($type->name);
240
        }
241
        if (!($ourType instanceof FormulariumScalarType)) {
242
            throw new Exception("Enum {$type->name} {$fieldName} is not a FormulariumScalarType");
243
        }
244
245
        /**
246
         * @var FormulariumScalarType $ourType
247
         */
248
        /**
249
         * @var Datatype_enum $ourDatatype
250
         */
251
        $ourDatatype = $ourType->getDatatype();
252
        $currentChoices = $ourDatatype->getChoices();
253
        if (array_diff_key($currentChoices, $parsedValues) || array_diff_key($parsedValues, $currentChoices)) {
254
            // TODO???
255
            $this->warn('Enum had its possible values changed. Please review the datatype class.');
256
        }
257
258
        $options = []; // TODO: from directives
259
        $codeFragment->appendBase('$table->'  . $ourType->getLaravelSQLType($fieldName, $options));
260
    }
261
262 8
    protected function processRelationship(
263
        \GraphQL\Type\Definition\FieldDefinition $field,
264
        \GraphQL\Language\AST\NodeList $directives
265
    ): void {
266 8
        list($type, $isRequired) = Parser::getUnwrappedType($field->type);
267 8
        $typeName = $type->name;
268
269
        // special types that should be skipped.
270 8
        if ($typeName === 'Can') {
271
            return;
272
        }
273
274 8
        $codeFragment = new MigrationCodeFragment();
275
276 8
        foreach ($directives as $directive) {
277 8
            $name = $directive->name->value;
278 8
            if ($name === 'migrationSkip') {
279
                return;
280
            }
281
282 8
            $className = $this->getDirectiveClass($name);
283 8
            if ($className) {
284 8
                $methodName = "$className::processMigrationRelationshipDirective";
285
                /** @phpstan-ignore-next-line */
286 8
                $methodName(
287 8
                    $this,
288
                    $field,
289
                    $directive,
290
                    $codeFragment
291
                );
292
            }
293
        }
294
295 8
        if ($codeFragment->base) {
296 6
            if (!($field->type instanceof NonNull)) {
297
                $codeFragment->appendBase('->nullable()');
298
            }
299 6
            $this->createCode[] = '$table' . $codeFragment->base . ';';
300
        }
301
        
302 8
        $this->createCode = array_merge($this->createCode, $codeFragment->extraLines);
303 8
    }
304
305 22
    public function generateString(): string
306
    {
307 22
        foreach ($this->type->getFields() as $field) {
308 22
            $directives = $field->astNode->directives;
309
            if (
310 22
                ($field->type instanceof ObjectType) ||
311 22
                ($field->type instanceof ListOfType) ||
312 22
                ($field->type instanceof UnionType) ||
313 22
                ($field->type instanceof NonNull && (
314 22
                    ($field->type->getWrappedType() instanceof ObjectType) ||
315 22
                    ($field->type->getWrappedType() instanceof ListOfType) ||
316 22
                    ($field->type->getWrappedType() instanceof UnionType)
317
                ))
318
            ) {
319
                // relationship
320 8
                $this->processRelationship($field, $directives);
321
            } else {
322 22
                $this->processBasetype($field, $directives);
323
            }
324
        }
325
326
        assert($this->type->astNode !== null);
327
        /**
328
         * @var \GraphQL\Language\AST\NodeList|null
329
         */
330 22
        $directives = $this->type->astNode->directives;
331 22
        if ($directives) {
332 22
            $this->processTypeDirectives($directives, 'Migration');
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\BaseGenerator::processTypeDirectives(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

332
            $this->processTypeDirectives(/** @scrutinizer ignore-type */ $directives, 'Migration');
Loading history...
333
        }
334
335
        $context = [
336 22
            'dummytablename' => $this->tableName,
337
            'modelSchemaCode' => "# start graphql\n" .
338 22
                $this->currentModel .
339 22
                "\n# end graphql",
340
        ];
341
342 22
        if ($this->mode === self::MODE_CREATE) {
343 22
            $context['className'] = 'Create' . $this->studlyName;
344 22
            $context['dummyCode'] = join("\n            ", $this->createCode);
345 22
            $context['dummyPostCreateCode'] = join("\n            ", $this->postCreateCode);
346
        } else {
347
            $context['className'] = 'Patch' . $this->studlyName . date('YmdHis');
348
            $context['dummyCode'] = '// TODO: write the patch please';
349
            $context['dummyPostCreateCode'] = '';
350
        }
351
352 22
        return $this->templateStub('migration', $context);
353
    }
354
355
    /**
356
     * creates a many-to-many morph relationship table
357
     *
358
     * @param string $name
359
     * @param string $relation
360
     * @return string The table name.
361
     */
362 1
    public function generateManyToManyMorphTable(string $name, string $relation): string
363
    {
364
        $dummyCode = <<<EOF
365
366 1
            \$table->unsignedBigInteger("{$name}_id");
367 1
            \$table->unsignedBigInteger("{$relation}_id");
368 1
            \$table->string("{$relation}_type");
369
EOF;
370
        $context = [
371 1
            'dummyCode' => $dummyCode,
372 1
            'dummytablename' => $this->getInflector()->pluralize($relation), // TODO: check, toTableName()?
373 1
            'modelSchemaCode' => ''
374
        ];
375 1
        $contents = $this->templateStub('migration', $context);
376
377 1
        $item = new GeneratedItem(
378 1
            GeneratedItem::TYPE_MIGRATION,
379
            $contents,
380 1
            $this->getBasePath(
381
                'database/migrations/' .
382 1
                date('Y_m_d_His') .
383 1
                str_pad((string)(static::$counter++), 3, "0", STR_PAD_LEFT) . // so we keep the same order of types in schema
384 1
                '_' . $this->mode . '_' .
385 1
                $relation .
386 1
                '_table.php'
387
            )
388
        );
389 1
        $this->collection->push($item);
390
391 1
        return $context['dummytablename'];
392
    }
393
394
    /**
395
     * creates a many-to-many relationship table
396
     *
397
     * @param string $type1
398
     * @param string $type2
399
     * @return string The table name.
400
     */
401 1
    public function generateManyToManyTable(string $type1, string $type2): string
402
    {
403
        $dummyCode = <<<EOF
404
405
            \$table->increments("id");
406 1
            \$table->unsignedBigInteger("{$type1}_id")->references('id')->on('{$type1}');
407 1
            \$table->unsignedBigInteger("{$type2}_id")->references('id')->on('{$type2}');
408
EOF;
409
        $context = [
410 1
            'dummyCode' => $dummyCode,
411 1
            'dummytablename' => "{$type1}_{$type2}",
412 1
            'className' => Str::studly($this->mode) . Str::studly($type1) . Str::studly($type2),
413 1
            'modelSchemaCode' => ''
414
        ];
415 1
        $contents = $this->templateStub('migration', $context);
416
417 1
        $item = new GeneratedItem(
418 1
            GeneratedItem::TYPE_MIGRATION,
419
            $contents,
420 1
            $this->getBasePath(
421
                'database/migrations/' .
422 1
                date('Y_m_d_His') .
423 1
                str_pad((string)(static::$counter++), 3, "0", STR_PAD_LEFT) . // so we keep the same order of types in schema
424 1
                '_' . $this->mode . '_' .
425 1
                $type1 . '_' . $type2 .
426 1
                '_table.php'
427
            )
428
        );
429 1
        $this->collection->push($item);
430
431 1
        return $context['dummytablename'];
432
    }
433
434 8
    protected function generateFilename(string $basename): string
435
    {
436 8
        $this->mode = self::MODE_CREATE;
437 8
        $match = '_' . $basename . '_table.php';
438
439 8
        $basepath = $this->getBasePath('database/migrations/');
440 8
        if (is_dir($basepath)) {
441
            $migrationFiles = \Safe\scandir($basepath);
442
            rsort($migrationFiles);
443
            foreach ($migrationFiles as $m) {
444
                if (!endsWith($m, $match)) {
445
                    continue;
446
                }
447
448
                // get source
449
                $data = \Safe\file_get_contents($basepath . '/' . $m);
450
451
                // compare with this source
452
                $model = trim(getStringBetween($data, '# start graphql', '# end graphql'));
453
454
                // if equal ignore and don't output file
455
                if ($model === $this->currentModel) {
456
                    $this->mode = self::MODE_NO_CHANGE;
457
                } else {
458
                    // else we'll generate a diff and patch
459
                    $this->mode = self::MODE_PATCH;
460
                }
461
                break;
462
            }
463
        }
464
465 8
        return $this->getBasePath(
466
            'database/migrations/' .
467 8
            date('Y_m_d_His') .
468 8
            str_pad((string)(static::$counter++), 3, "0", STR_PAD_LEFT) . // so we keep the same order of types in schema
469 8
            '_' . $this->mode . '_' .
470 8
            $basename . '_table.php'
471
        );
472
    }
473
}
474