Passed
Push — master ( 1dbc05...713241 )
by Bruno
03:36
created

MigrationGenerator::generateString()   C

Complexity

Conditions 12
Paths 18

Size

Total Lines 59
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 29
CRAP Score 12.725

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 12
eloc 41
c 2
b 0
f 0
nc 18
nop 0
dl 0
loc 59
ccs 29
cts 35
cp 0.8286
crap 12.725
rs 6.9666

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 Formularium\CodeGenerator\LaravelEloquent\CodeGenerator as LaravelCodeGenerator;
6
use Formularium\Datatype;
7
use Formularium\Datatype\Datatype_enum;
8
use Formularium\Exception\ClassNotFoundException;
9
use Formularium\Factory\DatatypeFactory;
10
use Formularium\Field;
11
use Illuminate\Support\Str;
12
use GraphQL\Language\AST\DirectiveNode;
13
use GraphQL\Type\Definition\BooleanType;
14
use GraphQL\Type\Definition\CustomScalarType;
15
use GraphQL\Type\Definition\Directive;
16
use GraphQL\Type\Definition\EnumType;
17
use GraphQL\Type\Definition\FloatType;
18
use GraphQL\Type\Definition\IDType;
19
use GraphQL\Type\Definition\IntType;
20
use GraphQL\Type\Definition\ListOfType;
21
use GraphQL\Type\Definition\NonNull;
22
use GraphQL\Type\Definition\ObjectType;
23
use GraphQL\Type\Definition\StringType;
24
use GraphQL\Type\Definition\UnionType;
25
use Modelarium\BaseGenerator;
26
use Modelarium\Exception\Exception;
27
use Modelarium\Exception\ScalarNotFoundException;
28
use Modelarium\GeneratedCollection;
29
use Modelarium\GeneratedItem;
30
use Modelarium\Parser;
31
use Modelarium\Types\FormulariumScalarType;
32
use Nette\PhpGenerator\ClassType;
33
34
use function Safe\array_combine;
35
use function Safe\rsort;
36
use function Safe\date;
37
use function Safe\preg_match;
38
39
function getStringBetween(string $string, string $start, string $end): string
40
{
41
    $ini = mb_strpos($string, $start);
42
    if ($ini === false) {
43
        return '';
44
    }
45
    $ini += mb_strlen($start);
46
    $len = mb_strpos($string, $end, $ini) - $ini;
47
    return mb_substr($string, $ini, $len);
48
}
49
50
function endsWith(string $haystack, string $needle): bool
51
{
52
    return substr_compare($haystack, $needle, -strlen($needle)) === 0;
53
}
54
class MigrationGenerator extends BaseGenerator
55
{
56
    /**
57
     * @var string
58
     */
59
    protected $stubDir = __DIR__ . "/stubs/";
60
61
    protected const MODE_CREATE = 'create';
62
    protected const MODE_PATCH = 'patch';
63
    protected const MODE_NO_CHANGE = 'nochange';
64
65
    /**
66
     * Unique counter
67
     *
68
     * @var integer
69
     */
70
    public static $counter = 0;
71
72
    /**
73
     * @var ObjectType
74
     */
75
    protected $type = null;
76
77
    /**
78
     * @var GeneratedCollection
79
     */
80
    protected $collection = null;
81
82
    /**
83
     * Code used in the create() call
84
     *
85
     * @var string[]
86
     */
87
    public $createCode = [];
88
89
    /**
90
     * Code used post the create() call
91
     *
92
     * @var string[]
93
     */
94
    public $postCreateCode = [];
95
96
    /**
97
     * 'create' or 'patch'
98
     *
99
     * @var string
100
     */
101
    protected $mode = self::MODE_CREATE;
102
103
    /**
104
     * Code used in the create() call
105
     *
106
     * @var string
107
     */
108
    protected $currentModel = '';
109
110
    /**
111
     * The last migration code
112
     *
113
     * @var string
114
     */
115
    protected $lastMigrationCode = null;
116
117
    /**
118
     * Time stamp
119
     *
120
     * @var string
121
     */
122
    protected $stamp = '';
123
124 8
    public function generate(): GeneratedCollection
125
    {
126 8
        $this->collection = new GeneratedCollection();
127 8
        $this->currentModel = \GraphQL\Language\Printer::doPrint($this->type->astNode);
128 8
        $this->stamp = date('Y_m_d_His');
129 8
        $filename = $this->generateFilename($this->lowerName);
130
131 8
        if ($this->mode !== self::MODE_NO_CHANGE) {
132 8
            $code = $this->generateString();
133 8
            if ($this->checkMigrationCodeChange($code)) {
134 8
                $item = new GeneratedItem(
135 8
                    GeneratedItem::TYPE_MIGRATION,
136
                    $code,
137
                    $filename
138
                );
139 8
                $this->collection->prepend($item);
140
            }
141
        }
142 8
        return $this->collection;
143
    }
144
145
    /**
146
     * @param \GraphQL\Type\Definition\FieldDefinition $field
147
     * @param \GraphQL\Language\AST\NodeList<\GraphQL\Language\AST\DirectiveNode> $directives
148
     * @return void
149
     */
150 22
    protected function processBasetype(
151
        \GraphQL\Type\Definition\FieldDefinition $field,
152
        \GraphQL\Language\AST\NodeList $directives
153
    ): void {
154 22
        $fieldName = $field->name;
155
156 22
        $required = false;
157 22
        if ($field->getType() instanceof NonNull) {
158 22
            $required = true;
159 22
            $type = $field->getType()->getWrappedType();
160
        } else {
161 1
            $type = $field->getType();
162
        }
163
164 22
        $codeFragment = new MigrationCodeFragment();
165 22
        $lcg = new LaravelCodeGenerator();
166 22
        $formulariumField = null;
167
168 22
        if ($type instanceof IDType) {
169 22
            $codeFragment->appendBase('$table->bigIncrements("id")');
170 17
        } elseif ($type instanceof StringType) {
171 17
            $formulariumField = new Field(
172 17
                $fieldName,
173 17
                'string',
174 17
                [],
175 17
                [ Datatype::REQUIRED => ['value' => $required ] ]
176
            );
177 4
        } elseif ($type instanceof IntType) {
178 1
            $formulariumField = new Field(
179 1
                $fieldName,
180 1
                'integer',
181 1
                [],
182 1
                [ Datatype::REQUIRED => ['value' => $required ] ]
183
            );
184 4
        } elseif ($type instanceof BooleanType) {
185 1
            $formulariumField = new Field(
186 1
                $fieldName,
187 1
                'boolean',
188 1
                [],
189 1
                [ Datatype::REQUIRED => ['value' => $required ] ]
190
            );
191 4
        } elseif ($type instanceof FloatType) {
192 1
            $formulariumField = new Field(
193 1
                $fieldName,
194 1
                'float',
195 1
                [],
196 1
                [ Datatype::REQUIRED => ['value' => $required ] ]
197
            );
198 3
        } elseif ($type instanceof EnumType) {
199
            $this->processEnum($field, $type, $codeFragment);
200 3
        } elseif ($type instanceof UnionType) {
201
            return;
202 3
        } elseif ($type instanceof CustomScalarType) {
203 3
            $ourType = $this->parser->getScalarType($type->name);
204 3
            if (!$ourType) {
205
                throw new Exception("Null scalar type: " . get_class($type));
206 3
            } elseif (!is_a($ourType, FormulariumScalarType::class) &&
207 3
                !is_a($ourType, \Modelarium\Types\ScalarType::class)
208
            ) {
209
                throw new Exception("Invalid extended scalar type: " . get_class($type));
210
            }
211
            /**
212
             * @var FormulariumScalarType $ourType
213
             */
214 3
            $formulariumField = new Field(
215 3
                $fieldName,
216 3
                $ourType->getDatatype(),
217 3
                [],
218 3
                [ Datatype::REQUIRED => ['value' => $required ] ]
219
            );
220
        } elseif ($type instanceof ListOfType) {
221
            throw new Exception("Invalid field type: " . get_class($type));
222
        } else {
223
            throw new Exception("Invalid field type: " . get_class($type));
224
        }
225
    
226 22
        if ($formulariumField) {
227 17
            $fieldList = $lcg->field($formulariumField);
228 17
            foreach (is_array($fieldList) ? $fieldList : [$fieldList] as $f) {
229 17
                $codeFragment->appendBase(
230 17
                    '$table->' . $f
231
                );
232
            }
233 22
        } elseif (!($field->getType() instanceof NonNull)) {
234
            $codeFragment->appendBase('->nullable()');
235
        }
236
237 22
        foreach ($directives as $directive) {
238 1
            $name = $directive->name->value;
239 1
            if ($name === 'migrationSkip') { // special case
240
                return;
241
            }
242
243 1
            $className = $this->getDirectiveClass($name);
244 1
            if ($className) {
245 1
                $methodName = "$className::processMigrationFieldDirective";
246
                /** @phpstan-ignore-next-line */
247 1
                $methodName(
248 1
                    $this,
249
                    $field,
250
                    $directive,
251
                    $codeFragment
252
                );
253
            }
254
        }
255
256 22
        $this->createCode[] = $codeFragment->base . ';';
257 22
        $this->createCode = array_merge($this->createCode, $codeFragment->extraLines);
258 22
    }
259
260
    protected function processEnum(
261
        \GraphQL\Type\Definition\FieldDefinition $field,
262
        EnumType $type,
263
        MigrationCodeFragment $codeFragment
264
    ): void {
265
        $fieldName = $field->name;
266
        $ourType = $this->parser->getScalarType($type->name);
267
        $parsedValues = $type->config['values'];
268
269
        if (!$ourType) {
270
            $parsedKeys = array_keys($parsedValues);
271
            $enumValues = array_combine($parsedKeys, $parsedKeys);
272
273
            // let's create this for the user
274
            $code = DatatypeFactory::generate(
275
                $type->name,
276
                'enum',
277
                'App\\Datatypes',
278
                'Tests\\Unit',
279
                function (ClassType $enumClass) use ($enumValues) {
280
                    $enumClass->addConstant('CHOICES', $enumValues);
281
                    $enumClass->getMethod('__construct')->addBody('$this->choices = self::CHOICES;');
282
                }
283
            );
284
    
285
            $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

285
            $path = /** @scrutinizer ignore-call */ base_path('app/Datatypes');
Loading history...
286
287
            $retval = DatatypeFactory::generateFile(
288
                $code,
289
                $path,
290
                base_path('tests/Unit/')
291
            );
292
293
            $php = \Modelarium\Util::generateLighthouseTypeFile($type->name, 'App\\Datatypes\\Types');
294
            $filename = $path . "/Types/Datatype_{$type->name}.php";
295
            if (!is_dir($path . "/Types")) {
296
                \Safe\mkdir($path . "/Types", 0777, true);
297
            }
298
            \Safe\file_put_contents($filename, $php);
299
    
300
            // recreate scalars
301
            \Modelarium\Util::generateScalarFile('App\\Datatypes', base_path('graphql/types.graphql'));
302
303
            // load php files that were just created
304
            require_once($retval['filename']);
305
            require_once($filename);
306
            $this->parser->appendScalar($type->name, 'App\\Datatypes\\Types\\Datatype_' . $type->name);
307
            $ourType = $this->parser->getScalarType($type->name);
308
        }
309
        if (!($ourType instanceof FormulariumScalarType)) {
310
            throw new Exception("Enum {$type->name} {$fieldName} is not a FormulariumScalarType");
311
        }
312
313
        /**
314
         * @var FormulariumScalarType $ourType
315
         */
316
        /**
317
         * @var Datatype_enum $ourDatatype
318
         */
319
        $ourDatatype = $ourType->getDatatype();
320
        $currentChoices = $ourDatatype->getChoices();
321
        if (array_diff_key($currentChoices, $parsedValues) || array_diff_key($parsedValues, $currentChoices)) {
322
            // TODO???
323
            $this->warn('Enum had its possible values changed. Please review the datatype class.');
324
        }
325
326
        $lcg = new LaravelCodeGenerator();
327
        $fieldList = $lcg->field(
328
            new Field($fieldName, $ourType->getDatatype())
329
        );
330
        foreach (is_array($fieldList) ? $fieldList : [$fieldList] as $f) {
331
            $codeFragment->appendBase(
332
                '$table->' . $f
333
            );
334
        }
335
    }
336
337
    /**
338
     * @param \GraphQL\Type\Definition\FieldDefinition $field
339
     * @param \GraphQL\Language\AST\NodeList<\GraphQL\Language\AST\DirectiveNode> $directives
340
     * @return void
341
     */
342 8
    protected function processRelationship(
343
        \GraphQL\Type\Definition\FieldDefinition $field,
344
        \GraphQL\Language\AST\NodeList $directives
345
    ): void {
346 8
        list($type, $isRequired) = Parser::getUnwrappedType($field->getType());
347 8
        $typeName = $type->name;
348
349
        // special types that should be skipped.
350 8
        if ($typeName === 'Can') {
351
            return;
352
        }
353
354 8
        $codeFragment = new MigrationCodeFragment();
355
356 8
        foreach ($directives as $directive) {
357 8
            $name = $directive->name->value;
358 8
            if ($name === 'migrationSkip') {
359
                return;
360
            }
361
362 8
            $className = $this->getDirectiveClass($name);
363 8
            if ($className) {
364 8
                $methodName = "$className::processMigrationRelationshipDirective";
365
                /** @phpstan-ignore-next-line */
366 8
                $methodName(
367 8
                    $this,
368
                    $field,
369
                    $directive,
370
                    $codeFragment
371
                );
372
            }
373
        }
374
375 8
        if ($codeFragment->base) {
376 6
            if (!($field->getType() instanceof NonNull)) {
377
                $codeFragment->appendBase('->nullable()');
378
            }
379 6
            $this->createCode[] = '$table' . $codeFragment->base . ';';
380
        }
381
        
382 8
        $this->createCode = array_merge($this->createCode, $codeFragment->extraLines);
383 8
    }
384
385 22
    public function generateString(): string
386
    {
387 22
        foreach ($this->type->getFields() as $field) {
388 22
            $directives = $field->astNode->directives;
389 22
            $type = $field->getType();
390
            if (
391 22
                ($type instanceof ObjectType) ||
392 22
                ($type instanceof ListOfType) ||
393 22
                ($type instanceof UnionType) ||
394 22
                ($type instanceof NonNull && (
395 22
                    ($type->getWrappedType() instanceof ObjectType) ||
396 22
                    ($type->getWrappedType() instanceof ListOfType) ||
397 22
                    ($type->getWrappedType() instanceof UnionType)
398
                ))
399
            ) {
400
                // relationship
401 8
                $this->processRelationship($field, $directives);
402
            } else {
403 22
                $this->processBasetype($field, $directives);
404
            }
405
        }
406
407
        assert($this->type->astNode !== null);
408
        /**
409
         * @var \GraphQL\Language\AST\NodeList<\GraphQL\Language\AST\DirectiveNode>|null
410
         */
411 22
        $directives = $this->type->astNode->directives;
412 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...
413 22
            $this->processTypeDirectives($directives, 'Migration');
414
        }
415
416
        $context = [
417 22
            'dummytablename' => $this->tableName,
418
            'modelSchemaCode' => "# start graphql\n" .
419 22
                $this->currentModel .
420 22
                "\n# end graphql",
421
        ];
422
423 22
        if ($this->mode === self::MODE_CREATE) {
424 22
            if ($this->lowerName == 'user') {
425 20
                $context['className'] = 'CreateUsers';
426
            } else {
427 8
                $context['className'] = 'Create' . $this->studlyName . str_replace('_', '', $this->stamp);
428
            }
429 22
            $context['upOperation'] = 'create';
430 22
            $context['downOperation'] = 'dropIfExists';
431 22
            $context['dummyCode'] = join("\n            ", $this->createCode);
432 22
            $context['dummyInverseCode'] = null;
433 22
            $context['dummyPostCreateCode'] = join("\n            ", $this->postCreateCode);
434
        } else {
435
            $context['className'] = 'Patch' . $this->studlyName . str_replace('_', '', $this->stamp);
436
            $context['upOperation'] = 'table';
437
            $context['downOperation'] = 'table';
438
            $context['dummyCode'] = '// TODO: write the patch please';
439
            $context['dummyInverseCode'] = '// TODO: write the inverse patch please';
440
            $context['dummyPostCreateCode'] = '';
441
        }
442
443 22
        return $this->templateStub('migration', $context);
444
    }
