Passed
Push — master ( a266d2...fe3e3c )
by Bruno
03:11
created

ModelGenerator::generateString()   B

Complexity

Conditions 5
Paths 16

Size

Total Lines 149
Code Lines 94

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 80
CRAP Score 5.0984

Importance

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