Passed
Push — master ( 4345d7...2fa866 )
by Bruno
18:55 queued 08:53
created

ModelGenerator::directiveToExtradata()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 4.3145

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 8
c 1
b 0
f 0
dl 0
loc 13
ccs 1
cts 6
cp 0.1666
rs 10
cc 2
nc 2
nop 1
crap 4.3145
1
<?php declare(strict_types=1);
2
3
namespace Modelarium\Laravel\Targets;
4
5
use Formularium\Datatype;
6
use Formularium\Extradata;
0 ignored issues
show
Bug introduced by
The type Formularium\Extradata was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
7
use Formularium\ExtradataParameter;
0 ignored issues
show
Bug introduced by
The type Formularium\ExtradataParameter was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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

385
            $this->fModel->/** @scrutinizer ignore-call */ 
386
                           appendExtradata($this->directiveToExtradata($directive));

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...
386
387
            switch ($name) {
388
            case 'migrationSoftDeletes':
389
                $this->traits[] = '\Illuminate\Database\Eloquent\SoftDeletes';
390
                break;
391
            case 'modelNotifiable':
392
                $this->traits[] = '\Illuminate\Notifications\Notifiable';
393 10
                break;
394
            case 'modelMustVerifyEmail':
395 10
                $this->traits[] = '\Illuminate\Notifications\MustVerifyEmail';
396 10
                break;
397 10
            case 'migrationRememberToken':
398 10
                $this->hidden[] = 'remember_token';
399 10
                break;
400 10
            case 'modelExtends':
401 10
                foreach ($directive->arguments as $arg) {
402
                    /**
403 10
                     * @var \GraphQL\Language\AST\ArgumentNode $arg
404 10
                     */
405 10
406 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...
407
408 10
                    switch ($arg->name->value) {
409 10
                    case 'class':
410 10
                        $this->parentClassName = $value;
411
                    }
412
                }
413 10
            }
414
        }
415 10
    }
416 1
417
    public function generateString(): string
