Passed
Push — master ( 09030e...cab6b5 )
by Bruno
08:31
created

ModelGenerator::generateString()   B

Complexity

Conditions 5
Paths 16

Size

Total Lines 123
Code Lines 86

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 81
CRAP Score 5.0049

Importance

Changes 18
Bugs 7 Features 0
Metric Value
cc 5
eloc 86
c 18
b 7
f 0
nc 16
nop 0
dl 0
loc 123
ccs 81
cts 86
cp 0.9419
crap 5.0049
rs 7.9943

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 10
    /**
129
     * Override to insert extradata
130
     *
131
     * @param \GraphQL\Language\AST\NodeList $directives
132
     * @param string $generatorType
133
     * @return void
134 10
     */
135
    protected function processTypeDirectives(
136 10
        \GraphQL\Language\AST\NodeList $directives,
137 10
        string $generatorType
138
    ): void {
139
        foreach ($directives as $directive) {
140 10
            $name = $directive->name->value;
141
            $this->fModel->appendExtradata(FormulariumUtils::directiveToExtradata($directive));
142
    
143
            $className = $this->getDirectiveClass($name, $generatorType);
144
            if ($className) {
145 10
                $methodName = "$className::process{$generatorType}TypeDirective";
146 10
                /** @phpstan-ignore-next-line */
147
                $methodName(
148 8
                    $this,
149 8
                    $directive
150 8
                );
151 8
            }
152
        }
153 5
    }
154 5
155 5
    protected function processField(
156 5
        string $typeName,
157 5
        \GraphQL\Type\Definition\FieldDefinition $field,
158
        \GraphQL\Language\AST\NodeList $directives,
159
        bool $isRequired
160
    ): void {
161
        $fieldName = $field->name;
162
163 10
        if ($typeName === 'ID') {
164 10
            return;
165 10
        }
166 10
167 10
        $scalarType = $this->parser->getScalarType($typeName);
168
169
        /**
170
         * @var Field $fieldFormularium
171 10
         */
172 8
        $fieldFormularium = null;
173 8
        if (!$scalarType) {
174 8
            // probably another model
175 8
            $fieldFormularium = FormulariumUtils::getFieldFromDirectives(
176
                $fieldName,
177 8
                $typeName,
178 8
                $directives
179
            );
180
        } elseif ($scalarType instanceof FormulariumScalarType) {
181
            $fieldFormularium = FormulariumUtils::getFieldFromDirectives(
182
                $fieldName,
183
                $scalarType->getDatatype()->getName(),
184
                $directives
185
            );
186 10
        } else {
187 10
            return;
188
        }
189 10
190
        if ($isRequired) {
191
            $fieldFormularium->setValidatorOption(
192
                Datatype::REQUIRED,
193 10
                'value',
194
                true
195 10
            );
196 10
        }
197 10
198
        foreach ($directives as $directive) {
199 8
            $name = $directive->name->value;
200
            $className = $this->getDirectiveClass($name);
201
            if ($className) {
202
                $methodName = "$className::processModelFieldDirective";
203 8
                /** @phpstan-ignore-next-line */
204 8
                $methodName(
205
                    $this,
206 8
                    $field,
207
                    $fieldFormularium,
208 8
                    $directive
209 8
                );
210
            }
211
        }
212 8
213
        $this->fModel->appendField($fieldFormularium);
214
    }
215
216
    protected function processRelationship(
217 8
        \GraphQL\Type\Definition\FieldDefinition $field,
218
        \GraphQL\Language\AST\NodeList $directives
219 8
    ): 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 8
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
        if ($typeName === 'Can') {
230 8
            $this->hasCan = true;
231 8
            return;
232
        }
233
234 8
        $relationshipDatatype = null;
235
236 8
        foreach ($directives as $directive) {
237
            $name = $directive->name->value;
238
239
            $className = $this->getDirectiveClass($name);
240 8
            if ($className) {
241
                $methodName = "$className::processModelRelationshipDirective";
242
                /** @phpstan-ignore-next-line */
243
                $r = $methodName(
244
                    $this,
245
                    $field,
246 8
                    $directive
247
                );
248
                if ($r) {
249
                    if ($relationshipDatatype) {
250
                        throw new Exception("Overwriting relationship in {$typeName} for {$field->name} in {$this->lowerName}");
251
                    }
252
                    $relationshipDatatype = $r;
253
                }
254
                continue;
255
            }
256
        }
