Passed
Push — master ( 5ddc17...136ed3 )
by Bruno
03:47
created

ModelGenerator::generateString()   B

Complexity

Conditions 5
Paths 16

Size

Total Lines 130
Code Lines 92

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 78
CRAP Score 5.1049

Importance

Changes 20
Bugs 7 Features 0
Metric Value
cc 5
eloc 92
c 20
b 7
f 0
nc 16
nop 0
dl 0
loc 130
ccs 78
cts 93
cp 0.8387
crap 5.1049
rs 7.8634

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
            $this->fModel->appendExtradata(
234
                new Extradata(
235
                    'hasCan',
236
                    [ new ExtradataParameter('value', true) ]
0 ignored issues
show
Bug introduced by
true of type true is incompatible with the type string expected by parameter $value of Formularium\ExtradataParameter::__construct(). ( Ignorable by Annotation )

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

236
                    [ new ExtradataParameter('value', /** @scrutinizer ignore-type */ true) ]
Loading history...
237
                )
238
            );
239
            return;
240
        }
241
242 8
        $relationshipDatatype = null;
243
244 8
        foreach ($directives as $directive) {
245 8
            $name = $directive->name->value;
246
247 8
            $className = $this->getDirectiveClass($name);
248 8
            if ($className) {
249 8
                $methodName = "$className::processModelRelationshipDirective";
250
                /** @phpstan-ignore-next-line */
251 8
                $r = $methodName(
252 8
                    $this,
253
                    $field,
254
                    $directive,
255
                    $relationshipDatatype
256
                );
257 8
                if ($r) {
258 8
                    if ($relationshipDatatype) {
259
                        throw new Exception("Overwriting relationship in {$typeName} for {$field->name} in {$this->lowerName}");
260
                    }
261 8
                    $relationshipDatatype = $r;
262
                }
263 8
                continue;
264
            }
265
        }
266
267 8
        if (!$relationshipDatatype) {
268
            // TODO: generate a warning, perhaps?
269
            // throw new Exception("Could not find a relationship in {$typeName} for {$field->name} in {$sourceTypeName}");
270
            return;
271
        }
272
    
273 8
        $this->processField($relationshipDatatype->getName(), $field, $directives, $isRequired);
274
275
        // TODO
276
        // if ($generateRandom) {
277
        //     if ($relationship == RelationshipFactory::RELATIONSHIP_MANY_TO_MANY || $relationship == RelationshipFactory::MORPH_MANY_TO_MANY) {
278
        //         // TODO: do we generate it? seed should do it?
279
        //     } else {
280
        //         $this->methodRandom->addBody(
281
        //             '$data["' . $lowerName . '_id"] = function () {' . "\n" .
282
        //         '    return factory(' . $targetClass . '::class)->create()->id;'  . "\n" .
283
        //         '};'
284
        //         );
285
        //     }
286
        // }
287 8
    }
288
289 8
    public static function getRelationshipDatatypeName(
290
        string $relationship,
291
        bool $isInverse,
292
        string $sourceTypeName,
293
        string $targetTypeName
294
    ): string {
295 8
        return "relationship:" . ($isInverse ? "inverse:" : "") .
296 8
            "$relationship:$sourceTypeName:$targetTypeName";
297
    }
298
299 10
    public function generateString(): string
300
    {
301 10
        $namespace = new \Nette\PhpGenerator\PhpNamespace('App\\Models');
302 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\BelongsTo');
303 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany');
304 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\HasOne');
305 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\HasMany');
306 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\MorphTo');
307 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\MorphOne');
308 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\MorphToMany');
309 10
        $namespace->addUse('\\Illuminate\\Database\\Eloquent\\Builder');
310 10
        $namespace->addUse('\\Illuminate\\Support\\Facades\\Auth');
311 10
        $namespace->addUse('\\Formularium\\Exception\\NoRandomException');
312 10
        $namespace->addUse('\\Modelarium\\Laravel\\Datatypes\\Datatype_relationship');
313
314 10
        $this->class = $namespace->addClass('Base' . $this->studlyName);
315 10
        $this->class
316 10
            ->addComment("This file was automatically generated by Modelarium.")
317 10
            ->setAbstract();
318
319 10
        $this->methodRandom = new Method('getRandomData');
320 10
        $this->methodRandom->addBody(
321 10
            '$data = static::getFormularium()->getRandom(get_called_class() . \'::getRandomFieldData\');' . "\n"
322
        );
323
324 10
        $this->processGraphql();
325
326
        // this might have changed
327 10
        $this->class->setExtends($this->parentClassName);
328
329 10
        foreach ($this->traits as $trait) {
330 1
            $this->class->addTrait($trait);
331
        }
332
333 10
        $this->class->addProperty('fillable')
334 10
            ->setProtected()
335 10
            ->setValue($this->fillable)
336 10
            ->setComment("The attributes that are mass assignable.\n@var array")
337 10
            ->setInitialized();
338
339 10
        $this->class->addProperty('hidden')
340 10
            ->setProtected()
341 10
            ->setValue($this->hidden)
342 10
            ->setComment("The attributes that should be hidden for arrays.\n@var array")
343 10
            ->setInitialized();
344
345 10
        $this->class->addProperty('with')
346 10
            ->setProtected()
347 10
            ->setValue($this->with)
348 10
            ->setComment("Eager load these relationships.\n@var array")
349 10
            ->setInitialized();
350
351 10
        if (!$this->migrationTimestamps) {
352 9
            $this->class->addProperty('timestamps')
353 9
                ->setPublic()
354 9
                ->setValue(false)
355 9
                ->setComment("Do not set timestamps.\n@var boolean")
356 9
                ->setInitialized();
357
        }
358
359 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...
360
            $this->class->addProperty('casts')
361
                ->setProtected()
362
                ->setValue($this->casts)
363
                ->setComment("The attributes that should be cast.\n@var array")
364
                ->setInitialized();
365
        }
