Passed
Push — master ( c5e7d6...f7ac2f )
by Bruno
03:57
created

ModelGenerator::processRelationship()   B

Complexity

Conditions 7
Paths 10

Size

Total Lines 61
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 7.0957

Importance

Changes 13
Bugs 5 Features 0
Metric Value
cc 7
eloc 35
c 13
b 5
f 0
nc 10
nop 2
dl 0
loc 61
ccs 28
cts 32
cp 0.875
crap 7.0957
rs 8.4266

How to fix   Long Method   

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 Illuminate\Support\Str;
11
use GraphQL\Type\Definition\ListOfType;
12
use GraphQL\Type\Definition\NonNull;
13
use GraphQL\Type\Definition\ObjectType;
14
use GraphQL\Type\Definition\UnionType;
15
use Modelarium\BaseGenerator;
16
use Modelarium\Datatypes\Datatype_relationship;
17
use Modelarium\Datatypes\RelationshipFactory;
18
use Modelarium\Exception\Exception;
19
use Modelarium\FormulariumUtils;
20
use Modelarium\GeneratedCollection;
21
use Modelarium\GeneratedItem;
22
use Modelarium\Parser;
23
use Modelarium\Types\FormulariumScalarType;
24
use Nette\PhpGenerator\Method;
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
    public $class = null;
47
48
    /**
49
     * fillable attributes
50
     *
51
     * @var array
52
     */
53
    public $fillable = [];
54
55
    /**
56
     * fillable attributes
57
     *
58
     * @var array
59
     */
60
    public $hidden = [];
61
62
    /**
63
     * cast attributes
64
     *
65
     * @var array
66
     */
67
    public $casts = [];
68
69
    /**
70
     *
71
     * @var string
72
     */
73
    public $parentClassName = '\Illuminate\Database\Eloquent\Model';
74
75
    /**
76
     * fields
77
     *
78
     * @var Model
79
     */
80
    public $fModel = null;
81
82
    /**
83
     *
84
     * @var array
85
     */
86
    public $traits = [];
87
88
    /**
89
     * Random generation
90
     *
91
     * @var Method
92
     */
93
    protected $methodRandom = null;
94
95
    /**
96
     * Do we have a 'can' attribute?
97
     *
98
     * @var boolean
99
     */
100
    protected $hasCan = true;
101
102
    /**
103
     * If true, we have timestamps on the migration.
104
     *
105
     * @var boolean
106
     */
107
    public $migrationTimestamps = false;
108
109 10
    public function generate(): GeneratedCollection
110
    {
111 10
        $this->fModel = Model::create($this->studlyName);
112 10
        $x = new GeneratedCollection([
113 10
            new GeneratedItem(
114 10
                GeneratedItem::TYPE_MODEL,
115 10
                $this->generateString(),
116 10
                $this->getGenerateFilename()
117
            ),
118 10
            new GeneratedItem(
119 10
                GeneratedItem::TYPE_MODEL,
120 10
                $this->templateStub('model'),
121 10
                $this->getGenerateFilename(false),
122 10
                true
123
            )
124
        ]);
125 10
        return $x;
126
    }
127
128 10
    protected function processField(
129
        string $typeName,
130
        \GraphQL\Type\Definition\FieldDefinition $field,
131
        \GraphQL\Language\AST\NodeList $directives,
132
        bool $isRequired
133
    ): void {
134 10
        $fieldName = $field->name;
135
136 10
        if ($typeName === 'ID') {
137 10
            return;
138
        }
139
140 10
        $scalarType = $this->parser->getScalarType($typeName);
141
142
        /**
143
         * @var Field $field
144
         */
145 10
        $field = null;
146 10
        if (!$scalarType) {
147
            // probably another model
148 8
            $field = FormulariumUtils::getFieldFromDirectives(
149 8
                $fieldName,
150 8
                $typeName,
151 8
                $directives
152
            );
153 5
        } elseif ($scalarType instanceof FormulariumScalarType) {
154 5
            $field = FormulariumUtils::getFieldFromDirectives(
155 5
                $fieldName,
156 5
                $scalarType->getDatatype()->getName(),
157 5
                $directives
158
            );
159
        } else {
160
            return;
161
        }
162
163 10
        if ($isRequired) {
164 10
            $field->setValidatorOption(
165 10
                Datatype::REQUIRED,
166 10
                'value',
167 10
                true
168
            );
169
        }
170
171 10
        $this->fModel->appendField($field);
172 10
    }