445
446
    /**
447
     * creates a many-to-many morph relationship table
448
     *
449
     * @param string $name
450
     * @param string $relation
451
     * @return string The table name.
452
     */
453 1
    public function generateManyToManyMorphTable(string $name, string $relation): string
454
    {
455
        $dummyCode = <<<EOF
456
457 1
            \$table->unsignedBigInteger("{$name}_id");
458 1
            \$table->unsignedBigInteger("{$relation}_id");
459 1
            \$table->string("{$relation}_type");
460
EOF;
461
        $context = [
462 1
            'dummyCode' => $dummyCode,
463 1
            'upOperation' => 'create',
464 1
            'downOperation' => 'dropIfExists',
465 1
            'dummytablename' => $this->getInflector()->pluralize($relation), // TODO: check, toTableName()?
466 1
            'modelSchemaCode' => ''
467
        ];
468 1
        $contents = $this->templateStub('migration', $context);
469
470 1
        $item = new GeneratedItem(
471 1
            GeneratedItem::TYPE_MIGRATION,
472
            $contents,
473 1
            $this->getBasePath(
474
                'database/migrations/' .
475 1
                $this->stamp .
476 1
                str_pad((string)(static::$counter++), 3, "0", STR_PAD_LEFT) . // so we keep the same order of types in schema
477 1
                '_' . $this->mode . '_' .
478 1
                $relation .
479 1
                '_table.php'
480
            )
481
        );
482 1
        $this->collection->push($item);
483
484 1
        return $context['dummytablename'];
485
    }
