Passed
Push — master ( 9503af...0b2a3d )
by Bruno
05:22
created

ModelGenerator::processRelationship()   D

Complexity

Conditions 26
Paths 55

Size

Total Lines 119
Code Lines 82

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 34
CRAP Score 26

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 82
c 1
b 0
f 0
dl 0
loc 119
rs 4.1666
ccs 34
cts 34
cp 1
cc 26
nc 55
nop 2
crap 26

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;
6
use GraphQL\Language\AST\NodeKind;
7
use GraphQL\Language\Visitor;
8
use Illuminate\Support\Str;
9
use GraphQL\Type\Definition\ListOfType;
10
use GraphQL\Type\Definition\NonNull;
11
use GraphQL\Type\Definition\ObjectType;
12
use GraphQL\Type\Definition\UnionType;
13
use Modelarium\BaseGenerator;
14
use Modelarium\Exception\Exception;
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->stubToString('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
        if ($scalarType) {
118
            if ($scalarType instanceof FormulariumScalarType) {
119 4
                $field = $scalarType->processDirectives(
120
                    $fieldName,
121 5
                    $directives
122 1
                );
123 1
                
124
                if ($isRequired) {
125 1
                    $field->setValidatorOption(
126
                        Datatype::REQUIRED,
127
                        'value',
128 1
                        true
129
                    );
130 4
                }
131 3
                $this->fields[$fieldName] = $field->toArray();
132 3
            }
133
        }
134 3
    }
135
136
    protected function processBasetype(
137 3
        \GraphQL\Type\Definition\FieldDefinition $field,
138 4
        \GraphQL\Language\AST\NodeList $directives
139 1
    ): void {
140 1
        $fieldName = $field->name;
141 1
142
        list($type, $isRequired) = Parser::getUnwrappedType($field->type);
143 1
144
        foreach ($directives as $directive) {
145
            $name = $directive->name->value;
146 1
            switch ($name) {
147
            case 'modelFillable':
148 4
                $this->fillable[] = $fieldName;
149
                break;
150
            case 'modelHidden':
151
                $this->hidden[] = $fieldName;
152 5
                break;
153
            case 'casts':
154
                foreach ($directive->arguments as $arg) {
155 7
                    /**
156
                     * @var \GraphQL\Language\AST\ArgumentNode $arg
157
                     */
158 7
159 1
                    $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...
160
161 1
                    switch ($arg->name->value) {
162 1
                    case 'type':
163 1
                        $this->casts[$fieldName] = $value;
164
                    }
165
                }
166 7
                break;
167
            }
168
        }
169 7
170
        $typeName = $type->name; /** @phpstan-ignore-line */
171
        $this->processField($typeName, $field, $directives, $isRequired);
172 7
    }
