Passed
Push — master ( cab6b5...845ff4 )
by Bruno
04:13 queued 01:09
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 2
Bugs 1 Features 0
Metric Value
cc 7
eloc 33
c 2
b 1
f 0
nc 14
nop 4
dl 0
loc 59
ccs 29
cts 30
cp 0.9667
crap 7.0018
rs 8.4586

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
    /**
129
     * Override to insert extradata
130
     *
131
     * @param \GraphQL\Language\AST\NodeList $directives
132
     * @param string $generatorType
133
     * @return void
134
     */
135 10
    protected function processTypeDirectives(
136
        \GraphQL\Language\AST\NodeList $directives,
137
        string $generatorType
138
    ): void {
139 10
        foreach ($directives as $directive) {
140 1
            $name = $directive->name->value;
141 1
            $this->fModel->appendExtradata(FormulariumUtils::directiveToExtradata($directive));
142
    
143 1
            $className = $this->getDirectiveClass($name, $generatorType);
144 1
            if ($className) {
145 1
                $methodName = "$className::process{$generatorType}TypeDirective";
146
                /** @phpstan-ignore-next-line */
147 1
                $methodName(
148 1
                    $this,
149
                    $directive
150
                );
151
            }
152
        }
153 10
    }
154
155 10
    protected function processField(
156
        string $typeName,
157
        \GraphQL\Type\Definition\FieldDefinition $field,
158
        \GraphQL\Language\AST\NodeList $directives,
159
        bool $isRequired
160
    ): void {
161 10
        $fieldName = $field->name;
162
163 10
        if ($typeName === 'ID') {
164 10
            return;
165
        }
166
167 10
        $scalarType = $this->parser->getScalarType($typeName);
168
169
        /**
170
         * @var Field $fieldFormularium
171
         */
172 10
        $fieldFormularium = null;
173 10
        if (!$scalarType) {
174
            // probably another model
175 8
            $fieldFormularium = FormulariumUtils::getFieldFromDirectives(
176 8
                $fieldName,
177 8
                $typeName,
178 8
                $directives
179
            );
180 5
        } elseif ($scalarType instanceof FormulariumScalarType) {
181 5
            $fieldFormularium = FormulariumUtils::getFieldFromDirectives(
182 5
                $fieldName,
183 5
                $scalarType->getDatatype()->getName(),
184 5
                $directives
185
            );
186
        } else {
187
            return;
188
        }
189
190 10
        if ($isRequired) {
191 10
            $fieldFormularium->setValidatorOption(
192 10
                Datatype::REQUIRED,
193 10
                'value',
194 10
                true
195
            );
196
        }
197
198 10
        foreach ($directives as $directive) {
199 8
            $name = $directive->name->value;
200 8
            $className = $this->getDirectiveClass($name);
201 8
            if ($className) {
202 8
                $methodName = "$className::processModelFieldDirective";
203
                /** @phpstan-ignore-next-line */
204 8
                $methodName(
205 8
                    $this,
206
                    $field,
207
                    $fieldFormularium,
208
                    $directive
209
                );
210
            }
211
        }
212
213 10
        $this->fModel->appendField($fieldFormularium);
214 10
    }
