ModelGenerator::processGraphql()   B
last analyzed

Complexity

Conditions 10
Paths 6

Size

Total Lines 30
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 10

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 10
eloc 19
c 1
b 1
f 0
nc 6
nop 0
dl 0
loc 30
ccs 18
cts 18
cp 1
crap 10
rs 7.6666

How to fix   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 Formularium\Extradata;
7
use Formularium\ExtradataParameter;
8
use Formularium\Field;
9
use Formularium\Model;
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 GraphQL\Language\AST\DirectiveNode;
15
use Modelarium\BaseGenerator;
16
use Modelarium\Exception\Exception;
17
use Modelarium\FormulariumUtils;
18
use Modelarium\GeneratedCollection;
19
use Modelarium\GeneratedItem;
20
use Modelarium\Parser;
21
use Modelarium\Types\FormulariumScalarType;
22
use Nette\PhpGenerator\Method;
23
24
class ModelGenerator extends BaseGenerator
25
{
26
27
    /**
28
     * @var string
29
     */
30
    protected $stubDir = __DIR__ . "/stubs/";
31
32
    /**
33
     * @var string
34
     */
35
    protected static $modelDir = 'app/Models/';
36
37
    /**
38
     * @var ObjectType
39
     */
40
    protected $type = null;
41
42
    /**
43
     * @var \Nette\PhpGenerator\ClassType
44
     */
45
    public $class = null;
46
47
    /**
48
     * fillable attributes
49
     *
50
     * @var array
51
     */
52
    public $fillable = [];
53
54
    /**
55
     * fillable attributes
56
     *
57
     * @var array
58
     */
59
    public $hidden = [];
60
61
    /**
62
     * cast attributes
63
     *
64
     * @var array
65
     */
66
    public $casts = [];
67
68
    /**
69
     *
70
     * @var string
71
     */
72
    public $parentClassName = '\Illuminate\Database\Eloquent\Model';
73
74
    /**
75
     * fields
76
     *
77
     * @var Model
78
     */
79
    public $fModel = null;
80
81
    /**
82
     * traits to include
83
     * @var array
84
     */
85
    public $traits = [];
86
87
    /**
88
     * Eager loading
89
     *
90
     * @var string[]
91
     */
92
    public $with = [];
93
94
    /**
95
     * Random generation
96
     *
97
     * @var Method
98
     */
99
    protected $methodRandom = null;
100
101
    /**
102
     * Do we have a 'can' attribute?
103
     *
104
     * @var boolean
105
     */
106
    protected $hasCan = false;
107
108
    /**
109
     * If true, we have timestamps on the migration.
110
     *
111
     * @var boolean
112
     */
113
    public $migrationTimestamps = false;
114
115
    /**
116
     * Undocumented variable
117
     *
118
     * @var GeneratedCollection
119
     */
120
    public $generatedCollection = null;
121
122 10
    public function generate(): GeneratedCollection
123
    {
124 10
        $this->generatedCollection = new GeneratedCollection();
125 10
        $this->fModel = Model::create($this->studlyName);
126 10
        $this->generatedCollection->push(new GeneratedItem(
127 10
            GeneratedItem::TYPE_MODEL,
128 10
            $this->generateString(),
129 10
            $this->getGenerateFilename()
130
        ));
131 10
        $this->generatedCollection->push(new GeneratedItem(
132 10
            GeneratedItem::TYPE_MODEL,
133 10
            $this->templateStub('model'),
134 10
            $this->getGenerateFilename(false),
135 10
            true
136
        ));
137 10
        return $this->generatedCollection;
138
    }
139
140
    /**
141
     * Override to insert extradata
142
     *
143
     * @param \GraphQL\Language\AST\NodeList<\GraphQL\Language\AST\DirectiveNode> $directives
144
     * @param string $generatorType
145
     * @return void
146
     */
147 10
    protected function processTypeDirectives(
148
        \GraphQL\Language\AST\NodeList $directives,
149
        string $generatorType
150
    ): void {
151 10
        foreach ($directives as $directive) {
152 1
            $name = $directive->name->value;
153 1
            $this->fModel->appendExtradata(FormulariumUtils::directiveToExtradata($directive));
154
    
155 1
            $className = $this->getDirectiveClass($name, $generatorType);
156 1
            if ($className) {
157 1
                $methodName = "$className::process{$generatorType}TypeDirective";
158
                /** @phpstan-ignore-next-line */
159 1
                $methodName(
160 1
                    $this,
161
                    $directive
162
                );
163
            }
164
        }
165 10
    }
166
167
    /**
168
     * @param string $typeName
169
     * @param \GraphQL\Type\Definition\FieldDefinition $field
170
     * @param \GraphQL\Language\AST\NodeList<DirectiveNode> $directives
171
     * @param boolean $isRequired
172
     * @return void
173
     */
174 10
    protected function processField(
175
        string $typeName,
176
        \GraphQL\Type\Definition\FieldDefinition $field,
177
        \GraphQL\Language\AST\NodeList $directives,
178
        bool $isRequired
179
    ): void {
180 10
        $fieldName = $field->name;
181
182 10
        if ($typeName === 'ID') {
183 10
            return;
184
        }
185
186 10
        $scalarType = $this->parser->getScalarType($typeName);
187
188
        /**
189
         * @var Field $fieldFormularium
190
         */
191 10
        $fieldFormularium = null;
192 10
        if (!$scalarType) {
193
            // probably another model
194 8
            $fieldFormularium = FormulariumUtils::getFieldFromDirectives(
195 8
                $fieldName,
196 8
                $typeName,
197 8
                $directives
198
            );
199 5
        } elseif ($scalarType instanceof FormulariumScalarType) {
200 5
            $fieldFormularium = FormulariumUtils::getFieldFromDirectives(
201 5
                $fieldName,
202 5
                $scalarType->getDatatype()->getName(),
203 5
                $directives
204
            );
205
        } else {
206
            return;
207
        }
208
209 10
        if ($isRequired) {
210 10
            $fieldFormularium->setValidatorOption(
211 10
                Datatype::REQUIRED,
212 10
                'value',
213 10
                true
214
            );
215
        }
216
217 10
        foreach ($directives as $directive) {
218 8
            $name = $directive->name->value;
219 8
            $className = $this->getDirectiveClass($name);
220 8
            if ($className) {
221 8
                $methodName = "$className::processModelFieldDirective";
222
                /** @phpstan-ignore-next-line */
223 8
                $methodName(
224 8
                    $this,
225
                    $field,
226
                    $fieldFormularium,
227
                    $directive
228
                );
229
            }
230
        }
231
232 10
        $this->fModel->appendField($fieldFormularium);
233 10
    }
234
235
    /**
236
     * @param \GraphQL\Type\Definition\FieldDefinition $field
237
     * @param \GraphQL\Language\AST\NodeList<DirectiveNode> $directives
238
     * @return void
239
     */
240 8
    protected function processRelationship(
241
        \GraphQL\Type\Definition\FieldDefinition $field,
242
        \GraphQL\Language\AST\NodeList $directives
243
    ): void {
244 8
        list($type, $isRequired) = Parser::getUnwrappedType($field->getType());
245 8
        $typeName = $type->name;
246
247
        // special types that should be skipped.
248 8
        if ($typeName === 'Can') {
249
            $this->hasCan = true;
250
            $this->fModel->appendExtradata(
251
                new Extradata(
252
                    'hasCan',
253
                    [ new ExtradataParameter('value', true) ]
254
                )
255
            );
256
            return;
257
        }
258
259 8
        $relationshipDatatype = null;
260
261 8
        foreach ($directives as $directive) {
262 8
            $name = $directive->name->value;
263
264 8
            $className = $this->getDirectiveClass($name);
265 8
            if ($className) {
266 8
                $methodName = "$className::processModelRelationshipDirective";
267
                /** @phpstan-ignore-next-line */
268 8
                $r = $methodName(
269 8
                    $this,
270
                    $field,
271
                    $directive,
272
                    $relationshipDatatype
273
                );
274 8
                if ($r) {
275 8
                    if ($relationshipDatatype) {
276
                        throw new Exception("Overwriting relationship in {$typeName} for {$field->name} in {$this->baseName}");
277
                    }
278 8
                    $relationshipDatatype = $r;
279
                }
280 8
                continue;
281
            }
282
        }
283
284 8
        if (!$relationshipDatatype) {
285
            // if target is a model...
286
            $targetType = $this->parser->getSchema()->getType($typeName);
287
            /** @phpstan-ignore-next-line */
288
            $directives = $targetType->astNode->directives;
0 ignored issues
show
Bug introduced by
Accessing directives on the interface GraphQL\Language\AST\TypeDefinitionNode suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
289
            $skip = false;
290
            foreach ($directives as $directive) {
291
                $dName = $directive->name->value;
292
                if ($dName === 'typeSkip') {
293
                    $skip = true;
294
                    break;
295
                }
296
            }
297
            if ($skip == false) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
298
                $this->warn("Could not find a relationship {$typeName} for {$field->name} in {$this->baseName}. Consider adding a @modelAccessor or declaring the relationship (e.g. @hasMany, @belongsTo).");
299
            }
300
            return;
301
        }
302
    
303 8
        $this->processField($relationshipDatatype->getName(), $field, $directives, $isRequired);
304
305
        // TODO
306
        // if ($generateRandom) {
307
        //     if ($relationship == RelationshipFactory::RELATIONSHIP_MANY_TO_MANY || $relationship == RelationshipFactory::MORPH_MANY_TO_MANY) {
308
        //         // TODO: do we generate it? seed should do it?
309
        //     } else {
310
        //         $this->methodRandom->addBody(
311
        //             '$data["' . $lowerName . '_id"] = function () {' . "\n" .
312
        //         '    return factory(' . $targetClass . '::class)->create()->id;'  . "\n" .
313
        //         '};'
314
        //         );
315
        //     }
316
        // }
317 8
    }
