Passed
Push — master ( 1645cc...d148de )
by Bruno
06:50
created

ModelGenerator   F

Complexity

Total Complexity 67

Size/Duplication

Total Lines 492
Duplicated Lines 0 %

Test Coverage

Coverage 84.76%

Importance

Changes 5
Bugs 2 Features 0
Metric Value
wmc 67
eloc 276
c 5
b 2
f 0
dl 0
loc 492
rs 3.04
ccs 89
cts 105
cp 0.8476

9 Methods

Rating   Name   Duplication   Size   Complexity  
A generate() 0 16 1
A processField() 0 41 5
B processBasetype() 0 36 7
B processDirectives() 0 29 9
B generateString() 0 88 2
A getGenerateFilename() 0 3 2
A formulariumModel() 0 16 2
F processRelationship() 0 149 29
B processGraphql() 0 27 10

How to fix   Complexity   

Complex Class

Complex classes like ModelGenerator often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ModelGenerator, and based on these observations, apply Extract Interface, too.

1
<?php declare(strict_types=1);
2
3
namespace Modelarium\Laravel\Targets;
4
5
use Formularium\Datatype;
6
use Illuminate\Support\Str;
7
use GraphQL\Type\Definition\ListOfType;
8
use GraphQL\Type\Definition\NonNull;
9
use GraphQL\Type\Definition\ObjectType;
10
use GraphQL\Type\Definition\UnionType;
11
use Modelarium\BaseGenerator;
12
use Modelarium\Datatypes\Datatype_relationship;
13
use Modelarium\Exception\Exception;
14
use Modelarium\FormulariumUtils;
15
use Modelarium\GeneratedCollection;
16
use Modelarium\GeneratedItem;
17
use Modelarium\Parser;
18
use Modelarium\Types\FormulariumScalarType;
19
use Nette\PhpGenerator\Method;
20
21
class ModelGenerator extends BaseGenerator
22
{
23
    /**
24
     * @var string
25
     */
26
    protected $stubDir = __DIR__ . "/stubs/";
27
28
    /**
29
     * @var ObjectType
30
     */
31
    protected $type = null;
32
33
    /**
34
     * @var \Nette\PhpGenerator\ClassType
35
     */
36
    protected $class = null;
37
38
    /**
39
     * fillable attributes
40
     *
41
     * @var array
42
     */
43
    protected $fillable = [];
44
45
    /**
46
     * fillable attributes
47 5
     *
48
     * @var array
49 5
     */
50 5
    protected $hidden = [];
51 5
52 5
    /**
53 5
     * cast attributes
54
     *
55 5
     * @var array
56 5
     */
57 5
    protected $casts = [];
58 5
59 5
    /**
60
     *
61
     * @var string
62 5
     */
63
    protected $parentClassName = '\Illuminate\Database\Eloquent\Model';
64
65
    /**
66
     * fields
67
     *
68
     * @var array
69
     */
70
    protected $fields = [];
71
72
    /**
73
     *
74
     * @var array
75
     */
76
    protected $traits = [];
77
78
    /**
79
     * cast attributes
80
     *
81
     * @var Method
82
     */
83
    protected $methodRandom = null;
84
85
    public function generate(): GeneratedCollection
86
    {
87
        $x = new GeneratedCollection([
88
            new GeneratedItem(
89
                GeneratedItem::TYPE_MODEL,
90
                $this->generateString(),
91
                $this->getGenerateFilename()
92
            ),
93 5
            new GeneratedItem(
94
                GeneratedItem::TYPE_MODEL,
95
                $this->templateStub('model'),
96
                $this->getGenerateFilename(false),
97 5
                true
98 5
            )
99
        ]);
100 5
        return $x;
101 5
    }
102
103 1
    protected function processField(
104
        string $typeName,
105
        \GraphQL\Type\Definition\FieldDefinition $field,
106 5
        \GraphQL\Language\AST\NodeList $directives,
107 5
        bool $isRequired
108
    ): void {
109 5
        $fieldName = $field->name;
110 5
111 5
        if ($typeName === 'ID') {
112 5
            return;
113 4
        }
114 4
115
        $scalarType = $this->parser->getScalarType($typeName);
116 4
117
        $field = null;
118
        if (!$scalarType) {
119 4
            // probably another model
120
            $field = FormulariumUtils::getFieldFromDirectives(
121 5
                $fieldName,
122 1
                $typeName,
123 1
                $directives
124
            );
125 1
        } elseif ($scalarType instanceof FormulariumScalarType) {
126
            $field = FormulariumUtils::getFieldFromDirectives(
127
                $fieldName,
128 1
                $scalarType->getDatatype()->getName(),
129
                $directives
130 4
            );
131 3
        } else {
132 3
            return;
133
        }
134 3
135
        if ($isRequired) {
136
            $field->setValidatorOption(
137 3
                Datatype::REQUIRED,
138 4
                'value',
139 1
                true
140 1
            );
141 1
        }
142
143 1
        $this->fields[$fieldName] = $field->toArray();
144
    }
145
146 1
    protected function processBasetype(
147
        \GraphQL\Type\Definition\FieldDefinition $field,
148 4
        \GraphQL\Language\AST\NodeList $directives
149
    ): void {
150
        $fieldName = $field->name;
151
152 5
        list($type, $isRequired) = Parser::getUnwrappedType($field->type);
153
154
        foreach ($directives as $directive) {
155 7
            $name = $directive->name->value;
156
            switch ($name) {
157
            case 'modelFillable':
158 7
                $this->fillable[] = $fieldName;
159 1
                break;
160
            case 'modelHidden':
161 1
                $this->hidden[] = $fieldName;
162 1
                break;
163 1
            case 'casts':
164
                foreach ($directive->arguments as $arg) {
165
                    /**
166 7
                     * @var \GraphQL\Language\AST\ArgumentNode $arg
167
                     */
168
169 7
                    $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...
170
171
                    switch ($arg->name->value) {
172 7
                    case 'type':
173
                        $this->casts[$fieldName] = $value;
174 7
                    }
175
                }
176
                break;
177 7
            }
178
        }
179 7
180 7
        $typeName = $type->name;
181 7
        $this->processField($typeName, $field, $directives, $isRequired);
182 7
    }
183
184
    protected function processRelationship(
185
        \GraphQL\Type\Definition\FieldDefinition $field,
186 5
        \GraphQL\Language\AST\NodeList $directives
187
    ): void {
188
        $lowerName = mb_strtolower($this->getInflector()->singularize($field->name));
189
        $lowerNamePlural = $this->getInflector()->pluralize($lowerName);
190
191
        $targetClass = '\\App\\Models\\' . Str::studly($this->getInflector()->singularize($field->name));
192
193
        list($type, $isRequired) = Parser::getUnwrappedType($field->type);
194
        $typeName = $type->name;
195 7
196 7
        // special types that should be skipped.
197 7
        if ($typeName === 'Can') {
198
            return;
199
        }
200 7
201 7
        $generateRandom = false;
202 7
        $sourceTypeName = $this->lowerName;
203 7
        $targetTypeName = $lowerName;
204
        $relationship = null;
205
206 7
        foreach ($directives as $directive) {
207 7
            $name = $directive->name->value;
208 7
            switch ($name) {
209 7
            case 'belongsTo':
210
                $generateRandom = true;
211
                $relationship = 'N1'; // TODO
212 7
                $this->class->addMethod($lowerName)
213 7
                    ->setPublic()
214 7
                    ->setReturnType('App\\BelongsTo')
215 7
                    ->setBody("return \$this->belongsTo($targetClass::class);");
216
                break;
217
218 7
            case 'belongsToMany':
219 7
                $generateRandom = true;
220 7
                $relationship = 'NN'; // TODO
221 7
                $this->class->addMethod($lowerNamePlural)
222
                    ->setPublic()
223
                    ->setReturnType('App\\BelongsTo')
224 7
                    ->setBody("return \$this->belongsToMany($targetClass::class);");
225 7
                break;
226 7
227 7
            case 'hasOne':
228
                $relationship = '11'; // TODO
229
                $this->class->addMethod($lowerName)
230 7
                    ->setPublic()
231 7
                    ->setReturnType('App\\HasOne')
232
                    ->setBody("return \$this->hasOne($targetClass::class);");
233
                break;
234 5
235
            case 'hasMany':
236 5
                $relationship = '1N'; // TODO, NN?
237
                $target = $this->getInflector()->singularize($targetClass);
238
                $this->class->addMethod($lowerNamePlural)
239
                    ->setPublic()
240
                    ->setReturnType('App\\HasMany')
241
                    ->setBody("return \$this->hasMany($target::class);");
242
                break;
243
244
            case 'morphOne':
245
            case 'morphMany':
246
            case 'morphToMany':
247
                if ($name === 'morphOne') {
248
                    $relationship = '11'; // Datatype_relationship::RELATIONSHIP_ONE_TO_ONE; // TODO
249
                } else {
250
                    $relationship = 'N1'; // Datatype_relationship::RELATIONSHIP_ONE_TO_MANY; // TODO
251
                }
252
253
                $targetType = $this->parser->getType($typeName);
254
                if (!$targetType) {
255
                    throw new Exception("Cannot get type {$typeName} as a relationship to {$this->name}");
256
                } elseif (!($targetType instanceof ObjectType)) {
257
                    throw new Exception("{$typeName} is not a type for a relationship to {$this->name}");
258
                }
259
                $targetField = null;
260
                foreach ($targetType->getFields() as $subField) {
261
                    $subDir = Parser::getDirectives($subField->astNode->directives);
262
                    if (array_key_exists('morphTo', $subDir) || array_key_exists('morphedByMany', $subDir)) {
263
                        $targetField = $subField->name;
264
                        break;
265
                    }
266
                }
267
                if (!$targetField) {
268
                    throw new Exception("{$targetType} does not have a '@morphTo' or '@morphToMany' field");
269
                }
270
271
                $this->class->addMethod($field->name)
272
                    ->setPublic()
273
                    ->setBody("return \$this->{$name}($typeName::class, '$targetField');");
274
                break;
275
    
276
            case 'morphTo':
277
                $relationship = 'N1'; // Datatype_relationship::RELATIONSHIP_ONE_TO_MANY; // TODO
278
                $this->class->addMethod($field->name)
279
                    ->setPublic()
280
                    ->setBody("return \$this->morphTo();");
281
                break;
282
283
            case 'morphedByMany':
284
                $relationship = 'NN';// TODO Datatype_relationship::RELATIONSHIP_MANY_TO_MANY; // TODO
285
                $typeMap = $this->parser->getSchema()->getTypeMap();
286
       
287
                foreach ($typeMap as $name => $object) {
288
                    if (!($object instanceof ObjectType) || $name === 'Query' || $name === 'Mutation' || $name === 'Subscription') {
289
                        continue;
290
                    }
291
292
                    /**
293
                     * @var ObjectType $object
294
                     */
295
296
                    if (str_starts_with((string)$name, '__')) {
297
                        // internal type
298
                        continue;
299
                    }
300
301
                    foreach ($object->getFields() as $subField) {
302
                        $subDirectives = Parser::getDirectives($subField->astNode->directives);
303
304
                        if (!array_key_exists('morphToMany', $subDirectives)) {
305
                            continue;
306
                        }
307
308
                        $methodName = $this->getInflector()->pluralize(mb_strtolower((string)$name));
309
                        $this->class->addMethod($methodName)
310
                                ->setPublic()
311
                                ->setBody("return \$this->morphedByMany($name::class, '$lowerName');");
312
                    }
313
                }
314
                break;
315
            
316
            default:
317
                break;
318
            }
319
        }
320
        if (!$relationship) {
321
            throw new Exception("Could not find a relationship in {$typeName}");
322
        }
323
324
        $relationshipDatatype = "relationship:$relationship:$sourceTypeName:$targetTypeName";
325
326
        $this->processField($relationshipDatatype, $field, $directives, $isRequired);
327
328
        if ($generateRandom) {
329
            $this->methodRandom->addBody(
330
                '$data["' . $lowerName . '_id"] = function () {' . "\n" .
331
                '    return factory(' . $targetClass . '::class)->create()->id;'  . "\n" .
332
                '};'
333
            );
334
        }
335
    }
336
337
    protected function processDirectives(
338
        \GraphQL\Language\AST\NodeList $directives
339
    ): void {
340
        foreach ($directives as $directive) {
341
            $name = $directive->name->value;
342
            switch ($name) {
343
            case 'migrationSoftDeletes':
344
                $this->traits[] = '\Illuminate\Database\Eloquent\SoftDeletes';
345
                break;
346
            case 'modelNotifiable':
347
                $this->traits[] = '\Illuminate\Notifications\Notifiable';
348
                break;
349
            case 'modelMustVerifyEmail':
350
                $this->traits[] = '\Illuminate\Notifications\MustVerifyEmail';
351
                break;
352
            case 'migrationRememberToken':
353
                $this->hidden[] = 'remember_token';
354
                break;
355
            case 'extends':
356
                foreach ($directive->arguments as $arg) {
357
                    /**
358
                     * @var \GraphQL\Language\AST\ArgumentNode $arg
359
                     */
360
361
                    $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...
362
363
                    switch ($arg->name->value) {
364
                    case 'class':
365
                        $this->parentClassName = $value;
366
                    }
367
                }
368
            }
369
        }
370
    }
371
372
    protected function formulariumModel(
373
374
    ): string {
375
        foreach ($this->fields as $f) {
376
            $string = <<<EOF
0 ignored issues
show
Unused Code introduced by
The assignment to $string is dead and can be removed.
Loading history...
377
            new \Formularium\Field(
378
                '{$f->name}',
379
                '',
380
                [ // renderable
381
                ],
382
                [ // validators
383
                ]
384
            ),
385
EOF;
386
        }
387
        return '';
388
    }
389
390
    public function generateString(): string
391
    {
392
        $namespace = new \Nette\PhpGenerator\PhpNamespace('App\\Models');
393
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\BelongsTo');
394
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\HasOne');
395
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\HasMany');
396
397
        $this->class = $namespace->addClass('Base' . $this->studlyName);
398
        $this->class->setExtends($this->parentClassName)
399
            ->addComment("This file was automatically generated by Modelarium.");
400
401
        $this->methodRandom = new Method('getRandomData');
402
        $this->methodRandom->addBody(
403
            '$data = static::getFormularium()->getRandom();' . "\n"
404
        );
405
406
        $this->processGraphql();
407
408
        foreach ($this->traits as $trait) {
409
            $this->class->addTrait($trait);
410
        }
411
412
        $this->class->addProperty('fillable')
413
            ->setProtected()
414
            ->setValue($this->fillable)
415
            ->setComment("The attributes that are mass assignable.\n@var array")
416
            ->setInitialized();
417
418
        $this->class->addProperty('hidden')
419
            ->setProtected()
420
            ->setValue($this->hidden)
421
            ->setComment("The attributes that should be hidden for arrays.\n@var array")
422
            ->setInitialized();
423
424
        $this->class->addProperty('casts')
425
            ->setProtected()
426
            ->setValue($this->casts)
427
            ->setComment("The attributes that should be cast to native types.\n@var array")
428
            ->setInitialized();
429
430
        $this->class->addMethod('getFields')
431
            ->setPublic()
432
            ->setStatic()
433
            ->setReturnType('array')
434
            ->addComment('@return array')
435
            ->addBody(
436
                "return ?;\n",
437
                [
438
                    $this->fields
439
                ]
440
            );
441
442
        $this->class->addMethod('getFormularium')
443
            ->setPublic()
444
            ->setStatic()
445
            ->setReturnType('\Formularium\Model')
446
            ->addComment('@return \Formularium\Model')
447
            ->addBody(
448
                '$model = \Formularium\Model::create(?, static::getFields());' . "\n" .
449
                'return $model;',
450
                [
451
                    $this->studlyName,
452
                ]
453
            );
454
        
455
        $this->methodRandom
456
            ->addComment('@return array')
457
            ->setPublic()
458
            ->setStatic()
459
            ->setReturnType('array')
460
            ->addBody('return $data;');
461
        $this->class->addMember($this->methodRandom);
462
463
        // TODO perhaps we can use PolicyGenerator->policyClasses to auto generate
464
        $this->class->addMethod('getCanAttribute')
465
            ->setPublic()
466
            ->setReturnType('array')
467
            ->addComment('@return \Formularium\Model')
468
            ->addBody(
469
                '$policy = new \\App\\Policies\\' . $this->studlyName . 'Policy();' . "\n" .
470
                '$user = Auth::user();' . "\n" .
471
                'return [' . "\n" .
472
                '    //[ "ability" => "create", "value" => $policy->create($user) ]' . "\n" .
473
                '];'
474
            );
475
        
476
        $printer = new \Nette\PhpGenerator\PsrPrinter;
477
        return $this->phpHeader() . $printer->printNamespace($namespace);
478
    }
479
480
    protected function processGraphql(): void
481
    {
482
        foreach ($this->type->getFields() as $field) {
483
            $directives = $field->astNode->directives;
484
            if (
485
                ($field->type instanceof ObjectType) ||
486
                ($field->type instanceof ListOfType) ||
487
                ($field->type instanceof UnionType) ||
488
                ($field->type instanceof NonNull && (
489
                    ($field->type->getWrappedType() instanceof ObjectType) ||
490
                    ($field->type->getWrappedType() instanceof ListOfType) ||
491
                    ($field->type->getWrappedType() instanceof UnionType)
492
                ))
493
            ) {
494
                // relationship
495
                $this->processRelationship($field, $directives);
496
            } else {
497
                $this->processBasetype($field, $directives);
498
            }
499
        }
500
501
        /**
502
         * @var \GraphQL\Language\AST\NodeList|null
503
         */
504
        $directives = $this->type->astNode->directives;
505
        if ($directives) {
506
            $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

506
            $this->processDirectives(/** @scrutinizer ignore-type */ $directives);
Loading history...
507
        }
508
    }
509
510
    public function getGenerateFilename(bool $base = true): string
511
    {
512
        return $this->getBasePath('app/Models/' . ($base ? 'Base' : '') . $this->studlyName . '.php');
513
    }
514
}
515