Passed
Push — master ( 011cd7...1f2ec0 )
by Bruno
05:44
created

MigrationGenerator::generateString()   B

Complexity

Conditions 11
Paths 12

Size

Total Lines 55
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 11.2682

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 38
c 1
b 0
f 0
nc 12
nop 0
dl 0
loc 55
ccs 20
cts 23
cp 0.8696
crap 11.2682
rs 7.3166

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\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
    /**
107
     * The last migration code
108 8
     *
109 8
     * @var string
110 8
     */
111
    protected $lastMigrationCode = null;
112 8
113 8
    /**
114 8
     * Time stamp
115 8
     *
116
     * @var string
117
     */
118 8
    protected $stamp = '';
119
120 8
    public function generate(): GeneratedCollection
121
    {
122
        $this->collection = new GeneratedCollection();
123
        $this->currentModel = \GraphQL\Language\Printer::doPrint($this->type->astNode);
124
        $this->stamp = date('Y_m_d_His');
125
        $filename = $this->generateFilename($this->lowerName);
126
127
        if ($this->mode !== self::MODE_NO_CHANGE) {
128 22
            $code = $this->generateString();
129
            if ($this->checkMigrationCodeChange($code)) {
130
                $item = new GeneratedItem(
131
                    GeneratedItem::TYPE_MIGRATION,
132 22
                    $code,
133
                    $filename
134 22
                );
135 22
                $this->collection->prepend($item);
136
            }
137 1
        }
138
        return $this->collection;
139
    }
140 22
141
    /**
142 22
     * @param \GraphQL\Type\Definition\FieldDefinition $field
143 22
     * @param \GraphQL\Language\AST\NodeList<\GraphQL\Language\AST\DirectiveNode> $directives
144 17
     * @return void
145 17
     */
146 4
    protected function processBasetype(
147 1
        \GraphQL\Type\Definition\FieldDefinition $field,
148 4
        \GraphQL\Language\AST\NodeList $directives
149 1
    ): void {
150 4
        $fieldName = $field->name;
151 1
152 3
        if ($field->getType() instanceof NonNull) {
153
            $type = $field->getType()->getWrappedType();
154 3
        } else {
155
            $type = $field->getType();
156 3
        }
157 3
158 3
        $codeFragment = new MigrationCodeFragment();
159
160
        if ($type instanceof IDType) {
161 3
            $codeFragment->appendBase('$table->bigIncrements("id")');
162 3
        } elseif ($type instanceof StringType) {
163
            $codeFragment->appendBase('$table->string("' . $fieldName . '")');
164
        } elseif ($type instanceof IntType) {
165
            $codeFragment->appendBase('$table->integer("' . $fieldName . '")');
166
        } elseif ($type instanceof BooleanType) {
167
            $codeFragment->appendBase('$table->boolean("' . $fieldName . '")');
168
        } elseif ($type instanceof FloatType) {
169 22
            $codeFragment->appendBase('$table->float("' . $fieldName . '")');
170 1
        } elseif ($type instanceof EnumType) {
171
            $this->processEnum($field, $type, $codeFragment);
172
        } elseif ($type instanceof UnionType) {
173 22
            return;
174 1
        } elseif ($type instanceof CustomScalarType) {
175 1
            $ourType = $this->parser->getScalarType($type->name);
176
            if (!$ourType) {
177
                throw new Exception("Invalid extended scalar type: " . get_class($type));
178
            }
179 1
            $options = []; // TODO: from directives
180 1
            $codeFragment->appendBase('$table->' . $ourType->getLaravelSQLType($fieldName, $options));
181 1
        } elseif ($type instanceof ListOfType) {
182
            throw new Exception("Invalid field type: " . get_class($type));
183 1
        } else {
184 1
            throw new Exception("Invalid field type: " . get_class($type));
185
        }
186
187
        if (!($field->getType() instanceof NonNull)) {
188
            $codeFragment->appendBase('->nullable()');
189
        }
190
191
        foreach ($directives as $directive) {
192 22
            $name = $directive->name->value;
193 22
            if ($name === 'migrationSkip') { // special case
194 22
                return;
195
            }
196
197
            $className = $this->getDirectiveClass($name);
198
            if ($className) {
199
                $methodName = "$className::processMigrationFieldDirective";
200
                /** @phpstan-ignore-next-line */
201
                $methodName(
202
                    $this,
203
                    $field,
204
                    $directive,
205
                    $codeFragment
206
                );
207
            }
208
        }
209
210
        $this->createCode[] = $codeFragment->base . ';';
211
        $this->createCode = array_merge($this->createCode, $codeFragment->extraLines);
212
    }
