Passed
Push — master ( f06b9e...c07204 )
by Bruno
03:23
created

ModelGenerator::processField()   B

Complexity

Conditions 7
Paths 14

Size

Total Lines 59
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 29
CRAP Score 7.0018

Importance

Changes 3
Bugs 1 Features 0
Metric Value
cc 7
eloc 33
nc 14
nop 4
dl 0
loc 59
ccs 29
cts 30
cp 0.9667
crap 7.0018
rs 8.4586
c 3
b 1
f 0

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

481
            $this->processTypeDirectives(/** @scrutinizer ignore-type */ $directives, 'Model');
Loading history...
482
        }
483 10
    }
484
485 10
    public function getGenerateFilename(bool $base = true): string
486
    {
487 10
        return $this->getBasePath(self::$modelDir . '/' . ($base ? 'Base' : '') . $this->studlyName . '.php');
488
    }
489
490
    public static function setModelDir(string $dir): void
491
    {
492
        self::$modelDir = $dir;
493
    }
494
}
495