215
216 8
    protected function processRelationship(
217
        \GraphQL\Type\Definition\FieldDefinition $field,
218
        \GraphQL\Language\AST\NodeList $directives
219
    ): void {
220 8
        $lowerName = mb_strtolower($this->getInflector()->singularize($field->name));
221 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...
222
223 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...
224
225 8
        list($type, $isRequired) = Parser::getUnwrappedType($field->type);
226 8
        $typeName = $type->name;
227
228
        // special types that should be skipped.
229 8
        if ($typeName === 'Can') {
230
            $this->hasCan = true;
231
            return;
232
        }
233
234 8
        $relationshipDatatype = null;
235
236 8
        foreach ($directives as $directive) {
237 8
            $name = $directive->name->value;
238
239 8
            $className = $this->getDirectiveClass($name);
240 8
            if ($className) {
241 8
                $methodName = "$className::processModelRelationshipDirective";
242
                /** @phpstan-ignore-next-line */
243 8
                $r = $methodName(
244 8
                    $this,
245
                    $field,
246
                    $directive
247
                );
248 8
                if ($r) {
249 8
                    if ($relationshipDatatype) {
250
                        throw new Exception("Overwriting relationship in {$typeName} for {$field->name} in {$this->lowerName}");
251
                    }
252 8
                    $relationshipDatatype = $r;
253
                }
254 8
                continue;
255
            }
256
        }
257
258 8
        if (!$relationshipDatatype) {
259
            // TODO: generate a warning, perhaps?
260
            // throw new Exception("Could not find a relationship in {$typeName} for {$field->name} in {$sourceTypeName}");
261
            return;
262
        }
263
    
264 8
        $this->processField($relationshipDatatype, $field, $directives, $isRequired);
265
266
        // TODO
267
        // if ($generateRandom) {
268
        //     if ($relationship == RelationshipFactory::RELATIONSHIP_MANY_TO_MANY || $relationship == RelationshipFactory::MORPH_MANY_TO_MANY) {
269
        //         // TODO: do we generate it? seed should do it?
270
        //     } else {
271
        //         $this->methodRandom->addBody(
272
        //             '$data["' . $lowerName . '_id"] = function () {' . "\n" .
273
        //         '    return factory(' . $targetClass . '::class)->create()->id;'  . "\n" .
274
        //         '};'
275
        //         );
276
        //     }
277
        // }
278 8
    }
279
280 8
    public static function getRelationshipDatatypeName(
281
        string $relationship,
282
        bool $isInverse,
283
        string $sourceTypeName,
284
        string $targetTypeName
285
    ): string {
286 8
        return "relationship:" . ($isInverse ? "inverse:" : "") .
287 8
            "$relationship:$sourceTypeName:$targetTypeName";
288
    }
289
290 10
    public function generateString(): string
291
    {
292 10
        $namespace = new \Nette\PhpGenerator\PhpNamespace('App\\Models');
293 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\BelongsTo');
294 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany');
295 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\HasOne');
296 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\HasMany');
297 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\MorphTo');
298 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\MorphOne');
299 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\MorphToMany');
300 10
        $namespace->addUse('\\Illuminate\\Support\\Facades\\Auth');
301 10
        $namespace->addUse('\\Formularium\\Exception\\NoRandomException');
302 10
        $namespace->addUse('\\Modelarium\\Laravel\\Datatypes\\Datatype_relationship');
303
304 10
        $this->class = $namespace->addClass('Base' . $this->studlyName);
305 10
        $this->class
306 10
            ->addComment("This file was automatically generated by Modelarium.")
307 10
            ->setAbstract();
308
309 10
        $this->methodRandom = new Method('getRandomData');
310 10
        $this->methodRandom->addBody(
311 10
            '$data = static::getFormularium()->getRandom(get_called_class() . \'::getRandomFieldData\');' . "\n"
312
        );
313
314 10
        $this->processGraphql();
315
316
        // this might have changed
317 10
        $this->class->setExtends($this->parentClassName);
318
319 10
        foreach ($this->traits as $trait) {
320 1
            $this->class->addTrait($trait);
321
        }
322
323 10
        $this->class->addProperty('fillable')
324 10
            ->setProtected()
325 10
            ->setValue($this->fillable)
326 10
            ->setComment("The attributes that are mass assignable.\n@var array")
327 10
            ->setInitialized();
328
329 10
        $this->class->addProperty('hidden')
330 10
            ->setProtected()
331 10
            ->setValue($this->hidden)
332 10
            ->setComment("The attributes that should be hidden for arrays.\n@var array")
333 10
            ->setInitialized();
334
335 10
        if (!$this->migrationTimestamps) {
336 9
            $this->class->addProperty('timestamps')
337 9
                ->setPublic()
338 9
                ->setValue(false)
339 9
                ->setComment("Do not set timestamps.\n@var boolean")
340 9
                ->setInitialized();
341
        }
342
343 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...
344
            $this->class->addProperty('casts')
345
                ->setProtected()
346
                ->setValue($this->casts)
347
                ->setComment("The attributes that should be cast.\n@var array")
348
                ->setInitialized();
349
        }
350
351 10
        $this->class->addMethod('getFields')