213
214
    protected function processEnum(
215
        \GraphQL\Type\Definition\FieldDefinition $field,
216
        EnumType $type,
217
        MigrationCodeFragment $codeFragment
218
    ): void {
219
        $fieldName = $field->name;
220
        $ourType = $this->parser->getScalarType($type->name);
221
        $parsedValues = $type->config['values'];
222
223
        if (!$ourType) {
224
            $parsedKeys = array_keys($parsedValues);
225
            $enumValues = array_combine($parsedKeys, $parsedKeys);
226
227
            // let's create this for the user
228
            $code = DatatypeFactory::generate(
229
                $type->name,
230
                'enum',
231
                'App\\Datatypes',
232
                'Tests\\Unit',
233
                function (ClassType $enumClass) use ($enumValues) {
234
                    $enumClass->addConstant('CHOICES', $enumValues);
235
                    $enumClass->getMethod('__construct')->addBody('$this->choices = self::CHOICES;');
236
                }
237
            );
238
    
239
            $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

239
            $path = /** @scrutinizer ignore-call */ base_path('app/Datatypes');
Loading history...
240
            $lowerTypeName = mb_strtolower($type->name);
241
242
            $retval = DatatypeFactory::generateFile(
243
                $code,
244
                $path,
245
                base_path('tests/Unit/')
246
            );
247
248
            $php = \Modelarium\Util::generateLighthouseTypeFile($lowerTypeName, 'App\\Datatypes\\Types');
249
            $filename = $path . "/Types/Datatype_{$lowerTypeName}.php";
250
            if (!is_dir($path . "/Types")) {
251
                \Safe\mkdir($path . "/Types", 0777, true);
252
            }
253
            \Safe\file_put_contents($filename, $php);
254
    
255
            // recreate scalars
256
            \Modelarium\Util::generateScalarFile('App\\Datatypes', base_path('graphql/types.graphql'));
257
258
            // load php files that were just created
259
            require_once($retval['filename']);
260
            require_once($filename);
261
            $this->parser->appendScalar($type->name, 'App\\Datatypes\\Types\\Datatype_' . $lowerTypeName);
262
            $ourType = $this->parser->getScalarType($type->name);
263
        }
264
        if (!($ourType instanceof FormulariumScalarType)) {
265
            throw new Exception("Enum {$type->name} {$fieldName} is not a FormulariumScalarType");
266
        }
267
268
        /**
269
         * @var FormulariumScalarType $ourType
270
         */
271
        /**
272 8
         * @var Datatype_enum $ourDatatype
273
         */
274
        $ourDatatype = $ourType->getDatatype();
275
        $currentChoices = $ourDatatype->getChoices();
276 8
        if (array_diff_key($currentChoices, $parsedValues) || array_diff_key($parsedValues, $currentChoices)) {
277 8
            // TODO???
278
            $this->warn('Enum had its possible values changed. Please review the datatype class.');
279
        }
280 8
281
        $options = []; // TODO: from directives
282
        $codeFragment->appendBase('$table->'  . $ourType->getLaravelSQLType($fieldName, $options));
283
    }
284 8
285
    /**
286 8
     * @param \GraphQL\Type\Definition\FieldDefinition $field
287 8
     * @param \GraphQL\Language\AST\NodeList<\GraphQL\Language\AST\DirectiveNode> $directives
288 8
     * @return void
289
     */
290
    protected function processRelationship(
291
        \GraphQL\Type\Definition\FieldDefinition $field,
292 8
        \GraphQL\Language\AST\NodeList $directives
293 8
    ): void {
294 8
        list($type, $isRequired) = Parser::getUnwrappedType($field->getType());
295
        $typeName = $type->name;
296 8
297 8
        // special types that should be skipped.
298
        if ($typeName === 'Can') {
299
            return;
300
        }
301
302
        $codeFragment = new MigrationCodeFragment();
303
304
        foreach ($directives as $directive) {
305 8
            $name = $directive->name->value;
306 6
            if ($name === 'migrationSkip') {
307
                return;
308
            }
309 6
310
            $className = $this->getDirectiveClass($name);
311
            if ($className) {
312 8
                $methodName = "$className::processMigrationRelationshipDirective";
313 8
                /** @phpstan-ignore-next-line */
314
                $methodName(
315 22
                    $this,
316
                    $field,
317 22
                    $directive,
318 22
                    $codeFragment
319 22
                );
320
            }
321 22
        }
322 22
323 22
        if ($codeFragment->base) {
324 22
            if (!($field->getType() instanceof NonNull)) {
325 22
                $codeFragment->appendBase('->nullable()');
326 22
            }
327 22
            $this->createCode[] = '$table' . $codeFragment->base . ';';
328
        }
329
        
330
        $this->createCode = array_merge($this->createCode, $codeFragment->extraLines);
331 8
    }
332
333 22
    public function generateString(): string
