Passed
Push — master ( 450d12...f65752 )
by Bruno
07:03
created

FrontendGenerator::getOptions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

282
    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...
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

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