Passed
Push — master ( 33a1b2...203229 )
by Bruno
09:06
created

Framework::getTemplateData()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 33
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 25
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 33
rs 9.52
1
<?php declare(strict_types=1);
2
3
namespace Formularium\Frontend\Vue;
4
5
use Formularium\Datatype;
6
use Formularium\Datatype\Datatype_bool;
7
use Formularium\Datatype\Datatype_number;
8
use Formularium\Exception\Exception;
9
use Formularium\HTMLNode;
10
use Formularium\Model;
11
12
class Framework extends \Formularium\Framework
13
{
14
    const VUE_MODE_SINGLE_FILE = 'VUE_MODE_SINGLE_FILE';
15
    const VUE_MODE_EMBEDDED = 'VUE_MODE_EMBEDDED';
16
    const VUE_PROP = 'VUE_PROP';
17
    const VUE_VARS = 'VUE_VARS';
18
19
    /**
20
     * @var string
21
     */
22
    protected $mode = self::VUE_MODE_EMBEDDED;
23
24
    /**
25
    * The tag used as container for fields in viewable()
26
    *
27
    * @var string
28
    */
29
    protected $viewableContainerTag = 'div';
30
31
    /**
32
     * The tag used as container for fields in editable()
33
     *
34
     * @var string
35
     */
36
    protected $editableContainerTag = 'div';
37
38
    /**
39
     * The viewable template.
40
     *
41
     * The following variables are replaced:
42
     *
43
     * {{form}}
44
     * {{jsonData}}
45
     * {{containerTag}}
46
     *
47
     * @var string|callable|null
48
     */
49
    protected $viewableTemplate = null;
50
51
    /**
52
     *
53
     *
54
     * @var string|callable|null
55
     */
56
    protected $editableTemplate = null;
57
58
    /**
59
     * Appended to the field variable names to handle models stored in an object field.
60
     *
61
     * Allows you to declare the model like this:
62
     *
63
     * data() {
64
     *   return {
65
     *       model: model,
66
     *   };
67
     * },
68
     *
69
     * @var string
70
     */
71
    protected $fieldModelVariable = '';
72
73
    /**
74
     * Extra props.
75
     *
76
     * @var array
77
     */
78
    protected $extraProps = [];
79
80
    /**
81
     * extra data fields
82
     *
83
     * @var string[]
84
     */
85
    protected $extraData = [];
86
87
    /**
88
     * The list of imports to add: import 'key' from 'value'
89
     *
90
     * @var string[]
91
     */
92
    protected $imports = [];
93
94
    public function __construct(string $name = 'Vue')
95
    {
96
        parent::__construct($name);
97
    }
98
99
    /**
100
     * Static counter to generate unique ids.
101
     *
102
     * @return integer
103
     */
104
    public static function counter(): int
105
    {
106
        static $counter = 0;
107
        return $counter++;
108
    }
109
110
    /**
111
     * Get the tag used as container for fields in viewable()
112
     *
113
     * @return  string
114
     */
115
    public function getViewableContainerTag(): string
116
    {
117
        return $this->viewableContainerTag;
118
    }
119
120
    /**
121
     * Set the tag used as container for fields in viewable()
122
     *
123
     * @param  string  $viewableContainerTag  The tag used as container for fields in viewable()
124
     *
125
     * @return  self
126
     */
127
    public function setViewableContainerTag(string $viewableContainerTag): Framework
128
    {
129
        $this->viewableContainerTag = $viewableContainerTag;
130
        return $this;
131
    }
132
133
    public function getEditableContainerTag(): string
134
    {
135
        return $this->editableContainerTag;
136
    }
137
    
138
    /**
139
     * @param string $tag
140
     * @return self
141
     */
142
    public function setEditableContainerTag(string $tag): Framework
143
    {
144
        $this->editableContainerTag = $tag;
145
        return $this;
146
    }
147
148
    /**
149
     * Get the value of editableTemplate
150
     * @return string|callable|null
151
     */
152
    public function getEditableTemplate()
153
    {
154
        return $this->editableTemplate;
155
    }
156
157
    /**
158
     * Set the value of editableTemplate
159
     *
160
     * @param string|callable|null $editableTemplate
161
     * @return self
162
     */
163
    public function setEditableTemplate($editableTemplate): Framework
164
    {
165
        $this->editableTemplate = $editableTemplate;
166
167
        return $this;
168
    }
169
    
170
    /**
171
     * Get viewable template
172
     *
173
     * @return string|callable|null
174
     */
175
    public function getViewableTemplate()
176
    {
177
        return $this->viewableTemplate;
178
    }
179
180
    /**
181
     * Set viewable tempalte
182
     *
183
     * @param string|callable|null  $viewableTemplate
184
     *
185
     * @return  self
186
     */
187
    public function setViewableTemplate($viewableTemplate)
188
    {
189
        $this->viewableTemplate = $viewableTemplate;
190
191
        return $this;
192
    }
193
    
194
    /**
195
     * Sets the vue render mode, single file component or embedded
196
     *
197
     * @param string $mode self::VUE_MODE_EMBEDDED or self::VUE_MODE_SINGLE_FILE
198
     * @return Framework
199
     */
200
    public function setMode(string $mode): Framework
201
    {
202
        $this->mode = $mode;
203
        return $this;
204
    }
205
206
    /**
207
     * Get the value of mode
208
     *
209
     * @return string
210
     */
211
    public function getMode(): string
212
    {
213
        return $this->mode;
214
    }
215
216
    public function htmlHead(HTMLNode &$head)
217
    {
218
        $head->prependContent(
219
            HTMLNode::factory('script', ['src' => "https://cdn.jsdelivr.net/npm/vue/dist/vue.js"])
220
        );
221
    }
222
223
    public function mapType(Datatype $type): string
224
    {
225
        if ($type instanceof Datatype_number) {
226
            return 'Number';
227
        } elseif ($type instanceof Datatype_bool) {
228
            return 'Boolean';
229
        }
230
        return 'String';
231
    }
232
233
    public function props(Model $m): array
234
    {
235
        $props = [];
236
        foreach ($m->getFields() as $field) {
237
            if ($field->getRenderable(self::VUE_PROP, true)) {
238
                $p = [
239
                    'name' => $field->getName(),
240
                    'type' => $this->mapType($field->getDatatype()),
241
                ];
242
                if ($field->getRenderable(Datatype::REQUIRED, false)) {
243
                    $p['required'] = true;
244
                }
245
                $props[] = $p;
246
            }
247
        }
248
        foreach ($this->extraProps as $p) {
249
            if (!array_key_exists('name', $p)) {
250
                throw new Exception('Missing prop name');
251
            }
252
            $props[] = $p;
253
        }
254
        
255
        return $props;
256
    }
257
258
    protected function serializeProps(array $props): string
259
    {
260
        $s = array_map(function ($p) {
261
            return "'{$p['name']}': { 'type': {$p['type']}" . ($p['required'] ?? false ? ", 'required': true" : '') . " } ";
262
        }, $props);
263
        return "{\n        " . implode(",\n        ", $s) . "\n    }\n";
264
    }
265
266
    /**
267
     * Generates template data for rendering
268
     *
269
     * @param Model $m
270
     * @param array $elements
271
     * @return array
272
     */
273
    protected function getTemplateData(Model $m, array $elements): array
274
    {
275
        $data = array_merge($m->getDefault(), $m->getData());
276
        $form = join('', $elements);
277
        $jsonData = json_encode($data);
278
        $props = $this->props($m);
279
        $propsBind = array_map(
280
            function ($p) {
281
                return 'v-bind:' . $p . '="model.' . $p . '"';
282
            },
283
            array_keys($props)
284
        );
285
        $templateData = [
286
            'form' => $form,
287
            'jsonData' => $jsonData,
288
            'props' => $props,
289
            'propsCode' => $this->serializeProps($props),
290
            'propsBind' => implode(' ', $propsBind),
291
            'imports' => implode(
292
                "\n",
293
                array_map(function ($key, $value) {
294
                    return "import $key from \"$value\";";
295
                }, array_keys($this->imports), $this->imports)
296
            ),
297
            'extraData' => implode(
298
                "\n",
299
                array_map(function ($key, $value) {
300
                    return "  $key: $value,";
301
                }, array_keys($this->extraData), $this->extraData)
302
            )
303
        ];
304
305
        return $templateData;
306
    }
307
308
    public function viewableCompose(Model $m, array $elements, string $previousCompose): string
309
    {
310
        $templateData = $this->getTemplateData($m, $elements);
311
        $templateData['containerTag'] = $this->getViewableContainerTag();
312
313
        if (is_callable($this->viewableTemplate)) {
314
            return call_user_func(
315
                $this->viewableTemplate,
316
                $this,
317
                $templateData,
318
                $m
319
            );
320
        } elseif ($this->mode === self::VUE_MODE_SINGLE_FILE) {
321
            $viewableTemplate = $this->viewableTemplate ? $this->viewableTemplate : <<<EOF
322
<template>
323
<{{containerTag}}>
324
    {{form}}
325
</{{containerTag}}>
326
</template>
327
<script>
328
module.exports = {
329
    data: function () {
330
        return {{jsonData}};
331
    },
332
    methods: {
333
    }
334
};
335
</script>
336
<style>
337
</style>
338
EOF;
339
            
340
            return $this->fillTemplate(
341
                $viewableTemplate,
0 ignored issues
show
Bug introduced by
It seems like $viewableTemplate can also be of type callable; however, parameter $template of Formularium\Frontend\Vue\Framework::fillTemplate() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

341
                /** @scrutinizer ignore-type */ $viewableTemplate,
Loading history...
342
                $templateData,
343
                $m
344
            );
345
        } else {
346
            $id = 'vueapp' . static::counter();
347
            $t = new HTMLNode($this->getViewableContainerTag(), ['id' => $id], $$templateData['form'], true);
348
            $script = <<<EOF
349
const app_$id = new Vue({
350
    el: '#$id',
351
    data: {$templateData['jsonData']}
352
});
353
EOF;
354
            $s = new HTMLNode('script', [], $script, true);
355
            return HTMLNode::factory('div', [], [$t, $s])->getRenderHTML();
356
        }
357
    }
358
359
    public function editableCompose(Model $m, array $elements, string $previousCompose): string
360
    {
361
        $templateData = $this->getTemplateData($m, $elements);
362
        $templateData['containerTag'] = $this->getEditableContainerTag();
363
        $templateData['methods'] = [
364
            'changedFile' => <<<EOF
365
changedFile(name, event) {
366
    console.log(name, event);
367
    const input = event.target;
368
    const files = input.files;
369
    if (files && files[0]) {
370
        // input.preview = window.URL.createObjectURL(files[0]);
371
    }
372
}
373
EOF
374
        ];
375
376
        if (is_callable($this->editableTemplate)) {
377
            return call_user_func(
378
                $this->editableTemplate,
379
                $this,
380
                $templateData,
381
                $m
382
            );
383
        } elseif ($this->editableTemplate) {
384
            return $this->fillTemplate(
385
                $this->editableTemplate,
0 ignored issues
show
Bug introduced by
It seems like $this->editableTemplate can also be of type callable; however, parameter $template of Formularium\Frontend\Vue\Framework::fillTemplate() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

385
                /** @scrutinizer ignore-type */ $this->editableTemplate,
Loading history...
386
                $templateData,
387
                $m
388
            );
389
        } elseif ($this->mode === self::VUE_MODE_SINGLE_FILE) {
390
            $editableTemplate = <<<EOF
391
<template>
392
<{{containerTag}}>
393
    {{form}}
394
</{{containerTag}}>
395
</template>
396
<script>
397
{{imports}}
398
module.exports = {
399
    data: function () {
400
        return {{jsonData}};
401
    },
402
    props: {
403
        {{propsCode}}
404
    },
405
    methods: {
406
        {{methods}}
407
    }
408
};
409
</script>
410
<style>
411
</style>
412
EOF;
413
            return $this->fillTemplate(
414
                $editableTemplate,
415
                $templateData,
416
                $m
417
            );
418
        } else {
419
            $id = 'vueapp' . static::counter();
420
            $t = new HTMLNode($templateData['containerTag'], ['id' => $id], $templateData['form'], true);
421
            $script = <<<EOF
422
const app_$id = new Vue({
423
    el: '#$id',
424
    data: function () {
425
        return {$templateData['jsonData']};
426
    },
427
    methods: {
428
    }
429
});
430
EOF;
431
            $s = new HTMLNode('script', [], $script, true);
432
            return HTMLNode::factory('div', [], [$t, $s])->getRenderHTML();
433
        }
434
    }
435
436
    protected function fillTemplate(string $template, array $data, Model $m): string
437
    {
438
        foreach ($data as $name => $value) {
439
            $template = str_replace(
440
                '{{' . $name . '}}',
441
                $value,
442
                $template
443
            );
444
        }
445
446
        $template = str_replace(
447
            '{{modelName}}',
448
            $m->getName(),
449
            $template
450
        );
451
        $template = str_replace(
452
            '{{modelNameLower}}',
453
            mb_strtolower($m->getName()),
454
            $template
455
        );
456
        return $template;
457
    }
458
459
    /**
460
     * Get appended to the field variable names to handle models stored in an object field.
461
     *
462
     * @return  string
463
     */
464
    public function getFieldModelVariable(): string
465
    {
466
        return $this->fieldModelVariable;
467
    }
468
469
    /**
470
     * Set appended to the field variable names to handle models stored in an object field.
471
     *
472
     * @param  string  $fieldModelVariable  Appended to the field variable names to handle models stored in an object field.
473
     *
474
     * @return  self
475
     */
476
    public function setFieldModelVariable(string $fieldModelVariable): self
477
    {
478
        $this->fieldModelVariable = $fieldModelVariable;
479
480
        return $this;
481
    }
482
483
    /**
484
     * @return array
485
     */
486
    public function getExtraProps(): array
487
    {
488
        return $this->extraProps;
489
    }
490
491
    /**
492
     *
493
     * @param array $extraProps
494
     *
495
     * @return  self
496
     */
497
    public function setExtraProps(array $extraProps): self
498
    {
499
        $this->extraProps = $extraProps;
500
501
        return $this;
502
    }
503
504
    /**
505
     *
506
     * @param array $extra Array of props. 'name' and 'type' keys are required for each element.
507
     *
508
     * @return  self
509
     */
510
    public function appendExtraProp(array $extra): self
511
    {
512
        $this->extraProps[] = $extra;
513
514
        return $this;
515
    }
516
517
    /**
518
     * Appends to the `data` field.
519
     *
520
     * @param string $name
521
     * @param string $value
522
     * @return self
523
     */
524
    public function appendExtraData(string $name, string $value): self
525
    {
526
        $this->extraData[$name] = $value;
527
        return $this;
528
    }
529
530
    /**
531
     * The list of imports to add: import 'key' from 'value'
532
     *
533
     * @param string $key
534
     * @param string $value
535
     * @return self
536
     */
537
    public function appendImport(string $key, string $value): self
538
    {
539
        $this->imports[$key] = $value;
540
541
        return $this;
542
    }
543
}
544