334
    {
335
        foreach ($this->type->getFields() as $field) {
336
            $directives = $field->astNode->directives;
337
            $type = $field->getType();
338
            if (
339
                ($type instanceof ObjectType) ||
340
                ($type instanceof ListOfType) ||
341 22
                ($type instanceof UnionType) ||
342 22
                ($type instanceof NonNull && (
343 22
                    ($type->getWrappedType() instanceof ObjectType) ||
344
                    ($type->getWrappedType() instanceof ListOfType) ||
345
                    ($type->getWrappedType() instanceof UnionType)
346
                ))
347 22
            ) {
348
                // relationship
349 22
                $this->processRelationship($field, $directives);
350 22
            } else {
351
                $this->processBasetype($field, $directives);
352
            }
353 22
        }
354 22
355 22
        assert($this->type->astNode !== null);
356 22
        /**
357
         * @var \GraphQL\Language\AST\NodeList<\GraphQL\Language\AST\DirectiveNode>|null
358
         */
359
        $directives = $this->type->astNode->directives;
360
        if ($directives) {
0 ignored issues
show
introduced by
$directives is of type GraphQL\Language\AST\NodeList, thus it always evaluated to true.
Loading history...
361
            $this->processTypeDirectives($directives, 'Migration');
362
        }
363 22
364
        $context = [
365
            'dummytablename' => $this->tableName,
366
            'modelSchemaCode' => "# start graphql\n" .
367
                $this->currentModel .
368
                "\n# end graphql",
369
        ];
370
371
        if ($this->mode === self::MODE_CREATE) {
372
            $context['className'] = 'Create' . $this->studlyName;
373 1
            $context['upOperation'] = 'create';
374
            $context['downOperation'] = 'dropIfExists';
375
            $context['dummyCode'] = join("\n            ", $this->createCode);
376
            $context['dummyInverseCode'] = null;
377 1
            $context['dummyPostCreateCode'] = join("\n            ", $this->postCreateCode);
378 1
        } else {
379 1
            $context['className'] = 'Patch' . $this->studlyName . str_replace('_', '', $this->stamp);
380
            $context['upOperation'] = 'table';
381
            $context['downOperation'] = 'table';
382 1
            $context['dummyCode'] = '// TODO: write the patch please';
383 1
            $context['dummyInverseCode'] = '// TODO: write the inverse patch please';
384 1
            $context['dummyPostCreateCode'] = '';
385
        }
386 1
387
        return $this->templateStub('migration', $context);
388 1
    }
389 1
390
    /**
391 1
     * creates a many-to-many morph relationship table
392
     *
393 1
     * @param string $name
394 1
     * @param string $relation
395 1
     * @return string The table name.
396 1
     */
397 1
    public function generateManyToManyMorphTable(string $name, string $relation): string
398
    {
399
        $dummyCode = <<<EOF
400 1
401
            \$table->unsignedBigInteger("{$name}_id");
402 1
            \$table->unsignedBigInteger("{$relation}_id");
403
            \$table->string("{$relation}_type");
404
EOF;
405
        $context = [
406
            'dummyCode' => $dummyCode,
407
            'upOperation' => 'create',
408
            'downOperation' => 'dropIfExists',
409
            'dummytablename' => $this->getInflector()->pluralize($relation), // TODO: check, toTableName()?
410
            'modelSchemaCode' => ''
411
        ];
412 1
        $contents = $this->templateStub('migration', $context);
413
414
        $item = new GeneratedItem(
415
            GeneratedItem::TYPE_MIGRATION,
416
            $contents,
417 1
            $this->getBasePath(
418 1
                'database/migrations/' .
419
                $this->stamp .
420
                str_pad((string)(static::$counter++), 3, "0", STR_PAD_LEFT) . // so we keep the same order of types in schema
421 1
                '_' . $this->mode . '_' .
422 1
                $relation .
423 1
                '_table.php'
424 1
            )
425
        );
426 1
        $this->collection->push($item);
427
428 1
        return $context['dummytablename'];
429 1
    }
430
431 1
    /**
432
     * creates a many-to-many relationship table
433 1
     *
434 1
     * @param string $type1
435 1
     * @param string $type2
436 1
     * @return string The table name.
437 1
     */
438
    public function generateManyToManyTable(string $type1, string $type2): string
