Passed
Push — master ( df5659...59487d )
by Bruno
04:26 queued 01:12
created

ModelGenerator::generateString()   B

Complexity

Conditions 5
Paths 16

Size

Total Lines 123
Code Lines 86

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 82
CRAP Score 5.0047

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

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

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