Passed
Push — master ( a56731...735edd )
by Bruno
04:24
created

FrontendGenerator::makeGraphql()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 183
Code Lines 95

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 7
eloc 95
c 1
b 1
f 0
nc 8
nop 0
dl 0
loc 183
rs 7.1757

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

262
    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

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