486
487
    /**
488
     * creates a many-to-many relationship table
489
     *
490
     * @param string $type1
491
     * @param string $type2
492
     * @return string The table name.
493
     */
494 1
    public function generateManyToManyTable(string $type1, string $type2): string
495
    {
496
        $dummyCode = <<<EOF
497
498
            \$table->increments("id");
499 1
            \$table->unsignedBigInteger("{$type1}_id")->references('id')->on('{$type1}');
500 1
            \$table->unsignedBigInteger("{$type2}_id")->references('id')->on('{$type2}');
501
EOF;
502
        $context = [
503 1
            'dummyCode' => $dummyCode,
504 1
            'upOperation' => 'create',
505 1
            'downOperation' => 'dropIfExists',
506 1
            'dummytablename' => "{$type1}_{$type2}",
507 1
            'className' => Str::studly($this->mode) . Str::studly($type1) . Str::studly($type2),
508 1
            'modelSchemaCode' => ''
509
        ];
510 1
        $contents = $this->templateStub('migration', $context);
511
512 1
        $item = new GeneratedItem(
513 1
            GeneratedItem::TYPE_MIGRATION,
514
            $contents,
515 1
            $this->getBasePath(
516
                'database/migrations/' .
517 1
                $this->stamp .
518 1
                str_pad((string)(static::$counter++), 3, "0", STR_PAD_LEFT) . // so we keep the same order of types in schema
519 1
                '_' . $this->mode . '_' .
520 1
                $type1 . '_' . $type2 .
521 1
                '_table.php'
522
            )
523
        );
524 1
        $this->collection->push($item);
525
526 1
        return $context['dummytablename'];
527
    }
