Passed
Push — master ( 41a7c0...823f6c )
by Bruno
15:50 queued 07:18
created

ModelGenerator::processField()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 41
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 5.002

Importance

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

512
            $this->processDirectives(/** @scrutinizer ignore-type */ $directives);
Loading history...
513
        }
514 10
    }
515
516 8
    public function getGenerateFilename(bool $base = true): string
517
    {
518 8
        return $this->getBasePath('app/Models/' . ($base ? 'Base' : '') . $this->studlyName . '.php');
519
    }
520
}
521