Passed
Push — master ( 1d70e3...37a26d )
by Bruno
03:13
created

ModelGenerator::processRelationship()   B

Complexity

Conditions 7
Paths 10

Size

Total Lines 50
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 8.8142

Importance

Changes 17
Bugs 6 Features 0
Metric Value
cc 7
eloc 29
nc 10
nop 2
dl 0
loc 50
ccs 18
cts 27
cp 0.6667
crap 8.8142
rs 8.5226
c 17
b 6
f 0
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