173
174 10
    protected function processFieldDirectives(
175
        \GraphQL\Type\Definition\FieldDefinition $field,
176
        \GraphQL\Language\AST\NodeList $directives
177
    ): void {
178 10
        list($type, $isRequired) = Parser::getUnwrappedType($field->type);
179
180 10
        foreach ($directives as $directive) {
181
            $name = $directive->name->value;
182
            $className = $this->getDirectiveClass($name);
183
            if ($className) {
184
                $methodName = "$className::processModelFieldDirective";
185
                /** @phpstan-ignore-next-line */
186
                $methodName(
187
                    $this,
188
                    $field,
189
                    $directive
190
                );
191
            }
192
        }
193
194 10
        $typeName = $type->name;
195 10
        $this->processField($typeName, $field, $directives, $isRequired);
196 10
    }
197
198 8
    protected function processRelationship(
199
        \GraphQL\Type\Definition\FieldDefinition $field,
200
        \GraphQL\Language\AST\NodeList $directives
201
    ): void {
202 8
        $lowerName = mb_strtolower($this->getInflector()->singularize($field->name));
203 8
        $lowerNamePlural = $this->getInflector()->pluralize($lowerName);
0 ignored issues
show
Unused Code introduced by
The assignment to $lowerNamePlural is dead and can be removed.
Loading history...
204
205 8
        $targetClass = '\\App\\Models\\' . Str::studly($this->getInflector()->singularize($field->name));
0 ignored issues
show
Unused Code introduced by
The assignment to $targetClass is dead and can be removed.
Loading history...
206
207 8
        list($type, $isRequired) = Parser::getUnwrappedType($field->type);
208 8
        $typeName = $type->name;
209
210
        // special types that should be skipped.
211 8
        if ($typeName === 'Can') {
212
            $this->hasCan = true;
213
            return;
214
        }
215
216 8
        $relationshipDatatype = null;
217
218 8
        $generateRandom = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $generateRandom is dead and can be removed.
Loading history...
219 8
        $sourceTypeName = $this->lowerName;
0 ignored issues
show
Unused Code introduced by
The assignment to $sourceTypeName is dead and can be removed.
Loading history...
220 8
        $targetTypeName = $lowerName;
0 ignored issues
show
Unused Code introduced by
The assignment to $targetTypeName is dead and can be removed.
Loading history...
221 8
        $isInverse = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $isInverse is dead and can be removed.
Loading history...
222
223 8
        foreach ($directives as $directive) {
224 8
            $name = $directive->name->value;
225 8
            $className = $this->getDirectiveClass($name);
226 8
            if ($className) {
227 8
                $methodName = "$className::processModelFieldDirective";
228
                /** @phpstan-ignore-next-line */
229 8
                $methodName(
230 8
                    $this,
231
                    $field,
232
                    $directive
233
                );
234
235 8
                $methodName = "$className::processModelRelationshipDirective";
236
                /** @phpstan-ignore-next-line */
237 8
                $r = $methodName(
238 8
                    $this,
239
                    $field,
240
                    $directive
241
                );
242 8
                if ($r) {
243 8
                    if ($relationshipDatatype) {
244
                        throw new Exception("Overwrting relationship in {$typeName} for {$field->name} in {$this->lowerName}");
245
                    }
246 8
                    $relationshipDatatype = $r;
247
                }
248 8
                continue;
249
            }
250
        }
251
252 8
        if (!$relationshipDatatype) {
253
            // TODO: generate a warning, perhaps?
254
            // throw new Exception("Could not find a relationship in {$typeName} for {$field->name} in {$sourceTypeName}");
255
            return;
256
        }
257
    
258 8
        $this->processField($relationshipDatatype, $field, $directives, $isRequired);
259
260
        // TODO
261
        // if ($generateRandom) {
262
        //     if ($relationship == RelationshipFactory::RELATIONSHIP_MANY_TO_MANY || $relationship == RelationshipFactory::MORPH_MANY_TO_MANY) {
263
        //         // TODO: do we generate it? seed should do it?
264
        //     } else {
265
        //         $this->methodRandom->addBody(
266
        //             '$data["' . $lowerName . '_id"] = function () {' . "\n" .
267
        //         '    return factory(' . $targetClass . '::class)->create()->id;'  . "\n" .
268
        //         '};'
269
        //         );
270
        //     }
271
        // }
272 8
    }