418
    {
419 10
        $namespace = new \Nette\PhpGenerator\PhpNamespace('App\\Models');
420 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\BelongsTo');
421 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\HasOne');
422 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\HasMany');
423 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\MorphTo');
424
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\MorphToMany');
425 10
        $namespace->addUse('\\Illuminate\\Support\\Facades\\Auth');
426 10
        $namespace->addUse('\\Modelarium\\Laravel\\Datatypes\\Datatype_relationship');
427 10
428 10
        $this->class = $namespace->addClass('Base' . $this->studlyName);
429 10
        $this->class
430
            ->addComment("This file was automatically generated by Modelarium.")
431 10
            ->setAbstract();
432 10
433 10
        $this->methodRandom = new Method('getRandomData');
434 10
        $this->methodRandom->addBody(
435 10
            '$data = static::getFormularium()->getRandom(get_called_class() . \'::getRandomDataFilterFields\');' . "\n"
436
        );
437 10
438 10
        $this->processGraphql();
439 10
440 10
        // this might have changed
441 10
        $this->class->setExtends($this->parentClassName);
442 10
443 10
        foreach ($this->traits as $trait) {
444
            $this->class->addTrait($trait);
445 10
        }
446
447
        $this->class->addProperty('fillable')
448
            ->setProtected()
449 10
            ->setValue($this->fillable)
450 10
            ->setComment("The attributes that are mass assignable.\n@var array")
451 10
            ->setInitialized();
452 10
453 10
        $this->class->addProperty('hidden')
454 10
            ->setProtected()
455
            ->setValue($this->hidden)
456 10
            ->setComment("The attributes that should be hidden for arrays.\n@var array")
457
            ->setInitialized();
458 10
459
        $this->class->addProperty('casts')
460
            ->setProtected()
461
            ->setValue($this->casts)
462 10
            ->setComment("The attributes that should be cast to native types.\n@var array")
463 10
            ->setInitialized();
464 10
465 10
        $this->class->addMethod('getFields')
466 10
            ->setPublic()
467 10
            ->setStatic()
468 10
            ->setReturnType('array')
469
            ->addComment('@return array')
470
            ->addBody(
471 10
                "return ?;\n",
472 10
                [
473 10
                    $this->fModel->serialize()
474 10
                ]
475 10
            );
476 10
477 10
        $this->class->addMethod('getFormularium')
478 10
            ->setPublic()
479 10
            ->setStatic()
480 10
            ->setReturnType('\Formularium\Model')
481
            ->addComment('@return \Formularium\Model')
482
            ->addBody(
483 10
                '$model = \Formularium\Model::fromStruct(static::getFields());' . "\n" .
484 10
                'return $model;',
485
                [
486
                    //$this->studlyName,
487 10
                ]
488
            );
489 10
        
490 10
        $this->methodRandom
491
            ->addComment('@return array')
492 10
            ->setPublic()
493 10
            ->setStatic()
494 10
            ->setReturnType('array')
495 10
            ->addBody('return $data;');
496 10
        $this->class->addMember($this->methodRandom);
497 10
498 10
        $this->class->addMethod('getRandomDataFilterFields')
499
            ->setPublic()
500
            ->setStatic()
501
            ->setReturnType('bool')
502 8
            ->addComment("Filters fields used for random data generation.")
503
            ->addBody('
504 10
$d = $f->getDatatype();
505
if ($d instanceof Datatype_relationship) {
506
    return false;
507
}
508
return true;')
509
            ->addParameter('f')->setType('Formularium\Field');
510
511 10
        // TODO perhaps we can use PolicyGenerator->policyClasses to auto generate
512 10
        $this->class->addMethod('getCanAttribute')
513 10
            ->setPublic()
514
            ->setReturnType('array')
515 10
            ->addComment("Returns the policy permissions for actions such as editing or deleting.\n@return \Formularium\Model")
516
            ->addBody(
517 8
                '$policy = new \\App\\Policies\\' . $this->studlyName . 'Policy();' . "\n" .
518
                '$user = Auth::user();' . "\n" .
519 8
                'return [' . "\n" .
520
                '    //[ "ability" => "create", "value" => $policy->create($user) ]' . "\n" .
521
                '];'
522
            );
523
        
524
        $printer = new \Nette\PhpGenerator\PsrPrinter;
525
        return $this->phpHeader() . $printer->printNamespace($namespace);
526
    }
527
528
    protected function processGraphql(): void
529
    {
530
        foreach ($this->type->getFields() as $field) {
531
            $directives = $field->astNode->directives;
532
            if (
533
                ($field->type instanceof ObjectType) ||
534
                ($field->type instanceof ListOfType) ||
535
                ($field->type instanceof UnionType) ||
536
                ($field->type instanceof NonNull && (
537
                    ($field->type->getWrappedType() instanceof ObjectType) ||
538
                    ($field->type->getWrappedType() instanceof ListOfType) ||
539
                    ($field->type->getWrappedType() instanceof UnionType)
540
                ))
541
            ) {
542
                // relationship
543
                $this->processRelationship($field, $directives);
544
            } else {
545
                $this->processBasetype($field, $directives);
546
            }
547
        }
548
549
        /**
550
         * @var \GraphQL\Language\AST\NodeList|null
551
         */
552
        $directives = $this->type->astNode->directives;
553
        if ($directives) {
554
            $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

554
            $this->processDirectives(/** @scrutinizer ignore-type */ $directives);
Loading history...
555
        }
556
    }
557
558
    public function getGenerateFilename(bool $base = true): string
559
    {
560
        return $this->getBasePath(self::$modelDir . '/' . ($base ? 'Base' : '') . $this->studlyName . '.php');
561
    }
562
563
    public static function setModelDir(string $dir): void
564
    {
565
        self::$modelDir = $dir;
566
    }
567
}
568