Passed
Push — master ( d1775e...656062 )
by Bruno
06:55
created

FrontendGenerator::makeJSModel()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 10
c 0
b 0
f 0
dl 0
loc 13
rs 9.9332
cc 1
nc 1
nop 0
1
<?php declare(strict_types=1);
2
3
namespace Modelarium\Frontend;
4
5
use Formularium\Datatype;
6
use Formularium\Element;
7
use Formularium\Field;
8
use Formularium\Model;
9
use Formularium\FrameworkComposer;
10
use Formularium\Frontend\HTML\Element\Button;
11
use Formularium\Frontend\HTML\Element\Table;
12
use Formularium\Frontend\Vue\Element\Pagination as PaginationVue;
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\NonNull;
19
use GraphQL\Type\Definition\ScalarType as DefinitionScalarType;
20
use Modelarium\Exception\Exception;
21
use Modelarium\Parser;
22
use Modelarium\GeneratedCollection;
23
use Modelarium\GeneratedItem;
24
use Modelarium\GeneratorInterface;
25
use Modelarium\GeneratorNameTrait;
26
use Modelarium\Types\ScalarType;
27
28
use function Safe\file_get_contents;
29
use function Safe\json_encode;
30
31
class FrontendGenerator implements GeneratorInterface
32
{
33
    use GeneratorNameTrait;
34
35
    /**
36
     * @var FrameworkComposer
37
     */
38
    protected $composer = null;
39
40
    /**
41
     * @var Model
42
     */
43
    protected $model = null;
44
45
    /**
46
     * @var Parser
47
     */
48
    protected $parser = null;
49
50
    /**
51
     * @var GeneratedCollection
52
     */
53
    protected $collection;
54
55
    /**
56
     *
57
     * @var string
58
     */
59
    protected $stubDir = __DIR__ . '/stubs';
60
61
    /**
62
     * Attributed used to fetch the item. It must be a unique key, and
63
     * defaults to 'id'.
64
     *
65
     * @var string
66
     */
67
    protected $keyAttribute = 'id';
68
69
    /**
70
     * Attributed used to fetch the item. It must be a unique key, and
71
     * defaults to lowerName.
72
     *
73
     * @var string
74
     */
75
    protected $routeBase = '';
76
77
    /**
78
     * String substitution
79
     *
80
     * @var array
81
     */
82
    protected $templateParameters = [];
83
84
    /**
85
     * Card fields
86
     *
87
     * @var Field[]
88
     */
89
    protected $cardFields = [];
90
91
    /**
92
     * Table fields
93
     *
94
     * @var Field[]
95
     */
96
    protected $tableFields = [];
97
98
    public function __construct(FrameworkComposer $composer, Model $model, Parser $parser)
99
    {
100
        $this->composer = $composer;
101
        $this->model = $model;
102
        // TODO: document keyAttribute renderable parameter
103
        $this->keyAttribute = $model->getRenderable('keyAttribute', 'id');
104
        $this->routeBase = $this->model->getRenderable('routeBase', $this->lowerName);
105
        $this->parser = $parser;
106
        $this->setBaseName($model->getName());
107
        $this->buildTemplateParameters();
108
    }
109
110
    public function generate(): GeneratedCollection
111
    {
112
        $this->collection = new GeneratedCollection();
113
114
        /**
115
         * @var FrameworkVue $vue
116
         */
117
        $vue = $this->composer->getByName('Vue');
118
        // $blade = FrameworkComposer::getByName('Blade');
119
        if ($this->lowerName !== 'appellation') {
120
            return $this->collection;
121
        }
122
123
        if ($vue !== null) {
124
            // build the fields for cards and tables
125
            $vueCode = $vue->getVueCode();
126
            $cardFieldNames = array_map(function (Field $f) {
127
                return $f->getName();
128
            }, $this->cardFields);
129
            $tableFieldNames = array_map(function (Field $f) {
130
                return $f->getName();
131
            }, $this->tableFields);
132
133
            // set basic data for vue
134
            var_dump("xxxx");
135
            $extraprops = [
136
                [
137
                    'name' => 'id',
138
                    'type' => 'String',
139
                    'required' => true
140
                ]
141
            ];
142
            $vueCode->setExtraProps($extraprops);
143
144
            // build basic vue components
145
            $this->vuePublish();
146
            $this->makeVuePaginationComponent();
147
            $this->makeJSModel();
148
            $this->vueCodeItem($vue);
149
150
            // card
151
            foreach ($this->cardFields as $f) {
152
                $vueCode->appendExtraProp([
153
                    'name' => $f->getName(),
154
                    'type' => $vueCode->mapTypeToJs($f->getDatatype()),
155
                    'required' => true
156
                ]);
157
            }
158
            $this->makeVue($vue, 'Card', 'viewable', $cardFieldNames);
159
            // reset props
160
            $vueCode->setExtraProps($extraprops);
161
162
            // table
163
            foreach ($this->tableFields as $f) {
164
                $vueCode->appendExtraProp([
165
                    'name' => $f->getName(),
166
                    'type' => $vueCode->mapTypeToJs($f->getDatatype()),
167
                    'required' => true
168
                ]);
169
            }
170
            $this->makeVue($vue, 'TableItem', 'viewable', $tableFieldNames);
171
            $vueCode->setExtraProps($extraprops);
172
173
            $this->makeVue($vue, 'List', 'viewable');
174
            $this->makeVue($vue, 'Table', 'viewable');
175
            $this->makeVue($vue, 'Show', 'viewable');
176
            $this->makeVue($vue, 'Edit', 'editable');
177
            $this->makeVue(
178
                $vue,
179
                'Form',
180
                'editable',
181
                function (Field $f) {
182
                    if (!$f->getExtradata('modelFillable')) {
183
                        return false;
184
                    }
185
                    return true;
186
                }
187
            );
188
            $this->makeVueRoutes();
189
            $this->makeVueIndex();
190
        }
191
192
        $this->makeGraphql();
193
194
        return $this->collection;
195
    }
196
197
    /**
198
     * Publishes the Vue component dependencies
199
     *
200
     * @return void
201
     */
202
    protected function vuePublish(): void
203
    {
204
        $this->collection->push(
205
            new GeneratedItem(
206
                GeneratedItem::TYPE_FRONTEND,
207
                file_get_contents(__DIR__ . "/Vue/Renderable/RelationshipAutocomplete.vue"),
208
                "Modelarium/RelationshipAutocomplete.vue"
209
            )
210
        );
211
        $this->collection->push(
212
            new GeneratedItem(
213
                GeneratedItem::TYPE_FRONTEND,
214
                file_get_contents(__DIR__ . "/Vue/Renderable/RelationshipSelect.vue"),
215
                "Modelarium/RelationshipSelect.vue"
216
            )
217
        );
218
        // $this->collection->push(
219
        //     new GeneratedItem(
220
        //         GeneratedItem::TYPE_FRONTEND,
221
        //         file_get_contents(__DIR__ . "/Vue/Renderable/RelationshipSelectMultiple.vue"),
222
        //         "Modelarium/RelationshipSelectMultiple.vue"
223
        //     )
224
        // );
225
    }
226
227
    protected function makeVuePaginationComponent(): void
228
    {
229
        $pagination = $this->composer->nodeElement(
230
            'Pagination',
231
            [
232
            ]
233
        );
234
        $html = $pagination->getRenderHTML();
235
        $script = PaginationVue::script();
236
237
        $this->collection->push(
238
            new GeneratedItem(
239
                GeneratedItem::TYPE_FRONTEND,
240
                "<template>\n$html\n</template>\n<script>\n$script\n</script>\n",
241
                "Modelarium/Pagination.vue"
242
            )
243
        );
244
    }
245
246
    protected function buildTemplateParameters(): void
247
    {
248
        $hasVue = $this->composer->getByName('Vue');
249
250
        $this->cardFields = $this->model->filterField(
251
            function (Field $field) {
252
                return $field->getRenderable('card', false);
253
            }
254
        );
255
        $this->tableFields = $this->model->filterField(
256
            function (Field $field) {
257
                return $field->getRenderable('table', false);
258
            }
259
        );
260
261
        $buttonCreate = $this->composer->nodeElement(
262
            'Button',
263
            [
264
                Button::TYPE => 'a',
265
                Button::ATTRIBUTES => [
266
                    'href' => "/{$this->routeBase}/edit"
267
                ] + ($hasVue ? [ "v-if" => 'can.create' ]: []),
268
            ]
269
        )->setContent(
270
            '<i class="fa fa-plus"></i> Add new',
271
            true,
272
            true
273
        )->getRenderHTML();
274
275
        $buttonEdit = $this->composer->nodeElement(
276
            'Button',
277
            [
278
                Button::TYPE => ($hasVue ? 'router-link' : 'a'),
279
                Button::ATTRIBUTES => [
280
                    ':to' => "'/{$this->lowerName}/' + model.{$this->keyAttribute} + '/edit'"
281
                ] + ($hasVue ? [ "v-if" => 'can.edit' ]: []),
282
            ]
283
        )->setContent(
284
            '<i class="fa fa-pencil"></i> Edit',
285
            true,
286
            true
287
        )->getRenderHTML();
288
289
        $buttonDelete = $this->composer->nodeElement(
290
            'Button',
291
            [
292
                Button::TYPE => 'a',
293
                Button::COLOR => Button::COLOR_WARNING,
294
                Button::ATTRIBUTES => [
295
                    'href' => '#',
296
                    '@click.prevent' => 'remove'
297
                ] + ($hasVue ? [ "v-if" => 'can.delete' ]: []),
298
            ]
299
        )->setContent(
300
            '<i class="fa fa-trash"></i> Delete',
301
            true,
302
            true
303
        )->getRenderHTML();
304
305
        /*
306
         * table
307
         */
308
        $table = $this->composer->nodeElement(
309
            'Table',
310
            [
311
                Table::ROW_NAMES => array_map(
312
                    function (Field $field) {
313
                        return $field->getRenderable(Renderable::LABEL, $field->getName());
314
                    },
315
                    $this->tableFields
316
                ),
317
                Table::STRIPED => true
318
            ]
319
        );
320
        /**
321
         * @var HTMLNode $tbody
322
         */
323
        $tbody = $table->get('tbody')[0];
324
        $tbody->setContent(
325
            '<' . $this->studlyName . 'TableItem v-for="l in list" :key="l.id" v-bind="l"></' . $this->studlyName . 'TableItem>',
326
            true,
327
            true
328
        );
329
        $titleFields = $this->model->filterField(
330
            function (Field $field) {
331
                return $field->getRenderable('title', false);
332
            }
333
        );
334
335
        $spinner = $this->composer->nodeElement('Spinner')
336
        ->addAttribute(
337
            'v-show',
338
            'isLoading'
339
        )->getRenderHTML();
340
        $this->templateParameters = [
341
            'buttonSubmit' => $this->composer->element(
342
                'Button',
343
                [
344
                    Button::TYPE => 'submit',
345
                    Element::LABEL => 'Submit'
346
                ]
347
            ),
348
            'buttonCreate' => $buttonCreate,
349
            'buttonEdit' => $buttonEdit,
350
            'buttonDelete' => $buttonDelete,
351
            'filters' => $this->getFilters(),
352
            // TODO 'hasCan' => $this->model
353
            'keyAttribute' => $this->keyAttribute,
354
            'spinner' => $spinner,
355
            'tablelist' => $table->getRenderHTML(),
356
            'tableItemFields' => array_keys(array_map(function (Field $f) {
357
                return $f->getName();
358
            }, $this->tableFields)),
359
            'titleField' => array_key_first($titleFields) ?: 'id'
360
        ];
361
    }
362
363
    /**
364
     * Appends computed item
365
     *
366
     * @param FrameworkVue $vue
367
     * @return void
368
     */
369
    protected function vueCodeItem(FrameworkVue $vue): void
370
    {
371
        $vue->getVueCode()->appendComputed(
372
            'link',
373
            'return "/' . $this->routeBase . '/" + this.' . $this->keyAttribute
374
        );
375
    }
376
377
    public function templateCallback(string $stub, FrameworkVue $vue, array $data, Model $m): string
378
    {
379
        $x = $this->templateFile(
380
            $stub,
381
            array_merge(
382
                $this->templateParameters,
383
                $data
384
            )
385
        );
386
        return $x;
387
    }
388
389
    /**
390
     * @param FrameworkVue $vue
391
     * @param string $component
392
     * @param string $mode
393
     * @param string[]|callable $restrictFields
394
     * @return void
395
     */
396
    protected function makeVue(FrameworkVue $vue, string $component, string $mode, $restrictFields = null): void
397
    {
398
        $path = $this->model->getName() . '/' .
399
            $this->model->getName() . $component . '.vue';
400
401
        $stub = $this->stubDir . "/Vue{$component}.mustache.vue";
402
403
        if ($mode == 'editable') {
404
            $vue->setEditableTemplate(
405
                function (FrameworkVue $vue, array $data, Model $m) use ($stub) {
406
                    return $this->templateCallback($stub, $vue, $data, $m);
407
                }
408
            );
409
410
            $this->collection->push(
411
                new GeneratedItem(
412
                    GeneratedItem::TYPE_FRONTEND,
413
                    $this->model->editable($this->composer, [], $restrictFields),
414
                    $path
415
                )
416
            );
417
        } else {
418
            $vue->setViewableTemplate(
419
                function (FrameworkVue $vue, array $data, Model $m) use ($stub) {
420
                    return $this->templateCallback($stub, $vue, $data, $m);
421
                }
422
            );
423
424
            $this->collection->push(
425
                new GeneratedItem(
426
                    GeneratedItem::TYPE_FRONTEND,
427
                    $this->model->viewable($this->composer, [], $restrictFields),
428
                    $path
429
                )
430
            );
431
        }
432
        $vue->resetVueCode();
433
        $vue->getVueCode()->setFieldModelVariable('model.');
434
    }
435
436
    /**
437
     * Filters for query, which might be used by component for rendering and props
438
     *
439
     * @return array
440
     */
441
    protected function getFilters(): array
442
    {
443
        $query = $this->parser->getSchema()->getQueryType();
444
        $filters = [];
445
        // find the query that matches our pagination model
446
        foreach ($query->getFields() as $field) {
447
            if ($field->name === $this->lowerNamePlural) {
448
                // found. parse its parameters.
449
450
                /**
451
                 * @var FieldArgument $arg
452
                 */
453
                foreach ($field->args as $arg) {
454
                    // if you need to parse directives: $directives = $arg->astNode->directives;
455
456
                    $type = $arg->getType();
457
458
                    $required = false;
459
                    if ($type instanceof NonNull) {
460
                        $type = $type->getWrappedType();
461
                        $required = true;
462
                    }
463
464
                    if ($type instanceof CustomScalarType) {
465
                        $typename = $type->astNode->name->value;
466
                    } elseif ($type instanceof DefinitionScalarType) {
467
                        $typename = $type->name;
468
                    }
469
                    // } elseif ($type instanceof Input with @spread) {
470
                    else {
471
                        // TODO throw new Exception("Unsupported type {$arg->name} in query filter generation for {$this->baseName} " . get_class($type));
472
                        continue;
473
                    }
474
475
                    $filters[] = [
476
                        'name' => $arg->name,
477
                        'type' => $typename,
478
                        'required' => $required,
479
                        'requiredJSBoolean' => $required ? 'true' : 'false'
480
                    ];
481
                }
482
                break;
483
            }
484
        }
485
        return $filters;
486
    }
487
488
    protected function makeGraphql(): void
489
    {
490
        /*
491
         * card
492
         */
493
        $cardFieldNames = array_map(
494
            function (Field $field) {
495
                return $field->getName();
496
            },
497
            $this->cardFields
498
        );
499
        $cardFieldParameters = implode("\n", $cardFieldNames);
500
501
        // generate filters for query
502
        $filters = $this->templateParameters['filters'] ?? [];
503
        if ($filters) {
504
            $filtersQuery = ', ' . join(
505
                ', ',
506
                array_map(
507
                    function ($item) {
508
                        return '$' . $item['name']  . ': ' . $item['type'] . ($item['required'] ? '!' : '');
509
                    },
510
                    $filters
511
                )
512
            );
513
            $filtersParams = ', ' . join(
514
                ', ',
515
                array_map(
516
                    function ($item) {
517
                        return $item['name'] . ': $' . $item['name'];
518
                    },
519
                    $filters
520
                )
521
            );
522
        } else {
523
            $filtersQuery = $filtersParams = '';
524
        }
525
526
        $listQuery = <<<EOF
527
query (\$page: Int!$filtersQuery) {
528
    {$this->lowerNamePlural}(page: \$page$filtersParams) {
529
        data {
530
            id
531
            $cardFieldParameters
532
        }
533
      
534
        paginatorInfo {
535
            currentPage
536
            perPage
537
            total
538
            lastPage
539
        }
540
    }
541
}
542
EOF;
543
        $this->collection->push(
544
            new GeneratedItem(
545
                GeneratedItem::TYPE_FRONTEND,
546
                $listQuery,
547
                $this->model->getName() . '/queryList.graphql'
548
            )
549
        );
550
551
        /*
552
         * table
553
         */
554
        $tableFieldNames = array_map(
555
            function (Field $field) {
556
                return $field->getName();
557
            },
558
            $this->tableFields
559
        );
560
        $tableFieldParameters = implode("\n", $tableFieldNames);
561
562
        $tableQuery = <<<EOF
563
query (\$page: Int!$filtersQuery) {
564
    {$this->lowerNamePlural}(page: \$page$filtersParams) {
565
        data {
566
            id
567
            $tableFieldParameters
568
        }
569
      
570
        paginatorInfo {
571
            currentPage
572
            perPage
573
            total
574
            lastPage
575
        }
576
    }
577
}
578
EOF;
579
        $this->collection->push(
580
            new GeneratedItem(
581
                GeneratedItem::TYPE_FRONTEND,
582
                $tableQuery,
583
                $this->model->getName() . '/queryTable.graphql'
584
            )
585
        );
586
587
        /*
588
         * item
589
         */
590
        $graphqlQuery = $this->model->mapFields(
591
            function (Field $f) {
592
                return \Modelarium\Frontend\Util::fieldShow($f) ? $f->toGraphqlQuery() : null;
593
            }
594
        );
595
        $graphqlQuery = join("\n", array_filter($graphqlQuery));
596
597
        $hasCan = method_exists($this->model, 'getCanAttribute');
598
        $canAttribute = $hasCan ? 'can' : '';
599
        if ($this->keyAttribute === 'id') {
600
            $keyAttributeType = 'ID';
601
        } else {
602
            $keyAttributeType = $this->model->getField($this->keyAttribute)->getDatatype()->getGraphqlType();
603
        }
604
605
        $itemQuery = <<<EOF
606
query (\${$this->keyAttribute}: {$keyAttributeType}!) {
607
    {$this->lowerName}({$this->keyAttribute}: \${$this->keyAttribute}) {
608
        id
609
        $graphqlQuery
610
        $canAttribute
611
    }
612
}
613
EOF;
614
615
        $this->collection->push(
616
            new GeneratedItem(
617
                GeneratedItem::TYPE_FRONTEND,
618
                $itemQuery,
619
                $this->model->getName() . '/queryItem.graphql'
620
            )
621
        );
622
623
        $upsertMutation = <<<EOF
624
mutation upsert(\${$this->lowerName}: {$this->studlyName}Input!) {
625
    upsert{$this->studlyName}(input: \${$this->lowerName}) {
626
        id
627
    }
628
}
629
EOF;
630
        $this->collection->push(
631
            new GeneratedItem(
632
                GeneratedItem::TYPE_FRONTEND,
633
                $upsertMutation,
634
                $this->model->getName() . '/mutationUpsert.graphql'
635
            )
636
        );
637
638
        $deleteMutation = <<<EOF
639
mutation delete(\$id: ID!) {
640
    delete{$this->studlyName}(id: \$id) {
641
        id
642
    }
643
}
644
EOF;
645
        $this->collection->push(
646
            new GeneratedItem(
647
                GeneratedItem::TYPE_FRONTEND,
648
                $deleteMutation,
649
                $this->model->getName() . '/mutationDelete.graphql'
650
            )
651
        );
652
    }
653
654
    protected function makeVueIndex(): void
655
    {
656
        $path = $this->model->getName() . '/index.js';
657
        $name = $this->studlyName;
658
659
        $items = [
660
            'Card',
661
            'Edit',
662
            'List',
663
            'Show',
664
            'Table',
665
        ];
666
667
        $import = array_map(
668
            function ($i) use ($name) {
669
                return "import {$name}$i from './{$name}$i.vue';";
670
            },
671
            $items
672
        );
673
674
        $export = array_map(
675
            function ($i) use ($name) {
676
                return "    {$name}$i,\n";
677
            },
678
            $items
679
        );
680
681
        $this->collection->push(
682
            new GeneratedItem(
683
                GeneratedItem::TYPE_FRONTEND,
684
                implode("\n", $import) . "\n" .
685
                "export default {\n" .
686
                implode("\n", $export) . "\n};\n",
687
                $path
688
            )
689
        );
690
    }
691
692
    protected function makeVueRoutes(): void
693
    {
694
        $path = $this->model->getName() . '/routes.js';
695
696
        $this->collection->push(
697
            new GeneratedItem(
698
                GeneratedItem::TYPE_FRONTEND,
699
                $this->templateFile(
700
                    $this->stubDir . "/routes.mustache.js",
701
                    [
702
                        'routeName' => $this->routeBase,
703
                        'keyAttribute' => $this->keyAttribute
704
                    ]
705
                ),
706
                $path
707
            )
708
        );
709
    }
710
711
    protected function makeJSModel(): void
712
    {
713
        $path = $this->model->getName() . '/model.js';
714
        $modelValues = $this->model->getDefault();
715
        $modelValues['id'] = 0;
716
        $modelJS = 'const model = ' . json_encode($modelValues) .
717
            ";\n\nexport default model;\n";
718
        
719
        $this->collection->push(
720
            new GeneratedItem(
721
                GeneratedItem::TYPE_FRONTEND,
722
                $modelJS,
723
                $path
724
            )
725
        );
726
    }
727
}
728