352 10
            ->setPublic()
353 10
            ->setStatic()
354 10
            ->setReturnType('array')
355 10
            ->addComment('@return array')
356 10
            ->addBody(
357 10
                "return ?;\n",
358
                [
359 10
                    $this->fModel->serialize()
360
                ]
361
            );
362
363 10
        $this->class->addMethod('getFormularium')
364 10
            ->setPublic()
365 10
            ->setStatic()
366 10
            ->setReturnType('\Formularium\Model')
367 10
            ->addComment('@return \Formularium\Model')
368 10
            ->addBody(
369
                '$model = \Formularium\Model::fromStruct(static::getFields());' . "\n" .
370 10
                'return $model;',
371
                [
372
                    //$this->studlyName,
373 10
                ]
374
            );
375
        
376 10
        $this->methodRandom
377 10
            ->addComment('@return array')
378 10
            ->setPublic()
379 10
            ->setStatic()
380 10
            ->setReturnType('array')
381 10
            ->addBody('return $data;');
382 10
        $this->class->addMember($this->methodRandom);
383
384 10
        $this->class->addMethod('getRandomFieldData')
385 10
            ->setPublic()
386 10
            ->setStatic()
387 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.")
388 10
            ->addBody('
389
$d = $f->getDatatype();
390
if ($d instanceof Datatype_relationship) {
391
    throw new NoRandomException($f->getName());
392
}
393
return $f->getDatatype()->getRandom();')
394 10
            ->addParameter('f')->setType('Formularium\Field');
395
396
        // TODO perhaps we can use PolicyGenerator->policyClasses to auto generate
397 10
        if ($this->hasCan) {
398 10
            $this->class->addMethod('getCanAttribute')
399 10
                ->setPublic()
400 10
                ->setReturnType('array')
401 10
                ->addComment("Returns the policy permissions for actions such as editing or deleting.\n@return \Formularium\Model")
402 10
                ->addBody(
403 10
                    '$policy = new \\App\\Policies\\' . $this->studlyName . 'Policy();' . "\n" .
404 10
                    '$user = Auth::user();' . "\n" .
405 10
                    'return [' . "\n" .
406 10
                    '    //[ "ability" => "create", "value" => $policy->create($user) ]' . "\n" .
407 10
                    '];'
408
                );
409
        }
410
        
411 10
        $printer = new \Nette\PhpGenerator\PsrPrinter;
412 10
        return $this->phpHeader() . $printer->printNamespace($namespace);
413
    }
414
415 10
    protected function processGraphql(): void
416
    {
417 10
        foreach ($this->type->getFields() as $field) {
418 10
            $directives = $field->astNode->directives;
419
            if (
420 10
                ($field->type instanceof ObjectType) ||
421 10
                ($field->type instanceof ListOfType) ||
422 10
                ($field->type instanceof UnionType) ||
423 10
                ($field->type instanceof NonNull && (
424 10
                    ($field->type->getWrappedType() instanceof ObjectType) ||
425 10
                    ($field->type->getWrappedType() instanceof ListOfType) ||
426 10
                    ($field->type->getWrappedType() instanceof UnionType)
427
                ))
428
            ) {
429
                // relationship
430 8
                $this->processRelationship($field, $directives);
431
            } else {
432 10
                list($type, $isRequired) = Parser::getUnwrappedType($field->type);
433 10
                $typeName = $type->name;
434 10
                $this->processField($typeName, $field, $directives, $isRequired);
435
            }
436
        }
437
438
        /**
439
         * @var \GraphQL\Language\AST\NodeList|null
440
         */
441 10
        $directives = $this->type->astNode->directives;
442 10
        if ($directives) {
443 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

443
            $this->processTypeDirectives(/** @scrutinizer ignore-type */ $directives, 'Model');
Loading history...
444
        }
445 10
    }
446
447 10
    public function getGenerateFilename(bool $base = true): string
448
    {
449 10
        return $this->getBasePath(self::$modelDir . '/' . ($base ? 'Base' : '') . $this->studlyName . '.php');
450
    }
451
452
    public static function setModelDir(string $dir): void
453
    {
454
        self::$modelDir = $dir;
455
    }
456
}
457