528
529 8
    protected function generateFilename(string $basename): string
530
    {
531 8
        $this->mode = self::MODE_CREATE;
532 8
        $match = '/(patch|create)_' . preg_quote($basename) . '_(table|[0-9])/';
533
534 8
        $basepath = $this->getBasePath('database/migrations/');
535 8
        if (is_dir($basepath)) {
536
            $migrationFiles = \Safe\scandir($basepath);
537
            rsort($migrationFiles);
538
            foreach ($migrationFiles as $m) {
539
                if (!preg_match($match, $m)) {
540
                    continue;
541
                }
542
543
                // get source
544
                $this->lastMigrationCode = \Safe\file_get_contents($basepath . '/' . $m);
545
546
                // compare with this source
547
                $model = trim(getStringBetween($this->lastMigrationCode, '# start graphql', '# end graphql'));
548
549
                // if equal ignore and don't output file
550
                if ($model === trim($this->currentModel)) {
551
                    $this->mode = self::MODE_NO_CHANGE;
552
                } else {
553
                    // else we'll generate a diff and patch
554
                    $this->mode = self::MODE_PATCH;
555
                }
556
                break;
557
            }
558
        }
559
560 8
        if ($this->mode === self::MODE_CREATE && $this->lowerName === 'user') {
561 6
            return $this->getBasePath(
562 6
                'database/migrations/2014_10_12_000000_create_users_table.php'
563
            );
564
        }
565
566 8
        return $this->getBasePath(
567
            'database/migrations/' .
568 8
            $this->stamp .
569 8
            str_pad((string)(static::$counter++), 3, "0", STR_PAD_LEFT) . // so we keep the same order of types in schema
570 8
            '_' . $this->mode . '_' .
571 8
            $basename . '_' .
572 8
            str_replace('_', '', $this->stamp) . '_' .
573 8
            'table' .
574 8
            '.php'
575
        );
576
    }
