Passed
Push — master ( cab6b5...845ff4 )
by Bruno
04:13 queued 01:09
created

ModelGenerator   A

Complexity

Total Complexity 38

Size/Duplication

Total Lines 429
Duplicated Lines 0 %

Test Coverage

Coverage 93.75%

Importance

Changes 28
Bugs 12 Features 1
Metric Value
eloc 202
dl 0
loc 429
ccs 180
cts 192
cp 0.9375
rs 9.36
c 28
b 12
f 1
wmc 38

9 Methods

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

443
            $this->processTypeDirectives(/** @scrutinizer ignore-type */ $directives, 'Model');
Loading history...
444
        }
445 10
    }
446
447 10
    public function getGenerateFilename(bool $base = true): string
448
    {
449 10
        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