Passed
Push — master ( a266d2...fe3e3c )
by Bruno
03:11
created

MigrationGenerator::generateString()   B

Complexity

Conditions 11
Paths 12

Size

Total Lines 49
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 11.1659

Importance

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

221
            $path = /** @scrutinizer ignore-call */ base_path('app/Datatypes');
Loading history...
222
            $lowerTypeName = mb_strtolower($type->name);
223
224
            $retval = DatatypeFactory::generateFile(
225
                $code,
226
                $path,
227
                base_path('tests/Unit/')
228
            );
229
230
            $php = \Modelarium\Util::generateLighthouseTypeFile($lowerTypeName, 'App\\Datatypes\\Types');
231
            $filename = $path . "/Types/Datatype_{$lowerTypeName}.php";
232
            if (!is_dir($path . "/Types")) {
233
                \Safe\mkdir($path . "/Types", 0777, true);
234
            }
235
            \Safe\file_put_contents($filename, $php);
236
    
237
            // recreate scalars
238
            \Modelarium\Util::generateScalarFile('App\\Datatypes', base_path('graphql/types.graphql'));
239
240
            // load php files that were just created
241
            require_once($retval['filename']);
242
            require_once($filename);
243
            $this->parser->appendScalar($type->name, 'App\\Datatypes\\Types\\Datatype_' . $lowerTypeName);
244
            $ourType = $this->parser->getScalarType($type->name);
245
        }
246
        if (!($ourType instanceof FormulariumScalarType)) {
247
            throw new Exception("Enum {$type->name} {$fieldName} is not a FormulariumScalarType");
248
        }
249
250
        /**
251
         * @var FormulariumScalarType $ourType
252
         */
253
        /**
254
         * @var Datatype_enum $ourDatatype
255
         */
256
        $ourDatatype = $ourType->getDatatype();
257
        $currentChoices = $ourDatatype->getChoices();
258
        if (array_diff_key($currentChoices, $parsedValues) || array_diff_key($parsedValues, $currentChoices)) {
259
            // TODO???
260
            $this->warn('Enum had its possible values changed. Please review the datatype class.');
261
        }
262
263
        $options = []; // TODO: from directives
264
        $codeFragment->appendBase('$table->'  . $ourType->getLaravelSQLType($fieldName, $options));
265
    }
266
267
    /**
268
     * @param \GraphQL\Type\Definition\FieldDefinition $field
269
     * @param \GraphQL\Language\AST\NodeList<\GraphQL\Language\AST\DirectiveNode> $directives
270
     * @return void
271
     */
272 8
    protected function processRelationship(
273
        \GraphQL\Type\Definition\FieldDefinition $field,
274
        \GraphQL\Language\AST\NodeList $directives
275
    ): void {
276 8
        list($type, $isRequired) = Parser::getUnwrappedType($field->getType());
277 8
        $typeName = $type->name;
278
279
        // special types that should be skipped.
280 8
        if ($typeName === 'Can') {
281
            return;
282
        }
283
284 8
        $codeFragment = new MigrationCodeFragment();
285
286 8
        foreach ($directives as $directive) {
287 8
            $name = $directive->name->value;
288 8
            if ($name === 'migrationSkip') {
289
                return;
290
            }
291
292 8
            $className = $this->getDirectiveClass($name);
293 8
            if ($className) {
294 8
                $methodName = "$className::processMigrationRelationshipDirective";
295
                /** @phpstan-ignore-next-line */
296 8
                $methodName(
297 8
                    $this,
298
                    $field,
299
                    $directive,
300
                    $codeFragment
301
                );
302
            }
303
        }
304
305 8
        if ($codeFragment->base) {
306 6
            if (!($field->getType() instanceof NonNull)) {
307
                $codeFragment->appendBase('->nullable()');
308
            }
309 6
            $this->createCode[] = '$table' . $codeFragment->base . ';';
310
        }
311
        
312 8
        $this->createCode = array_merge($this->createCode, $codeFragment->extraLines);
313 8
    }
314
315 22
    public function generateString(): string
316
    {
317 22
        foreach ($this->type->getFields() as $field) {
318 22
            $directives = $field->astNode->directives;
319 22
            $type = $field->getType();
320
            if (
321 22
                ($type instanceof ObjectType) ||
322 22
                ($type instanceof ListOfType) ||
323 22
                ($type instanceof UnionType) ||
324 22
                ($type instanceof NonNull && (
325 22
                    ($type->getWrappedType() instanceof ObjectType) ||
326 22
                    ($type->getWrappedType() instanceof ListOfType) ||
327 22
                    ($type->getWrappedType() instanceof UnionType)
328
                ))
329
            ) {
330
                // relationship
331 8
                $this->processRelationship($field, $directives);
332
            } else {
333 22
                $this->processBasetype($field, $directives);
334
            }
335
        }
336
337
        assert($this->type->astNode !== null);
338
        /**
339
         * @var \GraphQL\Language\AST\NodeList<\GraphQL\Language\AST\DirectiveNode>|null
340
         */
341 22
        $directives = $this->type->astNode->directives;
342 22
        if ($directives) {
0 ignored issues
show
introduced by
$directives is of type GraphQL\Language\AST\NodeList, thus it always evaluated to true.
Loading history...
343 22
            $this->processTypeDirectives($directives, 'Migration');
344
        }
345
346
        $context = [
347 22
            'dummytablename' => $this->tableName,
348
            'modelSchemaCode' => "# start graphql\n" .
349 22
                $this->currentModel .
350 22
                "\n# end graphql",
351
        ];
352
353 22
        if ($this->mode === self::MODE_CREATE) {
354 22
            $context['className'] = 'Create' . $this->studlyName;
355 22
            $context['dummyCode'] = join("\n            ", $this->createCode);
356 22
            $context['dummyPostCreateCode'] = join("\n            ", $this->postCreateCode);
357
        } else {
358
            $context['className'] = 'Patch' . $this->studlyName . date('YmdHis');
359
            $context['dummyCode'] = '// TODO: write the patch please';
360
            $context['dummyPostCreateCode'] = '';
361
        }
362
363 22
        return $this->templateStub('migration', $context);
364
    }
