Passed
Push — master ( 6f190e...89520e )
by Bruno
09:36
created

ModelGenerator::processDirectives()   C

Complexity

Conditions 12
Paths 12

Size

Total Lines 48
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 31
CRAP Score 12.0043

Importance

Changes 4
Bugs 2 Features 1
Metric Value
cc 12
eloc 32
c 4
b 2
f 1
nc 12
nop 1
dl 0
loc 48
ccs 31
cts 32
cp 0.9688
crap 12.0043
rs 6.9666

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;
6
use Formularium\Extradata;
7
use Formularium\ExtradataParameter;
8
use Formularium\Field;
9
use Formularium\Model;
10
use Illuminate\Support\Str;
11
use GraphQL\Type\Definition\ListOfType;
12
use GraphQL\Type\Definition\NonNull;
13
use GraphQL\Type\Definition\ObjectType;
14
use GraphQL\Type\Definition\UnionType;
15
use Modelarium\BaseGenerator;
16
use Modelarium\Datatypes\Datatype_relationship;
17
use Modelarium\Datatypes\RelationshipFactory;
18
use Modelarium\Exception\Exception;
19
use Modelarium\FormulariumUtils;
20
use Modelarium\GeneratedCollection;
21
use Modelarium\GeneratedItem;
22
use Modelarium\Parser;
23
use Modelarium\Types\FormulariumScalarType;
24
use Nette\PhpGenerator\Method;
25
use GraphQL\Language\AST\DirectiveNode;
26
27
class ModelGenerator extends BaseGenerator
28
{
29
    /**
30
     * @var string
31
     */
32
    protected $stubDir = __DIR__ . "/stubs/";
33
34
    /**
35
     * @var string
36
     */
37
    protected static $modelDir = 'app/Models/';
38
39
    /**
40
     * @var ObjectType
41
     */
42
    protected $type = null;
43
44
    /**
45
     * @var \Nette\PhpGenerator\ClassType
46
     */
47
    protected $class = null;
48
49
    /**
50
     * fillable attributes
51
     *
52
     * @var array
53
     */
54
    protected $fillable = [];
55
56
    /**
57
     * fillable attributes
58
     *
59
     * @var array
60
     */
61
    protected $hidden = [];
62
63
    /**
64
     * cast attributes
65
     *
66
     * @var array
67
     */
68
    protected $casts = [];
69
70
    /**
71
     *
72
     * @var string
73
     */
74
    protected $parentClassName = '\Illuminate\Database\Eloquent\Model';
75
76
    /**
77
     * fields
78
     *
79
     * @var Model
80
     */
81
    protected $fModel = null;
82
83
    /**
84
     *
85 8
     * @var array
86
     */
87 8
    protected $traits = [];
88 8
89 8
    /**
90 8
     * cast attributes
91 8
     *
92
     * @var Method
93 8
     */
94 8
    protected $methodRandom = null;
95 8
96 8
    /**
97 8
     * If true, we have timestamps on the migration.
98
     *
99
     * @var boolean
100 8
     */
101
    protected $migrationTimestamps = false;
102
103 10
    public function generate(): GeneratedCollection
104
    {
105
        $this->fModel = Model::create($this->studlyName);
106
        $x = new GeneratedCollection([
107
            new GeneratedItem(
108
                GeneratedItem::TYPE_MODEL,
109 10
                $this->generateString(),
110
                $this->getGenerateFilename()
111 10
            ),
112 10
            new GeneratedItem(
113
                GeneratedItem::TYPE_MODEL,
114
                $this->templateStub('model'),
115 10
                $this->getGenerateFilename(false),
116
                true
117 10
            )
118 10
        ]);
119
        return $x;
120 8
    }
121 8
122 8
    protected function processField(
123 8
        string $typeName,
124
        \GraphQL\Type\Definition\FieldDefinition $field,
125 5
        \GraphQL\Language\AST\NodeList $directives,
126 5
        bool $isRequired
127 5
    ): void {
128 5
        $fieldName = $field->name;
129 5
130
        if ($typeName === 'ID') {
131
            return;
132
        }
133
134
        $scalarType = $this->parser->getScalarType($typeName);
135 10
136 10
        /**
137 10
         * @var Field $field
138 10
         */
139 10
        $field = null;
140
        if (!$scalarType) {
141
            // probably another model
142
            $field = FormulariumUtils::getFieldFromDirectives(
143 10
                $fieldName,
144 10
                $typeName,
145
                $directives
146 10
            );
147
        } elseif ($scalarType instanceof FormulariumScalarType) {
148
            $field = FormulariumUtils::getFieldFromDirectives(
149
                $fieldName,
150 10
                $scalarType->getDatatype()->getName(),
151
                $directives
152 10
            );
153
        } else {
154 10
            return;
155
        }
156
157
        if ($isRequired) {
158
            $field->setValidatorOption(
159
                Datatype::REQUIRED,
160
                'value',
161
                true
162
            );
163
        }
164
165
        $this->fModel->appendField($field);
166
    }
167
168
    protected function processBasetype(
169
        \GraphQL\Type\Definition\FieldDefinition $field,
170
        \GraphQL\Language\AST\NodeList $directives
171
    ): void {
172
        $fieldName = $field->name;
173
174
        list($type, $isRequired) = Parser::getUnwrappedType($field->type);
175
176
        foreach ($directives as $directive) {
177
            $name = $directive->name->value;
178
            switch ($name) {
179
            case 'modelFillable':
180 10
                $this->fillable[] = $fieldName;
181 10
                break;
182 10
            case 'modelHidden':
183
                $this->hidden[] = $fieldName;
184 8
                break;
185
186
            case 'migrationUniqueIndex':
187
                $this->class->addMethod('from' . Str::studly($fieldName))
188 8
                    ->setPublic()
189 8
                    ->setStatic()
190
                    ->setReturnType('\\App\\Models\\' . $this->studlyName)
191 8
                    ->addComment("Factory from the $fieldName unique index")
192
                    ->setBody("return {$this->studlyName}::firstWhere('$fieldName', \$value);")
193 8
                    ->addParameter('value');
194 8
                break;
195
            
196
            case 'casts':
197 8
                foreach ($directive->arguments as $arg) {
198
                    /**
199
                     * @var \GraphQL\Language\AST\ArgumentNode $arg
200
                     */
201 8
202 8
                    $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...
203 8
204 8
                    switch ($arg->name->value) {
205
                    case 'type':
206 8
                        $this->casts[$fieldName] = $value;
207 8
                    }
208 8
                }
209 8
                break;
210 4
            }
211 4
        }
212 4
213 4
        $typeName = $type->name;
214 4
        $this->processField($typeName, $field, $directives, $isRequired);
215 4
    }
216 4
217
    protected function processRelationship(
218 8
        \GraphQL\Type\Definition\FieldDefinition $field,
219 1
        \GraphQL\Language\AST\NodeList $directives
220 1
    ): void {
221 1
        $lowerName = mb_strtolower($this->getInflector()->singularize($field->name));
222 1
        $lowerNamePlural = $this->getInflector()->pluralize($lowerName);
223 1
224 1
        $targetClass = '\\App\\Models\\' . Str::studly($this->getInflector()->singularize($field->name));
225 1
226
        list($type, $isRequired) = Parser::getUnwrappedType($field->type);
227 7
        $typeName = $type->name;
228 3
229 3
        // special types that should be skipped.
230 3
        if ($typeName === 'Can') {
231 3
            return;
232 3
        }
233 3
234
        $generateRandom = false;
235 7
        $sourceTypeName = $this->lowerName;
236 1
        $targetTypeName = $lowerName;
237 1
        $relationship = null;
238 1
        $isInverse = false;
239 1
240 1
        foreach ($directives as $directive) {
241 1
            $name = $directive->name->value;
242 1
            switch ($name) {
243
            case 'belongsTo':
244 7
                $generateRandom = true;
245 7
                $relationship = RelationshipFactory::RELATIONSHIP_ONE_TO_MANY;
246 7
                $isInverse = true;
247 3
                $this->class->addMethod($lowerName)
248 1
                    ->setPublic()
249
                    ->setReturnType('\\Illuminate\\Database\\Eloquent\\Relations\\BelongsTo')
250 2
                    ->setBody("return \$this->belongsTo($targetClass::class);");
251
                break;
252
253 3
            case 'belongsToMany':
254 3
                $generateRandom = true;
255
                $relationship = RelationshipFactory::RELATIONSHIP_MANY_TO_MANY;
256 3
                $isInverse = true;
257
                $this->class->addMethod($lowerNamePlural)
258
                    ->setPublic()
259 3
                    ->setReturnType('\\Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany')
260 3
                    ->setBody("return \$this->belongsToMany($targetClass::class);");
261 3
                break;
262 3
263 3
            case 'hasOne':
264 3
                $relationship = RelationshipFactory::RELATIONSHIP_ONE_TO_ONE;
265
                $isInverse = false;
266
                $this->class->addMethod($lowerName)
267 3
                    ->setPublic()
268
                    ->setReturnType('\\Illuminate\\Database\\Eloquent\\Relations\\HasOne')
269
                    ->setBody("return \$this->hasOne($targetClass::class);");
270
                break;
271 3
272
            case 'hasMany':
273 3
                $relationship = RelationshipFactory::RELATIONSHIP_ONE_TO_MANY;
274 3
                $isInverse = false;
275 3
                $target = $this->getInflector()->singularize($targetClass);
276
                $this->class->addMethod($lowerNamePlural)
277 7
                    ->setPublic()
278 2
                    ->setReturnType('\\Illuminate\\Database\\Eloquent\\Relations\\HasMany')
279 2
                    ->setBody("return \$this->hasMany($target::class);");
280 2
                break;
281 2
282 2
            case 'morphOne':
283 2
            case 'morphMany':
284
            case 'morphToMany':
285 5
                if ($name === 'morphOne') {
286 1
                    $relationship = RelationshipFactory::MORPH_ONE_TO_ONE;
287 1
                } else {
288
                    $relationship = RelationshipFactory::MORPH_ONE_TO_MANY;
289 1
                }
290 1
                $isInverse = false;
291 1
292
                $targetType = $this->parser->getType($typeName);
293
                if (!$targetType) {
294
                    throw new Exception("Cannot get type {$typeName} as a relationship to {$this->baseName}");
295
                } elseif (!($targetType instanceof ObjectType)) {
296
                    throw new Exception("{$typeName} is not a type for a relationship to {$this->baseName}");
297
                }
298 1
                $targetField = null;
299
                foreach ($targetType->getFields() as $subField) {
300 1
                    $subDir = Parser::getDirectives($subField->astNode->directives);
301
                    if (array_key_exists('morphTo', $subDir) || array_key_exists('morphedByMany', $subDir)) {
302
                        $targetField = $subField->name;
303 1
                        break;
304 1
                    }
305
                }
306 1
                if (!$targetField) {
307 1
                    throw new Exception("{$targetType} does not have a '@morphTo' or '@morphToMany' field");
308
                }
309
310 1
                $this->class->addMethod($field->name)
311 1
                    // TODO: return type
312 1
                    ->setPublic()
313 1
                    ->setBody("return \$this->{$name}($typeName::class, '$targetField');");
314 1
                break;
315
    
316
            case 'morphTo':
317 1
                $relationship = RelationshipFactory::MORPH_ONE_TO_MANY; // TODO
318
                $isInverse = true;
319
                $this->class->addMethod($field->name)
320 4
                    ->setReturnType('\\Illuminate\\Database\\Eloquent\\Relations\\MorphTo')
321
                    ->setPublic()
322
                    ->setBody("return \$this->morphTo();");
323 8
                break;
324
325
            case 'morphedByMany':
326
                $relationship = RelationshipFactory::MORPH_MANY_TO_MANY; // TODO
327 8
                $isInverse = true;
328
                $typeMap = $this->parser->getSchema()->getTypeMap();
329 8
       
330
                foreach ($typeMap as $name => $object) {
331 8
                    if (!($object instanceof ObjectType) || $name === 'Query' || $name === 'Mutation' || $name === 'Subscription') {
332 5
                        continue;
333 5
                    }
334 5
335 5
                    /**
336
                     * @var ObjectType $object
337
                     */
338 8
339
                    if (str_starts_with((string)$name, '__')) {
340 10
                        // internal type
341
                        continue;
342
                    }
343 10
344 1
                    foreach ($object->getFields() as $subField) {
345 1
                        $subDirectives = Parser::getDirectives($subField->astNode->directives);
346 1
347 1
                        if (!array_key_exists('morphToMany', $subDirectives)) {
348 1
                            continue;
349 1
                        }
350
351
                        $methodName = $this->getInflector()->pluralize(mb_strtolower((string)$name));
352 1
                        $this->class->addMethod($methodName)
353
                                ->setReturnType('\\Illuminate\\Database\\Eloquent\\Relations\\MorphToMany')
354
                                ->setPublic()
355 1
                                ->setBody("return \$this->morphedByMany($name::class, '$lowerName');");
356 1
                    }
357 1
                }
358 1
                break;
359
            
360
            default:
361
                break;
362
            }
363
        }
364
        if (!$relationship) {
365
            throw new Exception("Could not find a relationship in {$typeName} for {$field->name} in {$sourceTypeName}");
366
        }
367
368
        $relationshipDatatype = "relationship:" . ($isInverse ? "inverse:" : "") .
369
            "$relationship:$sourceTypeName:$targetTypeName";
370
371
        $this->processField($relationshipDatatype, $field, $directives, $isRequired);
372
373 10
        if ($generateRandom) {
374
            if ($relationship == RelationshipFactory::RELATIONSHIP_MANY_TO_MANY || $relationship == RelationshipFactory::MORPH_MANY_TO_MANY) {
375
                // TODO: do we generate it? seed should do it?
376
            } else {
377
                $this->methodRandom->addBody(
378
                    '$data["' . $lowerName . '_id"] = function () {' . "\n" .
379
                '    return factory(' . $targetClass . '::class)->create()->id;'  . "\n" .
380
                '};'
381
                );
382
            }
383
        }
384
    }
385
386
    protected function processDirectives(
387
        \GraphQL\Language\AST\NodeList $directives
388
    ): void {
389
        foreach ($directives as $directive) {
390
            $name = $directive->name->value;
391
            $this->fModel->appendExtradata(FormulariumUtils::directiveToExtradata($directive));
392
393 10
            switch ($name) {
394
            case 'migrationSoftDeletes':
395 10
                $this->traits[] = '\Illuminate\Database\Eloquent\SoftDeletes';
396 10
                break;
397 10
            case 'modelNotifiable':
398 10
                $this->traits[] = '\Illuminate\Notifications\Notifiable';
399 10
                break;
400 10
            case 'modelMustVerifyEmail':
401 10
                $this->traits[] = '\Illuminate\Notifications\MustVerifyEmail';
402
                break;
403 10
            case 'migrationRememberToken':
404 10
                $this->hidden[] = 'remember_token';
405 10
                break;
406 10
            case 'migrationTimestamps':
407
                $this->migrationTimestamps = true;
408 10
                break;
409 10
            case 'modelExtends':
410 10
                foreach ($directive->arguments as $arg) {
411
                    /**
412
                     * @var \GraphQL\Language\AST\ArgumentNode $arg
413 10
                     */
414
415 10
                    $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...
416 1
417
                    switch ($arg->name->value) {
418
                    case 'class':
419 10
                        $this->parentClassName = $value;
420 10
                    }
421 10
                }
422 10
                break;
423 10
            case 'renderable':
424
                foreach ($directive->arguments as $arg) {
425 10
                    /**
426 10
                     * @var \GraphQL\Language\AST\ArgumentNode $arg
427 10
                     */
428 10
429 10
                    $argName = $arg->name->value;
430
                    $argValue = $arg->value->value; /** @phpstan-ignore-line */
431 10
                    $this->fModel->appendRenderable($argName, $argValue);
0 ignored issues
show
Bug introduced by
The method appendRenderable() does not exist on Formularium\Model. ( Ignorable by Annotation )

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

431
                    $this->fModel->/** @scrutinizer ignore-call */ 
432
                                   appendRenderable($argName, $argValue);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
432 10
                }
433 10
                break;
434 10
            }
435 10
        }
436
    }
437 10
438 10
    public function generateString(): string
439 10
    {
440 10
        $namespace = new \Nette\PhpGenerator\PhpNamespace('App\\Models');
441 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\BelongsTo');
442 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\HasOne');
443 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\HasMany');
444
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\MorphTo');
445 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\MorphOne');
446
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\MorphToMany');
447
        $namespace->addUse('\\Illuminate\\Support\\Facades\\Auth');
448
        $namespace->addUse('\\Formularium\\Exception\\NoRandomException');
449 10
        $namespace->addUse('\\Modelarium\\Laravel\\Datatypes\\Datatype_relationship');
450 10
451 10
        $this->class = $namespace->addClass('Base' . $this->studlyName);
452 10
        $this->class
453 10
            ->addComment("This file was automatically generated by Modelarium.")
454 10
            ->setAbstract();
455
456 10
        $this->methodRandom = new Method('getRandomData');
457
        $this->methodRandom->addBody(
458 10
            '$data = static::getFormularium()->getRandom(get_called_class() . \'::getRandomFieldData\');' . "\n"
459
        );
460
461
        $this->processGraphql();
462 10
463 10
        // this might have changed
464 10
        $this->class->setExtends($this->parentClassName);
465 10
466 10
        foreach ($this->traits as $trait) {
467 10
            $this->class->addTrait($trait);
468 10
        }
469
470
        $this->class->addProperty('fillable')
471 10
            ->setProtected()
472 10
            ->setValue($this->fillable)
473 10
            ->setComment("The attributes that are mass assignable.\n@var array")
474 10
            ->setInitialized();
475 10
476 10
        $this->class->addProperty('hidden')
477 10
            ->setProtected()
478 10
            ->setValue($this->hidden)
479 10
            ->setComment("The attributes that should be hidden for arrays.\n@var array")
480 10
            ->setInitialized();
481
482
        if (!$this->migrationTimestamps) {
483 10
            $this->class->addProperty('timestamps')
484 10
                ->setPublic()
485
                ->setValue(false)
486
                ->setComment("Do not set timestamps.\n@var boolean")
487 10
                ->setInitialized();
488
        }
489 10
490 10
        if ($this->casts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->casts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
491
            $this->class->addProperty('casts')
492 10
                ->setProtected()
493 10
                ->setValue($this->casts)
494 10
                ->setComment("The attributes that should be cast.\n@var array")
495 10
                ->setInitialized();
496 10
        }
497 10
498 10
        $this->class->addMethod('getFields')
499
            ->setPublic()
500
            ->setStatic()
501
            ->setReturnType('array')
502 8
            ->addComment('@return array')
503
            ->addBody(
504 10
                "return ?;\n",
505
                [
506
                    $this->fModel->serialize()
507
                ]
508
            );
509
510
        $this->class->addMethod('getFormularium')
511 10
            ->setPublic()
512 10
            ->setStatic()
513 10
            ->setReturnType('\Formularium\Model')
514
            ->addComment('@return \Formularium\Model')
515 10
            ->addBody(
516
                '$model = \Formularium\Model::fromStruct(static::getFields());' . "\n" .
517 8
                'return $model;',
518
                [
519 8
                    //$this->studlyName,
520
                ]
521
            );
522
        
523
        $this->methodRandom
524
            ->addComment('@return array')
525
            ->setPublic()
526
            ->setStatic()
527
            ->setReturnType('array')
528
            ->addBody('return $data;');
529
        $this->class->addMember($this->methodRandom);
530
531
        $this->class->addMethod('getRandomFieldData')
532
            ->setPublic()
533
            ->setStatic()
534
            ->addComment("Filters fields and generate random data. Throw NoRandomException for fields you don't want to generate random data, or return a valid value.")
535
            ->addBody('
536
$d = $f->getDatatype();
537
if ($d instanceof Datatype_relationship) {
538
    throw new NoRandomException($f->getName());
539
}
540
return $f->getDatatype()->getRandom();')
541
            ->addParameter('f')->setType('Formularium\Field');
542
543
        // TODO perhaps we can use PolicyGenerator->policyClasses to auto generate
544
        $this->class->addMethod('getCanAttribute')
545
            ->setPublic()
546
            ->setReturnType('array')
547
            ->addComment("Returns the policy permissions for actions such as editing or deleting.\n@return \Formularium\Model")
548
            ->addBody(
549
                '$policy = new \\App\\Policies\\' . $this->studlyName . 'Policy();' . "\n" .
550
                '$user = Auth::user();' . "\n" .
551
                'return [' . "\n" .
552
                '    //[ "ability" => "create", "value" => $policy->create($user) ]' . "\n" .
553
                '];'
554
            );
555
        
556
        $printer = new \Nette\PhpGenerator\PsrPrinter;
557
        return $this->phpHeader() . $printer->printNamespace($namespace);
558
    }
559
560
    protected function processGraphql(): void
561
    {
562
        foreach ($this->type->getFields() as $field) {
563
            $directives = $field->astNode->directives;
564
            if (
565
                ($field->type instanceof ObjectType) ||
566
                ($field->type instanceof ListOfType) ||
567
                ($field->type instanceof UnionType) ||
568
                ($field->type instanceof NonNull && (
569
                    ($field->type->getWrappedType() instanceof ObjectType) ||
570
                    ($field->type->getWrappedType() instanceof ListOfType) ||
571
                    ($field->type->getWrappedType() instanceof UnionType)
572
                ))
573
            ) {
574
                // relationship
575
                $this->processRelationship($field, $directives);
576
            } else {
577
                $this->processBasetype($field, $directives);
578
            }
579
        }
580
581
        /**
582
         * @var \GraphQL\Language\AST\NodeList|null
583
         */
584
        $directives = $this->type->astNode->directives;
585
        if ($directives) {
586
            $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

586
            $this->processDirectives(/** @scrutinizer ignore-type */ $directives);
Loading history...
587
        }
588
    }
589
590
    public function getGenerateFilename(bool $base = true): string
591
    {
592
        return $this->getBasePath(self::$modelDir . '/' . ($base ? 'Base' : '') . $this->studlyName . '.php');
593
    }
594
595
    public static function setModelDir(string $dir): void
596
    {
597
        self::$modelDir = $dir;
598
    }
599
}
600