273
274 8
    public static function getRelationshipDatatypeName(
275
        string $relationship,
276
        bool $isInverse,
277
        string $sourceTypeName,
278
        string $targetTypeName
279
    ): string {
280 8
        return "relationship:" . ($isInverse ? "inverse:" : "") .
281 8
            "$relationship:$sourceTypeName:$targetTypeName";
282
    }
283
284 10
    protected function processDirectives(
285
        \GraphQL\Language\AST\NodeList $directives
286
    ): void {
287 10
        foreach ($directives as $directive) {
288 1
            $name = $directive->name->value;
289 1
            $this->fModel->appendExtradata(FormulariumUtils::directiveToExtradata($directive));
290
291 1
            $className = $this->getDirectiveClass($name);
292 1
            if ($className) {
293 1
                $methodName = "$className::processModelTypeDirective";
294
                /** @phpstan-ignore-next-line */
295 1
                $methodName(
296 1
                    $this,
297
                    $directive
298
                );
299
            }
300
        }
301 10
    }
302
303 10
    public function generateString(): string
304
    {
305 10
        $namespace = new \Nette\PhpGenerator\PhpNamespace('App\\Models');
306 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\BelongsTo');
307 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\HasOne');
308 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\HasMany');
309 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\MorphTo');
310 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\MorphOne');
311 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\MorphToMany');
312 10
        $namespace->addUse('\\Illuminate\\Support\\Facades\\Auth');
313 10
        $namespace->addUse('\\Formularium\\Exception\\NoRandomException');
314 10
        $namespace->addUse('\\Modelarium\\Laravel\\Datatypes\\Datatype_relationship');
315
316 10
        $this->class = $namespace->addClass('Base' . $this->studlyName);
317 10
        $this->class
318 10
            ->addComment("This file was automatically generated by Modelarium.")
319 10
            ->setAbstract();
320
321 10
        $this->methodRandom = new Method('getRandomData');
322 10
        $this->methodRandom->addBody(
323 10
            '$data = static::getFormularium()->getRandom(get_called_class() . \'::getRandomFieldData\');' . "\n"
324
        );
325
326 10
        $this->processGraphql();
327
328
        // this might have changed
329 10
        $this->class->setExtends($this->parentClassName);
330
331 10
        foreach ($this->traits as $trait) {
332 1
            $this->class->addTrait($trait);
333
        }
334
335 10
        $this->class->addProperty('fillable')
336 10
            ->setProtected()
337 10
            ->setValue($this->fillable)
338 10
            ->setComment("The attributes that are mass assignable.\n@var array")
339 10
            ->setInitialized();
340
341 10
        $this->class->addProperty('hidden')
342 10
            ->setProtected()
343 10
            ->setValue($this->hidden)
344 10
            ->setComment("The attributes that should be hidden for arrays.\n@var array")
345 10
            ->setInitialized();
346
347 10
        if (!$this->migrationTimestamps) {
348 9
            $this->class->addProperty('timestamps')
349 9
                ->setPublic()
350 9
                ->setValue(false)
351 9
                ->setComment("Do not set timestamps.\n@var boolean")
352 9
                ->setInitialized();
353
        }
354
355 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...
356
            $this->class->addProperty('casts')
357
                ->setProtected()
358
                ->setValue($this->casts)
359
                ->setComment("The attributes that should be cast.\n@var array")
360
                ->setInitialized();
361
        }
