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

ModelGenerator::getGenerateFilename()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 2
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 1
b 1
f 0
ccs 2
cts 2
cp 1
crap 2
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