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

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