365
366
    /**
367
     * creates a many-to-many morph relationship table
368
     *
369
     * @param string $name
370
     * @param string $relation
371
     * @return string The table name.
372
     */
373 1
    public function generateManyToManyMorphTable(string $name, string $relation): string
374
    {
375
        $dummyCode = <<<EOF
376
377 1
            \$table->unsignedBigInteger("{$name}_id");
378 1
            \$table->unsignedBigInteger("{$relation}_id");
379 1
            \$table->string("{$relation}_type");
380
EOF;
381
        $context = [
382 1
            'dummyCode' => $dummyCode,
383 1
            'dummytablename' => $this->getInflector()->pluralize($relation), // TODO: check, toTableName()?
384 1
            'modelSchemaCode' => ''
385
        ];
386 1
        $contents = $this->templateStub('migration', $context);
387
388 1
        $item = new GeneratedItem(
389 1
            GeneratedItem::TYPE_MIGRATION,
390
            $contents,
391 1
            $this->getBasePath(
392
                'database/migrations/' .
393 1
                date('Y_m_d_His') .
394 1
                str_pad((string)(static::$counter++), 3, "0", STR_PAD_LEFT) . // so we keep the same order of types in schema
395 1
                '_' . $this->mode . '_' .
396 1
                $relation .
397 1
                '_table.php'
398
            )
399
        );
400 1
        $this->collection->push($item);
401
402 1
        return $context['dummytablename'];
403
    }
404
405
    /**
406
     * creates a many-to-many relationship table
407
     *
408
     * @param string $type1
409
     * @param string $type2
410
     * @return string The table name.
411
     */
412 1
    public function generateManyToManyTable(string $type1, string $type2): string
413
    {
414
        $dummyCode = <<<EOF
415
416
            \$table->increments("id");
417 1
            \$table->unsignedBigInteger("{$type1}_id")->references('id')->on('{$type1}');
418 1
            \$table->unsignedBigInteger("{$type2}_id")->references('id')->on('{$type2}');
419
EOF;
420
        $context = [
421 1
            'dummyCode' => $dummyCode,
422 1
            'dummytablename' => "{$type1}_{$type2}",
423 1
            'className' => Str::studly($this->mode) . Str::studly($type1) . Str::studly($type2),
424 1
            'modelSchemaCode' => ''
425
        ];
426 1
        $contents = $this->templateStub('migration', $context);
427
428 1
        $item = new GeneratedItem(
429 1
            GeneratedItem::TYPE_MIGRATION,
430
            $contents,
431 1
            $this->getBasePath(
432
                'database/migrations/' .
433 1
                date('Y_m_d_His') .
434 1
                str_pad((string)(static::$counter++), 3, "0", STR_PAD_LEFT) . // so we keep the same order of types in schema
435 1
                '_' . $this->mode . '_' .
436 1
                $type1 . '_' . $type2 .
437 1
                '_table.php'
438
            )
439
        );
440 1
        $this->collection->push($item);
441
442 1
        return $context['dummytablename'];
443
    }
444
445 8
    protected function generateFilename(string $basename): string
446
    {
447 8
        $this->mode = self::MODE_CREATE;
448 8
        $match = '_' . $basename . '_table.php';
449
450 8
        $basepath = $this->getBasePath('database/migrations/');
451 8
        if (is_dir($basepath)) {
452
            $migrationFiles = \Safe\scandir($basepath);
453
            rsort($migrationFiles);
454
            foreach ($migrationFiles as $m) {
455
                if (!endsWith($m, $match)) {
456
                    continue;
457
                }
458
459
                // get source
460
                $data = \Safe\file_get_contents($basepath . '/' . $m);
461
462
                // compare with this source
463
                $model = trim(getStringBetween($data, '# start graphql', '# end graphql'));
464
465
                // if equal ignore and don't output file
466
                if ($model === $this->currentModel) {
467
                    $this->mode = self::MODE_NO_CHANGE;
468
                } else {
469
                    // else we'll generate a diff and patch
470
                    $this->mode = self::MODE_PATCH;
471
                }
472
                break;
473
            }
474
        }
475
476 8
        return $this->getBasePath(
477
            'database/migrations/' .
478 8
            date('Y_m_d_His') .
479 8
            str_pad((string)(static::$counter++), 3, "0", STR_PAD_LEFT) . // so we keep the same order of types in schema
480 8
            '_' . $this->mode . '_' .
481 8
            $basename . '_table.php'
482
        );
483
    }
484
}
485