439
    {
440 1
        $dummyCode = <<<EOF
441
442 1
            \$table->increments("id");
443
            \$table->unsignedBigInteger("{$type1}_id")->references('id')->on('{$type1}');
444
            \$table->unsignedBigInteger("{$type2}_id")->references('id')->on('{$type2}');
445 8
EOF;
446
        $context = [
447 8
            'dummyCode' => $dummyCode,
448 8
            'upOperation' => 'create',
449
            'downOperation' => 'dropIfExists',
450 8
            'dummytablename' => "{$type1}_{$type2}",
451 8
            'className' => Str::studly($this->mode) . Str::studly($type1) . Str::studly($type2),
452
            'modelSchemaCode' => ''
453
        ];
454
        $contents = $this->templateStub('migration', $context);
455
456
        $item = new GeneratedItem(
457
            GeneratedItem::TYPE_MIGRATION,
458
            $contents,
459
            $this->getBasePath(
460
                'database/migrations/' .
461
                $this->stamp .
462
                str_pad((string)(static::$counter++), 3, "0", STR_PAD_LEFT) . // so we keep the same order of types in schema
463
                '_' . $this->mode . '_' .
464
                $type1 . '_' . $type2 .
465
                '_table.php'
466
            )
467
        );
468
        $this->collection->push($item);
469
470
        return $context['dummytablename'];
471
    }
472
473
    protected function generateFilename(string $basename): string
474
    {
475
        $this->mode = self::MODE_CREATE;
476 8
        $match = '/(patch|create)_' . preg_quote($basename) . '_(table|[0-9])/';
477
478 8
        $basepath = $this->getBasePath('database/migrations/');
479 8
        if (is_dir($basepath)) {
480 8
            $migrationFiles = \Safe\scandir($basepath);
481 8
            rsort($migrationFiles);
482
            foreach ($migrationFiles as $m) {
483
                if (!preg_match($match, $m)) {
484
                    continue;
485
                }
486
487
                // get source
488
                $this->lastMigrationCode = \Safe\file_get_contents($basepath . '/' . $m);
489
490
                // compare with this source
491
                $model = trim(getStringBetween($this->lastMigrationCode, '# start graphql', '# end graphql'));
492
493
                // if equal ignore and don't output file
494
                if ($model === trim($this->currentModel)) {
495
                    $this->mode = self::MODE_NO_CHANGE;
496
                } else {
497
                    // else we'll generate a diff and patch
498
                    $this->mode = self::MODE_PATCH;
499
                }
500
                break;
501
            }
502
        }
503
504
        return $this->getBasePath(
505
            'database/migrations/' .
506
            $this->stamp .
507
            str_pad((string)(static::$counter++), 3, "0", STR_PAD_LEFT) . // so we keep the same order of types in schema
508
            '_' . $this->mode . '_' .
509
            $basename . '_' .
510
            str_replace('_', '', $this->stamp) . '_' .
511
            'table' .
512
            '.php'
513
        );
514
    }
515
516
    /**
517
     * Compares with the latest migration
518
     *
519
     * @param string $newcode
520
     * @return boolean
521
     */
522
    protected function checkMigrationCodeChange(string $newcode): bool
523
    {
524
        if (!$this->lastMigrationCode) {
525
            return true;
526
        }
527
        $tokens = token_get_all($this->lastMigrationCode);
528
        for ($i=0,$z=count($tokens); $i<$z; $i++) {
529
            if (is_array($tokens[$i]) && $tokens[$i] === T_FUNCTION
530
                && is_array($tokens[$i+1]) && $tokens[$i+1][0] == T_WHITESPACE
531
                && is_array($tokens[$i+2]) && $tokens[$i+2][1] == 'up'
532
            ) {
533
                $accumulator = [];
534
                // collect tokens from function head through opening brace
535
                while ($tokens[$i] != '{' && ($i < $z)) {
536
                    $accumulator[] = is_array($tokens[$i]) ? $tokens[$i][1] : $tokens[$i];
537
                    $i++;
538
                }
539
                if ($i == $z) {
540
                    // handle error
541
                } else {
542
                    // note, accumulate, and position index past brace
543
                    $braceDepth = 1;
544
                    $accumulator[] = '{';
545
                    $i++;
546
                }
547
                while ($braceDepth > 0 && ($i < $z)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $braceDepth does not seem to be defined for all execution paths leading up to this point.
Loading history...
548
                    if (is_array($tokens[$i])) {
549
                        $accumulator[] = $tokens[$i][1];
550
                    } else {
551
                        $accumulator[] = $tokens[$i];
552
                        if ($tokens[$i] == '{') {
553
                            $braceDepth++;
554
                        } elseif ($tokens[$i] == '}') {
555
                            $braceDepth--;
556
                        }
557
                    }
558
                }
559
                $functionSrc = implode(null, $accumulator);
0 ignored issues
show
Bug introduced by
null of type null is incompatible with the type string expected by parameter $glue of implode(). ( Ignorable by Annotation )

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

559
                $functionSrc = implode(/** @scrutinizer ignore-type */ null, $accumulator);
Loading history...
560
                var_dump($functionSrc, $newcode);
0 ignored issues
show
Security Debugging Code introduced by
var_dump($functionSrc, $newcode) looks like debug code. Are you sure you do not want to remove it?
Loading history...
561
                if ($functionSrc == $newcode) {
562
                    return false;
563
                }
564
            }
565
        }
566
567
        return true;
568
    }
569
}
570