FrontendGenerator::generate()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 23
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 3
eloc 10
nc 3
nop 0
dl 0
loc 23
rs 9.9332
c 2
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace Modelarium\Frontend;
4
5
use Formularium\CodeGenerator\GraphQL\CodeGenerator as GraphQLCodeGenerator;
6
use Formularium\Element;
7
use Formularium\Field;
8
use Formularium\Framework;
9
use Formularium\Model;
10
use Formularium\FrameworkComposer;
11
use Formularium\Frontend\HTML\Element\Button;
12
use Formularium\Frontend\HTML\Element\Table;
13
use Formularium\Frontend\Vue\Framework as FrameworkVue;
14
use Formularium\HTMLNode;
15
use Formularium\Renderable;
16
use GraphQL\Type\Definition\CustomScalarType;
17
use GraphQL\Type\Definition\FieldArgument;
18
use GraphQL\Type\Definition\InputObjectType;
19
use GraphQL\Type\Definition\InputType;
20
use GraphQL\Type\Definition\ListOfType;
21
use GraphQL\Type\Definition\NonNull;
22
use GraphQL\Type\Definition\ScalarType as DefinitionScalarType;
23
use Modelarium\Exception\Exception;
24
use Modelarium\Parser;
25
use Modelarium\GeneratedCollection;
26
use Modelarium\GeneratedItem;
27
use Modelarium\GeneratorInterface;
28
use Modelarium\GeneratorNameTrait;
29
use Modelarium\Options;
30
31
use function Safe\json_encode;
32
33
class FrontendGenerator implements GeneratorInterface
34
{
35
    use GeneratorNameTrait;
36
37
    /**
38
     *
39
     * @var Options
40
     */
41
    protected $options = null;
42
43
    /**
44
     * @var FrameworkComposer
45
     */
46
    protected $composer = null;
47
48
    /**
49
     * @var Model
50
     */
51
    protected $fModel = null;
52
53
    /**
54
     * @var Parser
55
     */
56
    protected $parser = null;
57
58
    /**
59
     * @var GeneratedCollection
60
     */
61
    protected $collection;
62
63
    /**
64
     *
65
     * @var string
66
     */
67
    protected $stubDir = __DIR__ . '/stubs';
68
69
    /**
70
     * Attributed used to fetch the item. It must be a unique key, and
71
     * defaults to 'id'.
72
     *
73
     * @var string
74
     */
75
    protected $keyAttribute = 'id';
76
77
    /**
78
     * Attributed used to fetch the item. It must be a unique key, and
79
     * defaults to lowerName.
80
     *
81
     * @var string
82
     */
83
    protected $routeBase = '';
84
85
    /**
86
     * String substitution
87
     *
88
     * @var array
89
     */
90
    public $templateParameters = [];
91
92
    /**
93
     * Card fields
94
     *
95
     * @var Field[]
96
     */
97
    protected $cardFields = [];
98
99
    /**
100
     * Table fields
101
     *
102
     * @var Field[]
103
     */
104
    protected $tableFields = [];
105
106
    /**
107
     * title fields
108
     *
109
     * @var Field[]
110
     */
111
    protected $titleFields = [];
112
113
    public function __construct(FrameworkComposer $composer, Model $model, Parser $parser)
114
    {
115
        $this->composer = $composer;
116
        $this->fModel = $model;
117
        $this->options = new Options();
118
        $this->setBaseName($model->getName());
119
        // TODO: document keyAttribute renderable parameter
120
        $this->keyAttribute = $model->getRenderable('keyAttribute', 'id');
121
        $this->routeBase = $this->fModel->getRenderable('routeBase', $this->lowerName);
122
        $this->parser = $parser;
123
        $this->buildTemplateParameters();
124
        $userPath = $this->options->getBasePath() . '/resources/modelarium/stubs/';
125
        if (is_dir($userPath)) {
126
            $this->stubDir = $userPath;
127
        }
128
    }
129
130
    public function generate(): GeneratedCollection
131
    {
132
        $this->collection = new GeneratedCollection();
133
        if ($this->fModel->getExtradata('frontendSkip')) {
134
            return $this->collection;
135
        }
136
137
        /**
138
         * @var FrameworkVue $vue
139
         */
140
        $vue = $this->composer->getByName('Vue');
141
        // $blade = FrameworkComposer::getByName('Blade');
142
143
        $this->makeJSModel();
144
145
        if ($vue !== null) {
146
            $vueGenerator = new FrontendVueGenerator($this);
147
            $vueGenerator->generate();
148
        }
149
150
        $this->makeGraphql();
151
152
        return $this->collection;
153
    }
154
155
    public function buildTemplateParameters(): void
156
    {
157
        $this->titleFields = $this->fModel->filterField(
158
            function (Field $field) {
159
                return $field->getRenderable('title', false);
160
            }
161
        );
162
        $this->cardFields = $this->fModel->filterField(
163
            function (Field $field) {
164
                return $field->getRenderable('card', false);
165
            }
166
        );
167
        $this->tableFields = $this->fModel->filterField(
168
            function (Field $field) {
169
                return $field->getRenderable('table', false);
170
            }
171
        );
172
173
        $buttonCreate = $this->composer->nodeElement(
174
            'Button',
175
            [
176
                Button::TYPE => 'a',
177
                Button::ATTRIBUTES => [
178
                    'href' => "/{$this->routeBase}/edit"
179
                ],
180
            ]
181
        )->setContent(
182
            '<i class="fa fa-plus"></i> '  . $this->getOptions()->getOption('frontend', 'messages')['addNew'],
183
            true,
184
            true
185
        )->getRenderHTML();
186
187
        $buttonEdit = $this->composer->nodeElement(
188
            'Button',
189
            [
190
                Button::TYPE => 'a',
191
                Button::ATTRIBUTES => [
192
                    ':to' => "'/{$this->lowerName}/' + model.{$this->keyAttribute} + '/edit'"
193
                ],
194
            ]
195
        )->setContent(
196
            '<i class="fa fa-pencil"></i> ' . $this->getOptions()->getOption('frontend', 'messages')['edit'],
197
            true,
198
            true
199
        )->getRenderHTML();
200
201
        $buttonDelete = $this->composer->nodeElement(
202
            'Button',
203
            [
204
                Button::TYPE => 'a',
205
                Button::COLOR => Button::COLOR_WARNING,
206
                Button::ATTRIBUTES => [
207
                    'href' => '#',
208
                    '@click.prevent' => 'remove'
209
                ],
210
            ]
211
        )->setContent(
212
            '<i class="fa fa-trash"></i> ' . $this->getOptions()->getOption('frontend', 'messages')['delete'],
213
            true,
214
            true
215
        )->getRenderHTML();
216
217
        /*
218
         * table
219
         */
220
        $table = $this->composer->nodeElement(
221
            'Table',
222
            [
223
                Table::ROW_NAMES => array_map(
224
                    function (Field $field) {
225
                        return $field->getRenderable(Renderable::LABEL, $field->getName());
226
                    },
227
                    $this->tableFields
228
                ),
229
                Table::STRIPED => true
230
            ]
231
        );
232
233
        // TODO: this has vue code, move to vue
234
        /**
235
         * @var HTMLNode $tbody
236
         */
237
        $tbody = $table->get('tbody')[0];
238
        $tbody->setContent(
239
            '<' . $this->studlyName . 'TableItem v-for="l in list" :key="l.id" v-bind="l">' .
240
            '<template v-for="(_, slot) in $scopedSlots" #[slot]="props"><slot :name="slot" v-bind="props" /></template>' .
241
            '</' . $this->studlyName . 'TableItem>',
242
            true,
243
            true
244
        );
245
        foreach ($table->get('tr') as $t) {
246
            $t->appendContent('<slot name="extraHeaders" :props="$props"></slot>', true);
247
        }
248
        $titleFields = $this->fModel->filterField(
249
            function (Field $field) {
250
                return $field->getRenderable('title', false);
251
            }
252
        );
253
254
        $spinner = $this->composer->nodeElement('Spinner')
255
        ->addAttribute(
256
            'v-show',
257
            'isLoading'
258
        )->getRenderHTML();
259
260
        $this->templateParameters = [
261
            'buttonSubmit' => $this->composer->element(
262
                'Button',
263
                [
264
                    Button::TYPE => 'submit',
265
                    Element::LABEL => $this->getOptions()->getOption("frontend", "submit", "Submit")
266
                ]
267
            ),
268
            'buttonCreate' => $buttonCreate,
269
            'buttonEdit' => $buttonEdit,
270
            'buttonDelete' => $buttonDelete,
271
            'filters' => $this->getFilters(),
272
            'keyAttribute' => $this->keyAttribute,
273
            'spinner' => $spinner,
274
            'routeBase' => $this->getRouteBase(),
275
            'tablelist' => $table->getRenderHTML(),
276
            'tableItemFields' => array_keys(array_map(function (Field $f) {
277
                return $f->getName();
278
            }, $this->tableFields)),
279
            'typeTitle' => $this->fModel->getRenderable('name', $this->studlyName),
280
            'titleField' => array_key_first($titleFields) ?: 'id'
281
        ];
282
    }
283
284
    public function templateCallback(string $stub, Framework $f, array $data, Model $m): string
0 ignored issues
show
Unused Code introduced by
The parameter $m is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

284
    public function templateCallback(string $stub, Framework $f, array $data, /** @scrutinizer ignore-unused */ Model $m): string

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $f is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

284
    public function templateCallback(string $stub, /** @scrutinizer ignore-unused */ Framework $f, array $data, Model $m): string

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
285
    {
286
        $x = $this->templateFile(
287
            $stub,
288
            array_merge(
289
                $this->templateParameters,
290
                $data
291
            )
292
        );
293
        return $x;
294
    }
295
296
    /**
297
     * Filters for query, which might be used by component for rendering and props
298
     *
299
     * @return array
300
     */
301
    protected function getFilters(): array
302
    {
303
        $query = $this->parser->getSchema()->getQueryType();
304
        $filters = [];
305
        // find the query that matches our pagination model
306
        foreach ($query->getFields() as $field) {
307
            if (mb_strtolower($field->name) === $this->lowerNamePlural) {
308
                // found. parse its parameters.
309
310
                /**
311
                 * @var FieldArgument $arg
312
                 */
313
                foreach ($field->args as $arg) {
314
                    // if you need to parse directives: $directives = $arg->astNode->directives;
315
316
                    $type = $arg->getType();
317
318
                    // this code for serializarion is crap. review it.
319
                    $isRequired = false;
320
                    $isArray = false;
321
                    // TODO phpstan $isInternalRequired = false;
322
                    $isInternalRequiredString = '';
323
                    if ($type instanceof NonNull) {
324
                        $type = $type->getWrappedType();
325
                        $isRequired = true;
326
                    }
327
328
                    if ($type instanceof ListOfType) {
329
                        $isArray = true;
330
                        $type = $type->getWrappedType();
331
                        if ($type instanceof NonNull) {
332
                            // TODO phpstan $isInternalRequired = true;
333
                            $isInternalRequiredString = '!';
334
                            $type = $type->getWrappedType();
335
                        }
336
                    }
337
338
                    if ($type instanceof CustomScalarType) {
339
                        $typename = $type->astNode->name->value;
340
                    } elseif ($type instanceof DefinitionScalarType) {
341
                        $typename = $type->name;
342
                    } elseif ($type instanceof InputObjectType) {
343
                        $typename = $type->name;
344
                    }
345
                    // } elseif ($type instanceof Input with @spread) {
346
                    else {
347
                        // TODO throw new Exception("Unsupported type {$arg->name} in query filter generation for {$this->baseName} " . get_class($type));
348
                        continue;
349
                    }
350
351
                    $filters[] = [
352
                        'name' => $arg->name,
353
                        'type' => $typename,
354
                        'graphqlType' =>  $arg->name  . ': ' .
355
                            ($isArray ? '[' : '') .
356
                            $typename .
357
                            $isInternalRequiredString . // TODO: phpstan complains, issue with graphqlphp ($isInternalRequired ? '!' : '') .
358
                            ($isArray ? ']' : '') .
359
                            ($isRequired ? '!' : ''),
360
                        'required' => (bool)$isRequired,
361
                        'requiredJSBoolean' => $isRequired ? 'true' : 'false'
362
                    ];
363
                }
364
                break;
365
            }
366
        }
367
        return $filters;
368
    }
369
370
    protected function makeGraphql(): void
371
    {
372
        $gcg = new GraphQLCodeGenerator();
373
374
        /*
375
         * card
376
         */
377
        $cardFieldNames = array_map(
378
            function (Field $field) {
379
                return $field->getName();
380
            },
381
            $this->cardFields
382
        );
383
        $graphqlQuery = $this->fModel->mapFields(
384
            function (Field $f) use ($cardFieldNames, $gcg) {
385
                if (in_array($f->getName(), $cardFieldNames)) {
386
                    // TODO: filter subfields in relationships
387
                    return $gcg->variable($f);
388
                }
389
                return null;
390
            }
391
        );
392
        $cardFieldParameters = join("\n", array_filter($graphqlQuery));
393
394
        // generate filters for query
395
        $filters = $this->templateParameters['filters'] ?? [];
396
        if ($filters) {
397
            $filtersQuery = ', ' . join(
398
                ', ',
399
                array_map(
400
                    function ($item) {
401
                        // TODO: still buggy, misses the internal ! in [Xyz!]!
402
                        return '$' . $item['graphqlType'];
403
                    },
404
                    $filters
405
                )
406
            );
407
            $filtersParams = ', ' . join(
408
                ', ',
409
                array_map(
410
                    function ($item) {
411
                        return $item['name'] . ': $' . $item['name'];
412
                    },
413
                    $filters
414
                )
415
            );
416
        } else {
417
            $filtersQuery = $filtersParams = '';
418
        }
419
420
        $listQuery = <<<EOF
421
query (\$page: Int!$filtersQuery) {
422
    {$this->lowerNamePlural}(page: \$page$filtersParams) {
423
        data {
424
            id
425
            $cardFieldParameters
426
        }
427
428
        paginatorInfo {
429
            currentPage
430
            perPage
431
            total
432
            lastPage
433
        }
434
    }
435
}
436
EOF;
437
        $this->collection->push(
438
            new GeneratedItem(
439
                GeneratedItem::TYPE_FRONTEND,
440
                $listQuery,
441
                $this->fModel->getName() . '/queryList.graphql'
442
            )
443
        );
444
445
        /*
446
         * table
447
         */
448
        $tableFieldNames = array_map(
449
            function (Field $field) {
450
                return $field->getName();
451
            },
452
            $this->tableFields
453
        );
454
455
        $graphqlQuery = $this->fModel->mapFields(
456
            function (Field $f) use ($tableFieldNames, $gcg) {
457
                if (in_array($f->getName(), $tableFieldNames)) {
458
                    // TODO: filter subfields in relationships
459
                    return $gcg->variable($f);
460
                }
461
                return null;
462
            }
463
        );
464
465
        $tableFieldParameters = join("\n", array_filter($graphqlQuery));
466
467
        $tableQuery = <<<EOF
468
query (\$page: Int!$filtersQuery) {
469
    {$this->lowerNamePlural}(page: \$page$filtersParams) {
470
        data {
471
            id
472
            $tableFieldParameters
473
        }
474
475
        paginatorInfo {
476
            currentPage
477
            perPage
478
            total
479
            lastPage
480
        }
481
    }
482
}
483
EOF;
484
        $this->collection->push(
485
            new GeneratedItem(
486
                GeneratedItem::TYPE_FRONTEND,
487
                $tableQuery,
488
                $this->fModel->getName() . '/queryTable.graphql'
489
            )
490
        );
491
492
        /*
493
         * item
494
         */
495
        $graphqlQuery = $this->fModel->mapFields(
496
            function (Field $f) use ($gcg) {
497
                return \Modelarium\Frontend\Util::fieldShow($f) ? $gcg->variable($f) : null;
498
            }
499
        );
500
        $graphqlQuery = join("\n", array_filter($graphqlQuery));
501
502
        $hasCan = $this->fModel->getExtradataValue('hasCan', 'value', false);
503
        $canAttribute = $hasCan ? "can {\nability\nvalue\n}" : '';
504
        if ($this->keyAttribute === 'id') {
505
            $keyAttributeType = 'ID';
506
        } else {
507
            $keyAttributeType = $gcg->datatypeDeclaration($this->fModel->getField($this->keyAttribute)->getDatatype());
508
        }
509
510
        $itemQuery = <<<EOF
511
query (\${$this->keyAttribute}: {$keyAttributeType}!) {
512
    {$this->lowerName}({$this->keyAttribute}: \${$this->keyAttribute}) {
513
        id
514
        $graphqlQuery
515
        $canAttribute
516
    }
517
}
518
EOF;
519
520
        $this->collection->push(
521
            new GeneratedItem(
522
                GeneratedItem::TYPE_FRONTEND,
523
                $itemQuery,
524
                $this->fModel->getName() . '/queryItem.graphql'
525
            )
526
        );
527
528
        $createMutation = <<<EOF
529
mutation(\$input: Create{$this->studlyName}Input!) {
530
    create{$this->studlyName}(input: \$input) {
531
        id
532
        $graphqlQuery
533
        $canAttribute
534
    }
535
}
536
EOF;
537
        $this->collection->push(
538
            new GeneratedItem(
539
                GeneratedItem::TYPE_FRONTEND,
540
                $createMutation,
541
                $this->fModel->getName() . '/mutationCreate.graphql'
542
            )
543
        );
544
545
        $upsertMutation = <<<EOF
546
mutation(\$input: Update{$this->studlyName}Input!) {
547
    update{$this->studlyName}(input: \$input) {
548
        id
549
        $graphqlQuery
550
        $canAttribute
551
    }
552
}
553
EOF;
554
        $this->collection->push(
555
            new GeneratedItem(
556
                GeneratedItem::TYPE_FRONTEND,
557
                $upsertMutation,
558
                $this->fModel->getName() . '/mutationUpdate.graphql'
559
            )
560
        );
561
562
        $deleteMutation = <<<EOF
563
mutation delete(\$id: ID!) {
564
    delete{$this->studlyName}(id: \$id) {
565
        id
566
    }
567
}
568
EOF;
569
        $this->collection->push(
570
            new GeneratedItem(
571
                GeneratedItem::TYPE_FRONTEND,
572
                $deleteMutation,
573
                $this->fModel->getName() . '/mutationDelete.graphql'
574
            )
575
        );
576
    }
577
578
    protected function makeJSModel(): void
579
    {
580
        $path = $this->fModel->getName() . '/model.js';
581
        $modelValues = $this->fModel->getDefault();
582
        $modelValues['id'] = 0;
583
584
        $hasCan = $this->fModel->getExtradataValue('hasCan', 'value', false);
585
        if ($hasCan) {
586
            $modelValues['can'] = [];
587
        }
588
589
        $modelJS = 'const model = ' . json_encode($modelValues) .
590
            ";\n\nexport default model;\n";
591
592
        $this->collection->push(
593
            new GeneratedItem(
594
                GeneratedItem::TYPE_FRONTEND,
595
                $modelJS,
596
                $path
597
            )
598
        );
599
    }
600
601
    /**
602
     * Get the value of composer
603
     *
604
     * @return  FrameworkComposer
605
     */
606
    public function getComposer(): FrameworkComposer
607
    {
608
        return $this->composer;
609
    }
610
611
    /**
612
     * Get the value of collection
613
     *
614
     * @return  GeneratedCollection
615
     */
616
    public function getCollection(): GeneratedCollection
617
    {
618
        return $this->collection;
619
    }
620
621
    /**
622
     * Get card fields
623
     *
624
     * @return  Field[]
625
     */
626
    public function getCardFields(): array
627
    {
628
        return $this->cardFields;
629
    }
630
631
    /**
632
     * Get table fields
633
     *
634
     * @return  Field[]
635
     */
636
    public function getTableFields(): array
637
    {
638
        return $this->tableFields;
639
    }
640
641
    /**
642
     * Get defaults to 'id'.
643
     *
644
     * @return  string
645
     */
646
    public function getKeyAttribute(): string
647
    {
648
        return $this->keyAttribute;
649
    }
650
651
    /**
652
     * Get defaults to lowerName.
653
     *
654
     * @return  string
655
     */
656
    public function getRouteBase(): string
657
    {
658
        return $this->routeBase;
659
    }
660
661
    /**
662
     * Get the value of model
663
     *
664
     * @return  Model
665
     */
666
    public function getModel()
667
    {
668
        return $this->fModel;
669
    }
670
671
    /**
672
     * Get the value of stubDir
673
     *
674
     * @return  string
675
     */
676
    public function getStubDir()
677
    {
678
        return $this->stubDir;
679
    }
680
681
    /**
682
     * Get title fields
683
     *
684
     * @return  Field[]
685
     */
686
    public function getTitleFields()
687
    {
688
        return $this->titleFields;
689
    }
690
691
    /**
692
     * Get the value of options
693
     *
694
     * @return  Options
695
     */
696
    public function getOptions()
697
    {
698
        return $this->options;
699
    }
700
}
701