173
174 7
    protected function processRelationship(
175
        \GraphQL\Type\Definition\FieldDefinition $field,
176
        \GraphQL\Language\AST\NodeList $directives
177 7
    ): void {
178
        $lowerName = mb_strtolower($this->getInflector()->singularize($field->name));
179 7
        $lowerNamePlural = $this->getInflector()->pluralize($lowerName);
180 7
181 7
        $targetClass = '\\App\\' . Str::studly($this->getInflector()->singularize($field->name));
182 7
183
        list($type, $isRequired) = Parser::getUnwrappedType($field->type);
184
        $typeName = $type->name;
185
186 5
        $generateRandom = false;
187
        foreach ($directives as $directive) {
188
            $name = $directive->name->value;
189
            switch ($name) {
190
            case 'belongsTo':
191
                $generateRandom = true;
192
                $this->class->addMethod($lowerName)
193
                    ->setPublic()
194
                    ->setReturnType('BelongsTo')
195 7
                    ->setBody("return \$this->belongsTo($targetClass::class);");
196 7
                break;
197 7
198
            case 'belongsToMany':
199
                $generateRandom = true;
200 7
                $this->class->addMethod($lowerNamePlural)
201 7
                    ->setPublic()
202 7
                    ->setReturnType('BelongsTo')
203 7
                    ->setBody("return \$this->belongsToMany($targetClass::class);");
204
                break;
205
206 7
            case 'hasOne':
207 7
                $this->class->addMethod($lowerName)
208 7
                    ->setPublic()
209 7
                    ->setBody("return \$this->hasOne($targetClass::class);");
210
                break;
211
212 7
            case 'hasMany':
213 7
                $target = $this->getInflector()->singularize($targetClass);
214 7
                $this->class->addMethod($lowerNamePlural)
215 7
                    ->setPublic()
216
                    ->setBody("return \$this->hasMany($target::class);");
217
                break;
218 7
219 7
            case 'morphOne':
220 7
            case 'morphMany':
221 7
            case 'morphToMany':
222
                    $targetType = $this->parser->getType($typeName);
223
                if (!$targetType) {
224 7
                    throw new Exception("Cannot get type {$typeName} as a relationship to {$this->name}");
225 7
                } elseif (!($targetType instanceof ObjectType)) {
226 7
                    throw new Exception("{$typeName} is not a type for a relationship to {$this->name}");
227 7
                }
228
                $targetField = null;
229
                foreach ($targetType->getFields() as $subField) {
230 7
                    $subDir = Parser::getDirectives($subField->astNode->directives);
231 7
                    if (array_key_exists('morphTo', $subDir) || array_key_exists('morphedByMany', $subDir)) {
232
                        $targetField = $subField->name;
233
                        break;
234 5
                    }
235
                }
236 5
                if (!$targetField) {
237
                    throw new Exception("{$targetType} does not have a '@morphTo' or '@morphToMany' field");
238
                }
239
240
                $this->class->addMethod($field->name)
241
                    ->setPublic()
242
                    ->setBody("return \$this->{$name}($typeName::class, '$targetField');");
243
                break;
244
    
245
            case 'morphTo':
246
                $this->class->addMethod($field->name)
247
                    ->setPublic()
248
                    ->setBody("return \$this->morphTo();");
249
                break;
250
251
            case 'morphedByMany':
252
                $typeMap = $this->parser->getSchema()->getTypeMap();
253
       
254
                foreach ($typeMap as $name => $object) {
255
                    if (!($object instanceof ObjectType) || $name === 'Query' || $name === 'Mutation' || $name === 'Subscription') {
256
                        continue;
257
                    }
258
                    if (str_starts_with($name, '__')) {
259
                        // internal type
260
                        continue;
261
                    }
262
263
                    /**
264
                     * @var ObjectType $object
265
                     */
266
                    foreach ($object->getFields() as $subField) {
267
                        $directives = Parser::getDirectives($subField->astNode->directives);
268
269
                        if (!array_key_exists('morphToMany', $directives)) {
270
                            continue;
271
                        }
272
273
                        $methodName = $this->getInflector()->pluralize(mb_strtolower($name));
274
                        $this->class->addMethod($methodName)
275
                                ->setPublic()
276
                                ->setBody("return \$this->morphedByMany($name::class, '$lowerName');");
277
                    }
278
                }
279
                break;
280
            
281
            default:
282
                break;
283
            }
284
        }
285
286
        // TODO: relationship $this->processField($typeName, $field, $directives, $isRequired);
287
288
        if ($generateRandom) {
289
            $this->methodRandom->addBody(
290
                '$data["' . $lowerName . '_id"] = function () {' . "\n" .
291
                '    return factory(' . $targetClass . '::class)->create()->id;'  . "\n" .
292
                '};'
293
            );
294
        }
295
    }
296
297
    protected function processDirectives(
298
        \GraphQL\Language\AST\NodeList $directives
299
    ): void {
300
        foreach ($directives as $directive) {
301
            $name = $directive->name->value;
302
            switch ($name) {
303
            case 'migrationSoftDeletes':
304
                $this->traits[] = '\Illuminate\Database\Eloquent\SoftDeletes';
305
                break;
306
            case 'modelNotifiable':
307
                $this->traits[] = '\Illuminate\Notifications\Notifiable';
308
                break;
309
            case 'modelMustVerifyEmail':
310
                $this->traits[] = '\Illuminate\Notifications\MustVerifyEmail';
311
                break;
312
            case 'migrationRememberToken':
313
                $this->hidden[] = 'remember_token';
314
                break;
315
            case 'extends':
316
                foreach ($directive->arguments as $arg) {
317
                    /**
318
                     * @var \GraphQL\Language\AST\ArgumentNode $arg
319
                     */
320
321
                    $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...
322
323
                    switch ($arg->name->value) {
324
                    case 'class':
325
                        $this->parentClassName = $value;
326
                    }
327
                }
328
            }
329
        }
330
    }
