Passed
Push — master ( 18ced7...ee41eb )
by Bruno
04:30
created

ModelGenerator::generateString()   B

Complexity

Conditions 5
Paths 16

Size

Total Lines 129
Code Lines 91

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 77
CRAP Score 5.1082

Importance

Changes 19
Bugs 7 Features 0
Metric Value
cc 5
eloc 91
c 19
b 7
f 0
nc 16
nop 0
dl 0
loc 129
ccs 77
cts 92
cp 0.837
crap 5.1082
rs 7.8852

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

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