257
258
        if (!$relationshipDatatype) {
259
            // TODO: generate a warning, perhaps?
260 8
            // throw new Exception("Could not find a relationship in {$typeName} for {$field->name} in {$sourceTypeName}");
261
            return;
262 8
        }
263
    
264
        $this->processField($relationshipDatatype, $field, $directives, $isRequired);
265
266
        // TODO
267
        // if ($generateRandom) {
268 8
        //     if ($relationship == RelationshipFactory::RELATIONSHIP_MANY_TO_MANY || $relationship == RelationshipFactory::MORPH_MANY_TO_MANY) {
269 8
        //         // TODO: do we generate it? seed should do it?
270
        //     } else {
271
        //         $this->methodRandom->addBody(
272 10
        //             '$data["' . $lowerName . '_id"] = function () {' . "\n" .
273
        //         '    return factory(' . $targetClass . '::class)->create()->id;'  . "\n" .
274
        //         '};'
275 10
        //         );
276 1
        //     }
277 1
        // }
278
    }
279 1
280 1
    public static function getRelationshipDatatypeName(
281 1
        string $relationship,
282
        bool $isInverse,
283 1
        string $sourceTypeName,
284 1
        string $targetTypeName
285
    ): string {
286
        return "relationship:" . ($isInverse ? "inverse:" : "") .
287
            "$relationship:$sourceTypeName:$targetTypeName";
288
    }
289 10
290
    public function generateString(): string
291 10
    {
292
        $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 10
304
        $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 10
309
        $this->methodRandom = new Method('getRandomData');
310 10
        $this->methodRandom->addBody(
311 10
            '$data = static::getFormularium()->getRandom(get_called_class() . \'::getRandomFieldData\');' . "\n"
312 10
        );
313
314
        $this->processGraphql();
315 10
316
        // this might have changed
317
        $this->class->setExtends($this->parentClassName);
318 10
319
        foreach ($this->traits as $trait) {
320 10
            $this->class->addTrait($trait);
321 1
        }
322
323
        $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 10
329
        $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 10
335
        if (!$this->migrationTimestamps) {
336 10
            $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 9
        }
342
343
        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 10
            $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
        $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 10
                [
359
                    $this->fModel->serialize()
360 10
                ]
361
            );
362
363
        $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 10
                '$model = \Formularium\Model::fromStruct(static::getFields());' . "\n" .
370
                'return $model;',
371 10
                [
372
                    //$this->studlyName,
373
                ]
374 10
            );
375
        
376
        $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 10
384
        $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 10
$d = $f->getDatatype();
390
if ($d instanceof Datatype_relationship) {
391
    throw new NoRandomException($f->getName());
392
}
393
return $f->getDatatype()->getRandom();')
394
            ->addParameter('f')->setType('Formularium\Field');
395 10
396
        // TODO perhaps we can use PolicyGenerator->policyClasses to auto generate
397
        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 10
                );
409
        }
410
        
411
        $printer = new \Nette\PhpGenerator\PsrPrinter;
412 10
        return $this->phpHeader() . $printer->printNamespace($namespace);
413 10
    }
414
415
    protected function processGraphql(): void
416 10
    {
417
        foreach ($this->type->getFields() as $field) {
418 10
            $directives = $field->astNode->directives;
419 10
            if (
420
                ($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 10
                ))
428
            ) {
429
                // relationship
430
                $this->processRelationship($field, $directives);
431 8
            } else {
432
                list($type, $isRequired) = Parser::getUnwrappedType($field->type);
433 10
                $typeName = $type->name;
434
                $this->processField($typeName, $field, $directives, $isRequired);
435
            }
436
        }
437
438
        /**
439
         * @var \GraphQL\Language\AST\NodeList|null
440 10
         */
441 10
        $directives = $this->type->astNode->directives;
442 10
        if ($directives) {
443
            $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 10
        }
445
    }
446 10
447
    public function getGenerateFilename(bool $base = true): string
448 10
    {
449
        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