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
|
|
|
return; |
234
|
|
|
} |
235
|
|
|
|
236
|
8 |
|
$relationshipDatatype = null; |
237
|
|
|
|
238
|
8 |
|
foreach ($directives as $directive) { |
239
|
8 |
|
$name = $directive->name->value; |
240
|
|
|
|
241
|
8 |
|
$className = $this->getDirectiveClass($name); |
242
|
8 |
|
if ($className) { |
243
|
8 |
|
$methodName = "$className::processModelRelationshipDirective"; |
244
|
|
|
/** @phpstan-ignore-next-line */ |
245
|
8 |
|
$r = $methodName( |
246
|
8 |
|
$this, |
247
|
|
|
$field, |
248
|
|
|
$directive, |
249
|
|
|
$relationshipDatatype |
250
|
|
|
); |
251
|
8 |
|
if ($r) { |
252
|
8 |
|
if ($relationshipDatatype) { |
253
|
|
|
throw new Exception("Overwriting relationship in {$typeName} for {$field->name} in {$this->lowerName}"); |
254
|
|
|
} |
255
|
8 |
|
$relationshipDatatype = $r; |
256
|
|
|
} |
257
|
8 |
|
continue; |
258
|
|
|
} |
259
|
|
|
} |
260
|
|
|
|
261
|
8 |
|
if (!$relationshipDatatype) { |
262
|
|
|
// TODO: generate a warning, perhaps? |
263
|
|
|
// throw new Exception("Could not find a relationship in {$typeName} for {$field->name} in {$sourceTypeName}"); |
264
|
|
|
return; |
265
|
|
|
} |
266
|
|
|
|
267
|
8 |
|
$this->processField($relationshipDatatype->getName(), $field, $directives, $isRequired); |
268
|
|
|
|
269
|
|
|
// TODO |
270
|
|
|
// if ($generateRandom) { |
271
|
|
|
// if ($relationship == RelationshipFactory::RELATIONSHIP_MANY_TO_MANY || $relationship == RelationshipFactory::MORPH_MANY_TO_MANY) { |
272
|
|
|
// // TODO: do we generate it? seed should do it? |
273
|
|
|
// } else { |
274
|
|
|
// $this->methodRandom->addBody( |
275
|
|
|
// '$data["' . $lowerName . '_id"] = function () {' . "\n" . |
276
|
|
|
// ' return factory(' . $targetClass . '::class)->create()->id;' . "\n" . |
277
|
|
|
// '};' |
278
|
|
|
// ); |
279
|
|
|
// } |
280
|
|
|
// } |
281
|
8 |
|
} |
282
|
|
|
|
283
|
8 |
|
public static function getRelationshipDatatypeName( |
284
|
|
|
string $relationship, |
285
|
|
|
bool $isInverse, |
286
|
|
|
string $sourceTypeName, |
287
|
|
|
string $targetTypeName |
288
|
|
|
): string { |
289
|
8 |
|
return "relationship:" . ($isInverse ? "inverse:" : "") . |
290
|
8 |
|
"$relationship:$sourceTypeName:$targetTypeName"; |
291
|
|
|
} |
292
|
|
|
|
293
|
10 |
|
public function generateString(): string |
294
|
|
|
{ |
295
|
10 |
|
$namespace = new \Nette\PhpGenerator\PhpNamespace('App\\Models'); |
296
|
10 |
|
$namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\BelongsTo'); |
297
|
10 |
|
$namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany'); |
298
|
10 |
|
$namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\HasOne'); |
299
|
10 |
|
$namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\HasMany'); |
300
|
10 |
|
$namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\MorphTo'); |
301
|
10 |
|
$namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\MorphOne'); |
302
|
10 |
|
$namespace->addUse('\\Illuminate\\Database\\Eloquent\\Relations\\MorphToMany'); |
303
|
10 |
|
$namespace->addUse('\\Illuminate\\Support\\Facades\\Auth'); |
304
|
10 |
|
$namespace->addUse('\\Formularium\\Exception\\NoRandomException'); |
305
|
10 |
|
$namespace->addUse('\\Modelarium\\Laravel\\Datatypes\\Datatype_relationship'); |
306
|
|
|
|
307
|
10 |
|
$this->class = $namespace->addClass('Base' . $this->studlyName); |
308
|
10 |
|
$this->class |
309
|
10 |
|
->addComment("This file was automatically generated by Modelarium.") |
310
|
10 |
|
->setAbstract(); |
311
|
|
|
|
312
|
10 |
|
$this->methodRandom = new Method('getRandomData'); |
313
|
10 |
|
$this->methodRandom->addBody( |
314
|
10 |
|
'$data = static::getFormularium()->getRandom(get_called_class() . \'::getRandomFieldData\');' . "\n" |
315
|
|
|
); |
316
|
|
|
|
317
|
10 |
|
$this->processGraphql(); |
318
|
|
|
|
319
|
|
|
// this might have changed |
320
|
10 |
|
$this->class->setExtends($this->parentClassName); |
321
|
|
|
|
322
|
10 |
|
foreach ($this->traits as $trait) { |
323
|
1 |
|
$this->class->addTrait($trait); |
324
|
|
|
} |
325
|
|
|
|
326
|
10 |
|
$this->class->addProperty('fillable') |
327
|
10 |
|
->setProtected() |
328
|
10 |
|
->setValue($this->fillable) |
329
|
10 |
|
->setComment("The attributes that are mass assignable.\n@var array") |
330
|
10 |
|
->setInitialized(); |
331
|
|
|
|
332
|
10 |
|
$this->class->addProperty('hidden') |
333
|
10 |
|
->setProtected() |
334
|
10 |
|
->setValue($this->hidden) |
335
|
10 |
|
->setComment("The attributes that should be hidden for arrays.\n@var array") |
336
|
10 |
|
->setInitialized(); |
337
|
|
|
|
338
|
10 |
|
$this->class->addProperty('with') |
339
|
10 |
|
->setProtected() |
340
|
10 |
|
->setValue($this->with) |
341
|
10 |
|
->setComment("Eager load these relationships.\n@var array") |
342
|
10 |
|
->setInitialized(); |
343
|
|
|
|
344
|
10 |
|
if (!$this->migrationTimestamps) { |
345
|
9 |
|
$this->class->addProperty('timestamps') |
346
|
9 |
|
->setPublic() |
347
|
9 |
|
->setValue(false) |
348
|
9 |
|
->setComment("Do not set timestamps.\n@var boolean") |
349
|
9 |
|
->setInitialized(); |
350
|
|
|
} |
351
|
|
|
|
352
|
10 |
|
if ($this->casts) { |
|
|
|
|
353
|
|
|
$this->class->addProperty('casts') |
354
|
|
|
->setProtected() |
355
|
|
|
->setValue($this->casts) |
356
|
|
|
->setComment("The attributes that should be cast.\n@var array") |
357
|
|
|
->setInitialized(); |
358
|
|
|
} |
359
|
|
|
|
360
|
10 |
|
$this->class->addMethod('getFields') |
361
|
10 |
|
->setPublic() |
362
|
10 |
|
->setStatic() |
363
|
10 |
|
->setReturnType('array') |
364
|
10 |
|
->addComment('@return array') |
365
|
10 |
|
->addBody( |
366
|
10 |
|
"return ?;\n", |
367
|
|
|
[ |
368
|
10 |
|
$this->fModel->serialize() |
369
|
|
|
] |
370
|
|
|
); |
371
|
|
|
|
372
|
10 |
|
$this->class->addMethod('getFormularium') |
373
|
10 |
|
->setPublic() |
374
|
10 |
|
->setStatic() |
375
|
10 |
|
->setReturnType('\Formularium\Model') |
376
|
10 |
|
->addComment('@return \Formularium\Model') |
377
|
10 |
|
->addBody( |
378
|
|
|
'$model = \Formularium\Model::fromStruct(static::getFields());' . "\n" . |
379
|
10 |
|
'return $model;', |
380
|
|
|
[ |
381
|
|
|
//$this->studlyName, |
382
|
10 |
|
] |
383
|
|
|
); |
384
|
|
|
|
385
|
10 |
|
$this->methodRandom |
386
|
10 |
|
->addComment('@return array') |
387
|
10 |
|
->setPublic() |
388
|
10 |
|
->setStatic() |
389
|
10 |
|
->setReturnType('array') |
390
|
10 |
|
->addBody('return $data;'); |
391
|
10 |
|
$this->class->addMember($this->methodRandom); |
392
|
|
|
|
393
|
10 |
|
$this->class->addMethod('getRandomFieldData') |
394
|
10 |
|
->setPublic() |
395
|
10 |
|
->setStatic() |
396
|
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.") |
397
|
10 |
|
->addBody(' |
398
|
|
|
$d = $f->getDatatype(); |
399
|
|
|
if ($d instanceof Datatype_relationship) { |
400
|
|
|
throw new NoRandomException($f->getName()); |
401
|
|
|
} |
402
|
|
|
return $f->getDatatype()->getRandom();') |
403
|
10 |
|
->addParameter('f')->setType('Formularium\Field'); |
404
|
|
|
|
405
|
|
|
// TODO perhaps we can use PolicyGenerator->policyClasses to auto generate |
406
|
10 |
|
if ($this->hasCan) { |
407
|
|
|
$this->class->addMethod('getCanAttribute') |
408
|
|
|
->setPublic() |
409
|
|
|
->setReturnType('array') |
410
|
|
|
->addComment("Returns the policy permissions for actions such as editing or deleting.\n@return array") |
411
|
|
|
->addBody( |
412
|
|
|
'$policy = new \\App\\Policies\\' . $this->studlyName . 'Policy();' . "\n" . |
413
|
|
|
'$user = Auth::user();' . "\n" . |
414
|
|
|
'return [' . "\n" . |
415
|
|
|
' //[ "ability" => "create", "value" => $policy->create($user) ]' . "\n" . |
416
|
|
|
'];' |
417
|
|
|
); |
418
|
|
|
} |
419
|
|
|
|
420
|
10 |
|
$printer = new \Nette\PhpGenerator\PsrPrinter; |
421
|
10 |
|
return $this->phpHeader() . $printer->printNamespace($namespace); |
422
|
|
|
} |
423
|
|
|
|
424
|
10 |
|
protected function processGraphql(): void |
425
|
|
|
{ |
426
|
10 |
|
foreach ($this->type->getFields() as $field) { |
427
|
10 |
|
$directives = $field->astNode->directives; |
428
|
|
|
if ( |
429
|
10 |
|
($field->type instanceof ObjectType) || |
430
|
10 |
|
($field->type instanceof ListOfType) || |
431
|
10 |
|
($field->type instanceof UnionType) || |
432
|
10 |
|
($field->type instanceof NonNull && ( |
433
|
10 |
|
($field->type->getWrappedType() instanceof ObjectType) || |
434
|
10 |
|
($field->type->getWrappedType() instanceof ListOfType) || |
435
|
10 |
|
($field->type->getWrappedType() instanceof UnionType) |
436
|
|
|
)) |
437
|
|
|
) { |
438
|
|
|
// relationship |
439
|
8 |
|
$this->processRelationship($field, $directives); |
440
|
|
|
} else { |
441
|
10 |
|
list($type, $isRequired) = Parser::getUnwrappedType($field->type); |
442
|
10 |
|
$typeName = $type->name; |
443
|
10 |
|
$this->processField($typeName, $field, $directives, $isRequired); |
444
|
|
|
} |
445
|
|
|
} |
446
|
|
|
|
447
|
|
|
/** |
448
|
|
|
* @var \GraphQL\Language\AST\NodeList|null |
449
|
|
|
*/ |
450
|
10 |
|
$directives = $this->type->astNode->directives; |
451
|
10 |
|
if ($directives) { |
452
|
10 |
|
$this->processTypeDirectives($directives, 'Model'); |
|
|
|
|
453
|
|
|
} |
454
|
10 |
|
} |
455
|
|
|
|
456
|
10 |
|
public function getGenerateFilename(bool $base = true): string |
457
|
|
|
{ |
458
|
10 |
|
return $this->getBasePath(self::$modelDir . '/' . ($base ? 'Base' : '') . $this->studlyName . '.php'); |
459
|
|
|
} |
460
|
|
|
|
461
|
|
|
public static function setModelDir(string $dir): void |
462
|
|
|
{ |
463
|
|
|
self::$modelDir = $dir; |
464
|
|
|
} |
465
|
|
|
} |
466
|
|
|
|
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.