331
332
    protected function formulariumModel(
333
334
    ): string {
335
        foreach ($this->fields as $f) {
336
            $string = <<<EOF
0 ignored issues
show
Unused Code introduced by
The assignment to $string is dead and can be removed.
Loading history...
337
            new \Formularium\Field(
338
                '{$f->name}',
339
                '',
340
                [ // renderable
341
                ],
342
                [ // validators
343
                ]
344
            ),
345
EOF;
346
        }
347
        return '';
348
    }
349
350
    public function generateString(): string
351
    {
352
        $namespace = new \Nette\PhpGenerator\PhpNamespace('App');
353
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\BelongsTo');
354
355
        $this->class = $namespace->addClass('Base' . $this->studlyName);
356
        $this->class->setExtends($this->parentClassName)
357
            ->addComment("This file was automatically generated by Modelarium.");
358
359
        $this->methodRandom = new Method('getRandomData');
360
        $this->methodRandom->addBody(
361
            '$data = static::getFormularium()->getRandom();' . "\n"
362
        );
363
364
        $this->processGraphql();
365
366
        foreach ($this->traits as $trait) {
367
            $this->class->addTrait($trait);
368
        }
369
370
        $this->class->addProperty('fillable')
371
            ->setProtected()
372
            ->setValue($this->fillable)
373
            ->setComment("The attributes that are mass assignable.\n@var array")
374
            ->setInitialized();
375
376
        $this->class->addProperty('hidden')
377
            ->setProtected()
378
            ->setValue($this->hidden)
379
            ->setComment("The attributes that should be hidden for arrays.\n@var array")
380
            ->setInitialized();
381
382
        $this->class->addProperty('casts')
383
            ->setProtected()
384
            ->setValue($this->casts)
385
            ->setComment("The attributes that should be cast to native types.\n@var array")
386
            ->setInitialized();
387
388
        $this->class->addMethod('getFields')
389
            ->setPublic()
390
            ->setStatic()
391
            ->setReturnType('array')
392
            ->addComment('@return array')
393
            ->addBody(
394
                "return ?;\n",
395
                [
396
                    $this->fields
397
                ]
398
            );
399
400
        $this->class->addMethod('getFormularium')
401
            ->setPublic()
402
            ->setStatic()
403
            ->setReturnType('\Formularium\Model')
404
            ->addComment('@return \Formularium\Model')
405
            ->addBody(
406
                '$model = \Formularium\Model::create(?, static::getFields());' . "\n" .
407
                'return $model;',
408
                [
409
                    $this->studlyName,
410
                ]
411
            );
412
        
413
        $this->methodRandom
414
            ->addComment('@return array')
415
            ->setPublic()
416
            ->setStatic()
417
            ->setReturnType('array')
418
            ->addBody('return $data;');
419
        $this->class->addMember($this->methodRandom);
420
421
        $printer = new \Nette\PhpGenerator\PsrPrinter;
422
        return "<?php declare(strict_types=1);\n\n" . $printer->printNamespace($namespace);
423
    }
424
425
    protected function processGraphql(): void
426
    {
427
        foreach ($this->type->getFields() as $field) {
428
            $directives = $field->astNode->directives;
429
            if (
430
                ($field->type instanceof ObjectType) ||
431
                ($field->type instanceof ListOfType) ||
432
                ($field->type instanceof UnionType) ||
433
                ($field->type instanceof NonNull && (
434
                    ($field->type->getWrappedType() instanceof ObjectType) ||
435
                    ($field->type->getWrappedType() instanceof ListOfType) ||
436
                    ($field->type->getWrappedType() instanceof UnionType)
437
                ))
438
            ) {
439
                // relationship
440
                $this->processRelationship($field, $directives);
441
            } else {
442
                $this->processBasetype($field, $directives);
443
            }
444
        }
445
446
        /**
447
         * @var \GraphQL\Language\AST\NodeList|null
448
         */
449
        $directives = $this->type->astNode->directives;
450
        if ($directives) {
451
            $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

451
            $this->processDirectives(/** @scrutinizer ignore-type */ $directives);
Loading history...
452
        }
453
    }
454
455
    public function getGenerateFilename(bool $base = true): string
456
    {
457
        return $this->getBasePath('app/' . ($base ? 'Base' : '') . $this->studlyName . '.php');
458
    }
459
}
460