318
319 8
    public static function getRelationshipDatatypeName(
320
        string $relationship,
321
        bool $isInverse,
322
        string $sourceTypeName,
323
        string $targetTypeName
324
    ): string {
325 8
        return "relationship:" . ($isInverse ? "inverse:" : "") .
326 8
            "$relationship:$sourceTypeName:$targetTypeName";
327
    }
328
329 10
    public function generateString(): string
330
    {
331 10
        $namespace = new \Nette\PhpGenerator\PhpNamespace('App\\Models');
332 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\BelongsTo');
333 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany');
334 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\HasOne');
335 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\HasMany');
336 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\MorphTo');
337 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\MorphOne');
338 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\MorphToMany');
339 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Builder');
340 10
        $namespace->addUse('\\Illuminate\\Support\\Facades\\Auth');
341 10
        $namespace->addUse('\\Formularium\\Exception\\NoRandomException');
342 10
        $namespace->addUse('\\Modelarium\\Laravel\\Datatypes\\Datatype_relationship');
343
344 10
        $this->class = $namespace->addClass('Base' . $this->studlyName);
345 10
        $this->class
346 10
            ->addComment("This file was automatically generated by Modelarium.")
347 10
            ->setAbstract();
348
349 10
        $this->methodRandom = new Method('getRandomData');
350 10
        $this->methodRandom->addBody(
351 10
            '$data = static::getFormularium()->getRandom(get_called_class() . \'::getRandomFieldData\');' . "\n"
352
        );
353
354 10
        $this->processGraphql();
355
356
        // this might have changed
357 10
        $this->class->setExtends($this->parentClassName);
358
359 10
        foreach ($this->traits as $trait) {
360 1
            $this->class->addTrait($trait);
361
        }
362
363 10
        $this->class->addProperty('fillable')
364 10
            ->setProtected()
365 10
            ->setValue($this->fillable)
366 10
            ->setComment("The attributes that are mass assignable.\n@var array")
367 10
            ->setInitialized();
368
369 10
        $this->class->addProperty('hidden')
370 10
            ->setProtected()
371 10
            ->setValue($this->hidden)
372 10
            ->setComment("The attributes that should be hidden for arrays.\n@var array")
373 10
            ->setInitialized();
374
375 10
        $this->class->addProperty('with')
376 10
            ->setProtected()
377 10
            ->setValue($this->with)
378 10
            ->setComment("Eager load these relationships.\n@var array")
379 10
            ->setInitialized();
380
381 10
        if (!$this->migrationTimestamps) {
382 9
            $this->class->addProperty('timestamps')
383 9
                ->setPublic()
384 9
                ->setValue(false)
385 9
                ->setComment("Do not set timestamps.\n@var boolean")
386 9
                ->setInitialized();
387
        }
388
389 10
        if ($this->casts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->casts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
390
            $this->class->addProperty('casts')
391
                ->setProtected()
392
                ->setValue($this->casts)
393
                ->setComment("The attributes that should be cast.\n@var array")
394
                ->setInitialized();
395
        }
396
397 10
        $this->class->addMethod('getFields')
398 10
            ->setPublic()
399 10
            ->setStatic()
400 10
            ->setReturnType('array')
401 10
            ->addComment('@return array')
402 10
            ->addBody(
403 10
                "return ?;\n",
404
                [
405 10
                    $this->fModel->serialize()
406
                ]
407
            );
408
409 10
        $this->class->addMethod('getFormularium')
410 10
            ->setPublic()
411 10
            ->setStatic()
412 10
            ->setReturnType('\Formularium\Model')
413 10
            ->addComment('@return \Formularium\Model')
414 10
            ->addBody(
415
                '$model = \Formularium\Model::fromStruct(static::getFields());' . "\n" .
416 10
                'return $model;',
417
                [
418
                    //$this->studlyName,
419 10
                ]
420
            );
421
        
422 10
        $this->methodRandom
423 10
            ->addComment('@return array')
424 10
            ->setPublic()
425 10
            ->setStatic()
426 10
            ->setReturnType('array')
427 10
            ->addBody('return $data;');
428 10
        $this->class->addMember($this->methodRandom);
429
430 10
        $getRandomFieldData = $this->class->addMethod('getRandomFieldData')
431 10
            ->setPublic()
432 10
            ->setStatic()
433 10
            ->addComment("Filters fields and generate random data. Throw NoRandomException for fields you don't want to generate random data, or return a valid value.")
434 10
            ->addBody('
435
$d = $field->getDatatype();
436
if ($field->getExtradata("migrationSkip")) {
437
    throw new NoRandomException($field->getName());
438
}
439
if ($d instanceof Datatype_relationship) {
440
    if (!$d->getIsInverse() || !$field->getValidatorOption("required", "value", false)) {
441
        throw new NoRandomException($field->getName());
442
    }
443
    $data[$field->getName() . "_id"] = $field->getDatatype()->getRandom();
444
} else {
445
    $data[$field->getName()] = $field->getDatatype()->getRandom();
446
}');
447 10
        $getRandomFieldData->addParameter('field')->setType('Formularium\Field');
448 10
        $getRandomFieldData->addParameter('model')->setType('Formularium\Model');
449 10
        $getRandomFieldData->addParameter('data')->setType('array')->setReference(true);
450
451
        // TODO perhaps we can use PolicyGenerator->policyClasses to auto generate
452
453 10
        if ($this->hasCan) {
454
            $this->class->addMethod('getCanAttribute')
455
                ->setPublic()
456
                ->setReturnType('array')
457
                ->addComment("Returns the policy permissions for actions such as editing or deleting.\n@return array")
458
                ->addBody(
459
                    '$policy = new \\App\\Policies\\' . $this->studlyName . 'Policy();' . "\n" .
460
                    '$user = Auth::user();' . "\n" .
461
                    'return [' . "\n" .
462
                    '    //[ "ability" => "create", "value" => $policy->create($user) ]' . "\n" .
463
                    '];'
464
                );
465
466
            /*  This creates a policy, but it's not useful. It's an empty file and @can won't patch it for now
467
            if (!class_exists('\\App\\Policies\\' . $this->studlyName . 'Policy')) {
468
                $policyGenerator = new PolicyGenerator($this->parser, 'Mutation', $this->type);
469
                $z = $policyGenerator->getPolicyClass($this->studlyName);
470
                $x = $policyGenerator->generate();
471
                $this->generatedCollection = $this->generatedCollection->merge($x);
472
            }
473
            */
474
        }
475
        
476 10
        $printer = new \Nette\PhpGenerator\PsrPrinter;
477 10
        return $this->phpHeader() . $printer->printNamespace($namespace);
478
    }
479
480 10
    protected function processGraphql(): void
481
    {
482 10
        foreach ($this->type->getFields() as $field) {
483 10
            $directives = $field->astNode->directives;
484 10
            $type = $field->getType();
485
            if (
486 10
                ($type instanceof ObjectType) ||
487 10
                ($type instanceof ListOfType) ||
488 10
                ($type instanceof UnionType) ||
489 10
                ($type instanceof NonNull && (
490 10
                    ($type->getWrappedType() instanceof ObjectType) ||
491 10
                    ($type->getWrappedType() instanceof ListOfType) ||
492 10
                    ($type->getWrappedType() instanceof UnionType)
493
                ))
494
            ) {
495
                // relationship
496 8
                $this->processRelationship($field, $directives);
497
            } else {
498 10
                list($type, $isRequired) = Parser::getUnwrappedType($field->getType());
499 10
                $typeName = $type->name;
500 10
                $this->processField($typeName, $field, $directives, $isRequired);
501
            }
502
        }
503
504
        /**
505
         * @var \GraphQL\Language\AST\NodeList<\GraphQL\Language\AST\DirectiveNode>|null
506
         */
507 10
        $directives = $this->type->astNode->directives;
508 10
        if ($directives) {
0 ignored issues
show
introduced by
$directives is of type GraphQL\Language\AST\NodeList, thus it always evaluated to true.
Loading history...
509 10
            $this->processTypeDirectives($directives, 'Model');
510
        }
511 10
    }
512
513 10
    public function getGenerateFilename(bool $base = true): string
514
    {
515 10
        return $this->getBasePath(self::$modelDir . '/' . ($base ? 'Base' : '') . $this->studlyName . '.php');
516
    }
517
518
    public static function setModelDir(string $dir): void
519
    {
520
        self::$modelDir = $dir;
521
    }
522
}
523