577
578
    /**
579
     * Compares with the latest migration
580
     *
581
     * @param string $newcode
582
     * @return boolean
583
     */
584 8
    protected function checkMigrationCodeChange(string $newcode): bool
585
    {
586 8
        if (!$this->lastMigrationCode) {
587 8
            return true;
588
        }
589
        $tokens = token_get_all($this->lastMigrationCode);
590
        for ($i=0, $z=count($tokens); $i < $z; $i++) {
591
            if (is_array($tokens[$i]) && $tokens[$i][0] === T_FUNCTION
592
                && is_array($tokens[$i+1]) && $tokens[$i+1][0] == T_WHITESPACE
593
                && is_array($tokens[$i+2]) && $tokens[$i+2][1] == 'up'
594
            ) {
595
                $accumulator = [];
596
                $braceDepth = 0;
597
                // collect tokens from function head through opening brace
598
                while ($tokens[$i] != '{' && ($i < $z)) {
599
                    $accumulator[] = is_array($tokens[$i]) ? $tokens[$i][1] : $tokens[$i];
600
                    $i++;
601
                }
602
                if ($i == $z) {
603
                    // handle error
604
                } else {
605
                    // note, accumulate, and position index past brace
606
                    $braceDepth = 1;
607
                    $accumulator[] = '{';
608
                    $i++;
609
                }
610
                while ($braceDepth > 0 && ($i < $z)) {
611
                    if (is_array($tokens[$i])) {
612
                        $accumulator[] = $tokens[$i][1];
613
                    } else {
614
                        $accumulator[] = $tokens[$i];
615
                        if ($tokens[$i] == '{') {
616
                            $braceDepth++;
617
                        } elseif ($tokens[$i] == '}') {
618
                            $braceDepth--;
619
                        }
620
                    }
621
                    $i++;
622
                }
623
                $functionSrc = implode("", $accumulator);
624
                if ($functionSrc == $newcode) {
625
                    return false;
626
                }
627
            }
628
        }
629
630
        return true;
631
    }
632
}
633