Passed
Push — master ( ee41eb...972703 )
by Bruno
03:32
created

ModelGenerator::processRelationship()   B

Complexity

Conditions 7
Paths 10

Size

Total Lines 51
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 8.4275

Importance

Changes 16
Bugs 6 Features 0
Metric Value
cc 7
eloc 28
c 16
b 6
f 0
nc 10
nop 2
dl 0
loc 51
rs 8.5386
ccs 18
cts 26
cp 0.6923
crap 8.4275

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\\Support\\Facades\\Auth');
310 10
        $namespace->addUse('\\Formularium\\Exception\\NoRandomException');
311 10
        $namespace->addUse('\\Modelarium\\Laravel\\Datatypes\\Datatype_relationship');
312
313 10
        $this->class = $namespace->addClass('Base' . $this->studlyName);
314 10
        $this->class
315 10
            ->addComment("This file was automatically generated by Modelarium.")
316 10
            ->setAbstract();
317
318 10
        $this->methodRandom = new Method('getRandomData');
319 10
        $this->methodRandom->addBody(
320 10
            '$data = static::getFormularium()->getRandom(get_called_class() . \'::getRandomFieldData\');' . "\n"
321
        );
322
323 10
        $this->processGraphql();
324
325
        // this might have changed
326 10
        $this->class->setExtends($this->parentClassName);
327
328 10
        foreach ($this->traits as $trait) {
329 1
            $this->class->addTrait($trait);
330
        }
331
332 10
        $this->class->addProperty('fillable')
333 10
            ->setProtected()
334 10
            ->setValue($this->fillable)
335 10
            ->setComment("The attributes that are mass assignable.\n@var array")
336 10
            ->setInitialized();
337
338 10
        $this->class->addProperty('hidden')
339 10
            ->setProtected()
340 10
            ->setValue($this->hidden)
341 10
            ->setComment("The attributes that should be hidden for arrays.\n@var array")
342 10
            ->setInitialized();
343
344 10
        $this->class->addProperty('with')
345 10
            ->setProtected()
346 10
            ->setValue($this->with)
347 10
            ->setComment("Eager load these relationships.\n@var array")
348 10
            ->setInitialized();
349
350 10
        if (!$this->migrationTimestamps) {
351 9
            $this->class->addProperty('timestamps')
352 9
                ->setPublic()
353 9
                ->setValue(false)
354 9
                ->setComment("Do not set timestamps.\n@var boolean")
355 9
                ->setInitialized();
356
        }
357
358 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...
359
            $this->class->addProperty('casts')
360
                ->setProtected()
361
                ->setValue($this->casts)
362
                ->setComment("The attributes that should be cast.\n@var array")
363
                ->setInitialized();
364
        }
365
366 10
        $this->class->addMethod('getFields')
367 10
            ->setPublic()
368 10
            ->setStatic()
369 10
            ->setReturnType('array')
370 10
            ->addComment('@return array')
371 10
            ->addBody(
372 10
                "return ?;\n",
373
                [
374 10
                    $this->fModel->serialize()
375
                ]
376
            );
377
378 10
        $this->class->addMethod('getFormularium')
379 10
            ->setPublic()
380 10
            ->setStatic()
381 10
            ->setReturnType('\Formularium\Model')
382 10
            ->addComment('@return \Formularium\Model')
383 10
            ->addBody(
384
                '$model = \Formularium\Model::fromStruct(static::getFields());' . "\n" .
385 10
                'return $model;',
386
                [
387
                    //$this->studlyName,
388 10
                ]
389
            );
390
        
391 10
        $this->methodRandom
392 10
            ->addComment('@return array')
393 10
            ->setPublic()
394 10
            ->setStatic()
395 10
            ->setReturnType('array')
396 10
            ->addBody('return $data;');
397 10
        $this->class->addMember($this->methodRandom);
398
399 10
        $this->class->addMethod('getRandomFieldData')
400 10
            ->setPublic()
401 10
            ->setStatic()
402 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.")
403 10
            ->addBody('
404
$d = $f->getDatatype();
405
if ($d instanceof Datatype_relationship) {
406
    throw new NoRandomException($f->getName());
407
}
408
return $f->getDatatype()->getRandom();')
409 10
            ->addParameter('f')->setType('Formularium\Field');
410
411
        // TODO perhaps we can use PolicyGenerator->policyClasses to auto generate
412 10
        if ($this->hasCan) {
413
            $this->class->addMethod('getCanAttribute')
414
                ->setPublic()
415
                ->setReturnType('array')
416
                ->addComment("Returns the policy permissions for actions such as editing or deleting.\n@return array")
417
                ->addBody(
418
                    '$policy = new \\App\\Policies\\' . $this->studlyName . 'Policy();' . "\n" .
419
                    '$user = Auth::user();' . "\n" .
420
                    'return [' . "\n" .
421
                    '    //[ "ability" => "create", "value" => $policy->create($user) ]' . "\n" .
422
                    '];'
423
                );
424
        }
425
        
426 10
        $printer = new \Nette\PhpGenerator\PsrPrinter;
427 10
        return $this->phpHeader() . $printer->printNamespace($namespace);
428
    }
429
430 10
    protected function processGraphql(): void
431
    {
432 10
        foreach ($this->type->getFields() as $field) {
433 10
            $directives = $field->astNode->directives;
434
            if (
435 10
                ($field->type instanceof ObjectType) ||
436 10
                ($field->type instanceof ListOfType) ||
437 10
                ($field->type instanceof UnionType) ||
438 10
                ($field->type instanceof NonNull && (
439 10
                    ($field->type->getWrappedType() instanceof ObjectType) ||
440 10
                    ($field->type->getWrappedType() instanceof ListOfType) ||
441 10
                    ($field->type->getWrappedType() instanceof UnionType)
442
                ))
443
            ) {
444
                // relationship
445 8
                $this->processRelationship($field, $directives);
446
            } else {
447 10
                list($type, $isRequired) = Parser::getUnwrappedType($field->type);
448 10
                $typeName = $type->name;
449 10
                $this->processField($typeName, $field, $directives, $isRequired);
450
            }
451
        }
452
453
        /**
454
         * @var \GraphQL\Language\AST\NodeList|null
455
         */
456 10
        $directives = $this->type->astNode->directives;
457 10
        if ($directives) {
458 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

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