Passed
Push — master ( 7c420c...5a2b93 )
by Bruno
08:49
created

ModelGenerator::setModelDir()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

545
            $this->processDirectives(/** @scrutinizer ignore-type */ $directives);
Loading history...
546
        }
547
    }
548
549
    public function getGenerateFilename(bool $base = true): string
550
    {
551
        return $this->getBasePath(self::$modelDir . '/' . ($base ? 'Base' : '') . $this->studlyName . '.php');
552
    }
553
554
    public static function setModelDir(string $dir): void
555
    {
556
        self::$modelDir = $dir;
557
    }
558
}
559