ModelGenerator::processRelationship()   B
last analyzed

Complexity

Conditions 10
Paths 30

Size

Total Lines 64
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 22.5

Importance

Changes 18
Bugs 6 Features 0
Metric Value
cc 10
eloc 38
c 18
b 6
f 0
nc 30
nop 2
dl 0
loc 64
rs 7.6666
ccs 18
cts 36
cp 0.5
crap 22.5

How to fix   Long Method    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