Passed
Push — master ( 0b2a3d...a47e04 )
by Bruno
17:31 queued 04:58
created

ModelGenerator::generateString()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 89
Code Lines 63

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 63
c 2
b 0
f 0
dl 0
loc 89
rs 8.8072
ccs 0
cts 0
cp 0
cc 2
nc 2
nop 0
crap 6

How to fix   Long Method   

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

480
            $this->processDirectives(/** @scrutinizer ignore-type */ $directives);
Loading history...
481
        }
482
    }
483
484
    public function getGenerateFilename(bool $base = true): string
485
    {
486
        return $this->getBasePath('app/' . ($base ? 'Base' : '') . $this->studlyName . '.php');
487
    }
488
}
489