362
363 10
        $this->class->addMethod('getFields')
364 10
            ->setPublic()
365 10
            ->setStatic()
366 10
            ->setReturnType('array')
367 10
            ->addComment('@return array')
368 10
            ->addBody(
369 10
                "return ?;\n",
370
                [
371 10
                    $this->fModel->serialize()
372
                ]
373
            );
374
375 10
        $this->class->addMethod('getFormularium')
376 10
            ->setPublic()
377 10
            ->setStatic()
378 10
            ->setReturnType('\Formularium\Model')
379 10
            ->addComment('@return \Formularium\Model')
380 10
            ->addBody(
381
                '$model = \Formularium\Model::fromStruct(static::getFields());' . "\n" .
382 10
                'return $model;',
383
                [
384
                    //$this->studlyName,
385 10
                ]
386
            );
387
        
388 10
        $this->methodRandom
389 10
            ->addComment('@return array')
390 10
            ->setPublic()
391 10
            ->setStatic()
392 10
            ->setReturnType('array')
393 10
            ->addBody('return $data;');
394 10
        $this->class->addMember($this->methodRandom);
395
396 10
        $this->class->addMethod('getRandomFieldData')
397 10
            ->setPublic()
398 10
            ->setStatic()
399 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.")
400 10
            ->addBody('
401
$d = $f->getDatatype();
402
if ($d instanceof Datatype_relationship) {
403
    throw new NoRandomException($f->getName());
404
}
405
return $f->getDatatype()->getRandom();')
406 10
            ->addParameter('f')->setType('Formularium\Field');
407
408
        // TODO perhaps we can use PolicyGenerator->policyClasses to auto generate
409 10
        if ($this->hasCan) {
410 10
            $this->class->addMethod('getCanAttribute')
411 10
                ->setPublic()
412 10
                ->setReturnType('array')
413 10
                ->addComment("Returns the policy permissions for actions such as editing or deleting.\n@return \Formularium\Model")
414 10
                ->addBody(
415 10
                    '$policy = new \\App\\Policies\\' . $this->studlyName . 'Policy();' . "\n" .
416 10
                    '$user = Auth::user();' . "\n" .
417 10
                    'return [' . "\n" .
418 10
                    '    //[ "ability" => "create", "value" => $policy->create($user) ]' . "\n" .
419 10
                    '];'
420
                );
421
        }
422
        
423 10
        $printer = new \Nette\PhpGenerator\PsrPrinter;
424 10
        return $this->phpHeader() . $printer->printNamespace($namespace);
425
    }
426
427 10
    protected function processGraphql(): void
428
    {
429 10
        foreach ($this->type->getFields() as $field) {
430 10
            $directives = $field->astNode->directives;
431
            if (
432 10
                ($field->type instanceof ObjectType) ||
433 10
                ($field->type instanceof ListOfType) ||
434 10
                ($field->type instanceof UnionType) ||
435 10
                ($field->type instanceof NonNull && (
436 10
                    ($field->type->getWrappedType() instanceof ObjectType) ||
437 10
                    ($field->type->getWrappedType() instanceof ListOfType) ||
438 10
                    ($field->type->getWrappedType() instanceof UnionType)
439
                ))
440
            ) {
441
                // relationship
442 8
                $this->processRelationship($field, $directives);
443
            } else {
444 10
                $this->processFieldDirectives($field, $directives);
445
            }
446
        }
447
448
        /**
449
         * @var \GraphQL\Language\AST\NodeList|null
450
         */
451 10
        $directives = $this->type->astNode->directives;
452 10
        if ($directives) {
453 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

453
            $this->processDirectives(/** @scrutinizer ignore-type */ $directives);
Loading history...
454
        }
455 10
    }
456
457 10
    public function getGenerateFilename(bool $base = true): string
458
    {
459 10
        return $this->getBasePath(self::$modelDir . '/' . ($base ? 'Base' : '') . $this->studlyName . '.php');
460
    }
461
462
    public static function setModelDir(string $dir): void
463
    {
464
        self::$modelDir = $dir;
465
    }
466
}
467