366
367 10
        $this->class->addMethod('getFields')
368 10
            ->setPublic()
369 10
            ->setStatic()
370 10
            ->setReturnType('array')
371 10
            ->addComment('@return array')
372 10
            ->addBody(
373 10
                "return ?;\n",
374
                [
375 10
                    $this->fModel->serialize()
376
                ]
377
            );
378
379 10
        $this->class->addMethod('getFormularium')
380 10
            ->setPublic()
381 10
            ->setStatic()
382 10
            ->setReturnType('\Formularium\Model')
383 10
            ->addComment('@return \Formularium\Model')
384 10
            ->addBody(
385
                '$model = \Formularium\Model::fromStruct(static::getFields());' . "\n" .
386 10
                'return $model;',
387
                [
388
                    //$this->studlyName,
389 10
                ]
390
            );
391
        
392 10
        $this->methodRandom
393 10
            ->addComment('@return array')
394 10
            ->setPublic()
395 10
            ->setStatic()
396 10
            ->setReturnType('array')
397 10
            ->addBody('return $data;');
398 10
        $this->class->addMember($this->methodRandom);
399
400 10
        $this->class->addMethod('getRandomFieldData')
401 10
            ->setPublic()
402 10
            ->setStatic()
403 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.")
404 10
            ->addBody('
405
$d = $f->getDatatype();
406
if ($d instanceof Datatype_relationship) {
407
    throw new NoRandomException($f->getName());
408
}
409
return $f->getDatatype()->getRandom();')
410 10
            ->addParameter('f')->setType('Formularium\Field');
411
412
        // TODO perhaps we can use PolicyGenerator->policyClasses to auto generate
413 10
        if ($this->hasCan) {
414
            $this->class->addMethod('getCanAttribute')
415
                ->setPublic()
416
                ->setReturnType('array')
417
                ->addComment("Returns the policy permissions for actions such as editing or deleting.\n@return array")
418
                ->addBody(
419
                    '$policy = new \\App\\Policies\\' . $this->studlyName . 'Policy();' . "\n" .
420
                    '$user = Auth::user();' . "\n" .
421
                    'return [' . "\n" .
422
                    '    //[ "ability" => "create", "value" => $policy->create($user) ]' . "\n" .
423
                    '];'
424
                );
425
        }
426
        
427 10
        $printer = new \Nette\PhpGenerator\PsrPrinter;
428 10
        return $this->phpHeader() . $printer->printNamespace($namespace);
429
    }
430
431 10
    protected function processGraphql(): void
432
    {
433 10
        foreach ($this->type->getFields() as $field) {
434 10
            $directives = $field->astNode->directives;
435
            if (
436 10
                ($field->type instanceof ObjectType) ||
437 10
                ($field->type instanceof ListOfType) ||
438 10
                ($field->type instanceof UnionType) ||
439 10
                ($field->type instanceof NonNull && (
440 10
                    ($field->type->getWrappedType() instanceof ObjectType) ||
441 10
                    ($field->type->getWrappedType() instanceof ListOfType) ||
442 10
                    ($field->type->getWrappedType() instanceof UnionType)
443
                ))
444
            ) {
445
                // relationship
446 8
                $this->processRelationship($field, $directives);
447
            } else {
448 10
                list($type, $isRequired) = Parser::getUnwrappedType($field->type);
449 10
                $typeName = $type->name;
450 10
                $this->processField($typeName, $field, $directives, $isRequired);
451
            }
452
        }
453
454
        /**
455
         * @var \GraphQL\Language\AST\NodeList|null
456
         */
457 10
        $directives = $this->type->astNode->directives;
458 10
        if ($directives) {
459 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

459
            $this->processTypeDirectives(/** @scrutinizer ignore-type */ $directives, 'Model');
Loading history...
460
        }
461 10
    }
462
463 10
    public function getGenerateFilename(bool $base = true): string
464
    {
465 10
        return $this->getBasePath(self::$modelDir . '/' . ($base ? 'Base' : '') . $this->studlyName . '.php');
466
    }
467
468
    public static function setModelDir(string $dir): void
469
    {
470
        self::$modelDir = $dir;
471
    }
472
}
473