Passed
Pull Request — 2.x (#1421)
by Quentin
09:12
created

Block::make()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 8
nc 2
nop 4
dl 0
loc 13
ccs 4
cts 4
cp 1
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace A17\Twill\Services\Blocks;
4
5
use Exception;
6
use Illuminate\Support\Str;
7
8
/**
9
 * @todo(3.x): This is not really a service, and we should move this to another location.
10
 */
11
class Block
12
{
13
    const SOURCE_APP = 'app';
14
15
    const SOURCE_TWILL = 'twill';
16
17
    const SOURCE_CUSTOM = 'custom';
18
19
    const TYPE_BLOCK = 'block';
20
21
    const TYPE_REPEATER = 'repeater';
22
23
    const PREG_REPLACE_INNER = '(?:\'|")(.*)(?:\'|")';
24
25
    /**
26
     * @var string
27
     */
28
    public $title;
29
30
    /**
31
     * @var string
32
     */
33
    public $titleField;
34
35
    /**
36
     * @var boolean
37
     */
38
    public $hideTitlePrefix;
39
40
    /**
41
     * @var string
42
     */
43
    public $trigger;
44
45
    /**
46
     * @var string
47
     */
48
    public $source;
49
50
    /**
51
     * @var string
52
     */
53
    public $name;
54
55
    /**
56
     * @var string
57
     */
58
    public $group;
59
60
    /**
61
     * @var string
62
     */
63
    public $type;
64
65
    /**
66
     * @var string
67
     */
68
    public $icon;
69
70
    /**
71
     * @var boolean
72
     */
73
    public $compiled;
74
75
    /**
76
     * @var string
77
     */
78
    public $component;
79
80
    /**
81
     * @var integer
82
     */
83
    public $max = 999;
84
85
    /**
86
     * @var boolean
87
     */
88
    public $isNewFormat;
89
90
    /**
91
     * @var \Symfony\Component\Finder\SplFileInfo
92
     */
93
    public $file;
94
95
    /**
96
     * @var string
97 69
     */
98
    public $fileName;
99 69
100
    /**
101 69
     * @var string
102
     */
103 69
    public $contents;
104
105 69
    /**
106
     * @var string
107 69
     */
108 69
    public $rules = [];
109 69
110
    /**
111
     * @var string
112 69
     */
113 69
    public $rulesForTranslatedFields = [];
114
115
    /**
116
     * Make a block instance out of arguments.
117
     *
118 69
     * @param $file
119 69
     * @param $type
120
     * @param $source
121
     * @param $name
122
     * @return static
123
     */
124
    public static function make($file, $type, $source, $name = null): self {
125 69
        $name = $name ?? Str::before(
126
                $file->getFilename(),
127 69
                '.blade.php'
128
            );
129 69
130
        $transformed = Str::studly($name) . 'Block';
131
        $className = "\App\Twill\Block\\$transformed";
132
        if (class_exists($className)) {
133
            return new $className($file, $type, $source, $name);
134
        }
135 11
136
        return new self($file, $type, $source, $name);
137 11
    }
138 11
139 11
    public static function getForType(string $type, bool $repeater = false): self {
140 11
        if ($repeater) {
141 11
            $blocksList = app(BlockCollection::class)->getRepeaterList();
142 11
        } else {
143 11
            $blocksList = app(BlockCollection::class)->getBlockList();
144 11
        }
145 11
146 11
        return $blocksList->first(function(self $blockConfig) use ($type) {
147 11
            return $blockConfig->name === $type;
148 11
        });
149 11
    }
150
151
    public static function getForComponent(string $type, bool $repeater = false): self {
152
        if ($repeater) {
153
            $blocksList = app(BlockCollection::class)->getRepeaterList();
154
        } else {
155
            $blocksList = app(BlockCollection::class)->getBlockList();
156
        }
157
158
        return $blocksList->first(function(self $blockConfig) use ($type) {
159
            return $blockConfig->component === $type;
160
        });
161
    }
162
163
    /**
164
     * Block constructor.
165
     * @param $file
166
     * @param $type
167
     * @param $source
168
     * @param $name
169
     * @throws \Exception
170
     */
171 3
    public function __construct($file, $type, $source, $name = null)
172
    {
173 3
        $this->file = $file;
174
175
        $this->type = $type;
176
177
        $this->source = $source;
178
179
        $this->fileName = $this->getFilename();
180 69
181
        $this->name = $name ?? Str::before(
182 69
            $this->file->getFilename(),
183
            '.blade.php'
184 69
        );
185 69
186 69
        if ($type === self::TYPE_BLOCK
187 69
            && config('twill.block_editor.repeaters.' . $this->name) !== null
188 69
        ) {
189 69
            $this->type = self::TYPE_REPEATER;
190 69
        }
191 69
192 69
        $this->parse();
193
    }
194 69
195
    /**
196
     * @param $source
197
     * @return $this
198
     */
199
    public function setSource($source)
200
    {
201
        $this->source = $source;
202
203
        return $this;
204
    }
205 69
206
    public function getData(array $data, \A17\Twill\Models\Block $block): array
0 ignored issues
show
Unused Code introduced by
The parameter $block 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

206
    public function getData(array $data, /** @scrutinizer ignore-unused */ \A17\Twill\Models\Block $block): array

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...
207
    {
208
        return $data;
209
    }
210
211 69
    /**
212
     * @return \Illuminate\Support\Collection
213 69
     */
214 69
    public function toList()
215
    {
216 69
        return collect([
217 69
            'title' => $this->title,
218
            'titleField' => $this->titleField,
219
            'hideTitlePrefix' => $this->hideTitlePrefix,
220
            'trigger' => $this->trigger,
221
            'name' => $this->name,
222 69
            'group' => $this->group,
223 69
            'type' => $this->type,
224
            'icon' => $this->icon,
225
            'compiled' => $this->compiled ? 'yes' : '-',
226
            'source' => $this->source,
227
            'new_format' => $this->isNewFormat ? 'yes' : '-',
228
            'file' => $this->getFilename(),
229
            'component' => $this->component,
230 69
            'rules' => $this->getRules(),
231 69
            'rulesForTranslatedFields' => $this->getRulesForTranslatedFields(),
232
            'max' => $this->type === self::TYPE_REPEATER ? $this->max : null,
233
        ]);
234
    }
235
236
    /**
237 69
     * @return \Illuminate\Support\Collection
238 69
     */
239
    public function toShortList()
240
    {
241
        return collect([
242
            'title' => $this->title,
243
            'name' => $this->name,
244
            'group' => $this->group,
245
            'type' => $this->type,
246
            'icon' => $this->icon,
247
        ]);
248
    }
249
250
    /**
251
     * @param $name
252
     * @return string
253 69
     */
254
    public function makeName($name)
255 69
    {
256
        return Str::kebab($name);
257 69
    }
258
259
    /**
260
     * @return $this
261
     * @throws \Exception
262
     */
263 69
    public function parse()
264
    {
265 69
        $contents = $this->file ? file_get_contents((string) $this->file->getPathName()) : '';
266
267
        $this->title = $this->parseProperty('title', $contents, $this->name);
268
        $this->trigger = $this->parseProperty('trigger', $contents, $this->name, $this->type === self::TYPE_REPEATER ? twillTrans('twill::lang.fields.block-editor.add-item') : null);
0 ignored issues
show
Bug introduced by
It seems like $this->type === self::TY...ditor.add-item') : null can also be of type array and array; however, parameter $default of A17\Twill\Services\Blocks\Block::parseProperty() does only seem to accept null|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

268
        $this->trigger = $this->parseProperty('trigger', $contents, $this->name, /** @scrutinizer ignore-type */ $this->type === self::TYPE_REPEATER ? twillTrans('twill::lang.fields.block-editor.add-item') : null);
Loading history...
269
        $this->max = (int) $this->parseProperty('max', $contents, $this->name, 999);
270
        $this->group = $this->parseProperty('group', $contents, $this->name, 'app');
271
        $this->icon = $this->parseProperty('icon', $contents, $this->name, 'text');
272 5
        $this->compiled = (boolean) $this->parseProperty('compiled', $contents, $this->name, false);
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type null|string expected by parameter $default of A17\Twill\Services\Blocks\Block::parseProperty(). ( Ignorable by Annotation )

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

272
        $this->compiled = (boolean) $this->parseProperty('compiled', $contents, $this->name, /** @scrutinizer ignore-type */ false);
Loading history...
273
        $this->component = $this->parseProperty('component', $contents, $this->name, "a17-block-{$this->name}");
274 5
        $this->isNewFormat = $this->isNewFormat($contents);
275 5
        $this->contents = $contents;
276
277 5
        $this->parseArrayProperty('ValidationRules', $contents, $this->name, function ($value) {
278
            $this->rules = $value ?? $this->rules;
279
        });
280
281
        $this->parseArrayProperty('ValidationRulesForTranslatedFields', $contents, $this->name, function ($value) {
282
            $this->rulesForTranslatedFields = $value ?? $this->rulesForTranslatedFields;
283
        });
284
285
        $this->parseMixedProperty('titleField', $contents, $this->name, function ($value, $options) {
286 5
            $this->titleField = $value;
287
            $this->hideTitlePrefix = (boolean) ($options['hidePrefix'] ?? false);
288 5
        });
289 5
290
        return $this;
291
    }
292 5
293
    /**
294
     * Checks both the blade file or helper class for validation rules. Returns in order the first one with data.
295
     */
296
    public function getRules(): array {
297
        return $this->rules;
298
    }
299
300
    /**
301
     * Checks both the blade file or helper class for validation rules. Returns in order the first one with data.
302
     */
303
    public function getRulesForTranslatedFields(): array {
304
        return $this->rulesForTranslatedFields;
305
    }
306
307
    /**
308
     * Parse a string property directive in the form of `@twillTypeProperty('value')`.
309
     *
310
     * @param string $property
311
     * @param string $block
312
     * @param string $blockName
313
     * @param string|null $default
314
     * @return string
315
     * @throws \Exception
316
     */
317
    public function parseProperty(
318
        $property,
319
        $block,
320
        $blockName,
321
        $default = null
322
    ) {
323
        $bladeProperty = ucfirst($property);
324
325
        foreach (['twillProp', 'twillBlock', 'twillRepeater'] as $pattern) {
326
            preg_match("/@{$pattern}{$bladeProperty}\(" . self::PREG_REPLACE_INNER . "\)/", $block, $matches);
327
328
            if (filled($matches)) {
329
                return $matches[1];
330
            }
331
        }
332
333
        return $this->parsePropertyFallback($property, $blockName, $default);
0 ignored issues
show
Bug introduced by
It seems like $default can also be of type string; however, parameter $default of A17\Twill\Services\Block...parsePropertyFallback() does only seem to accept null, 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

333
        return $this->parsePropertyFallback($property, $blockName, /** @scrutinizer ignore-type */ $default);
Loading history...
334
    }
335
336
    /**
337
     * Parse an array property directive in the form of `@twillTypeProperty([...])`
338
     * and pass the result to a given callback.
339
     *
340
     * @param string $property
341
     * @param string $block
342
     * @param string $blockName
343
     * @param Callable $callback  Should have the following signature: `function (array $value)`
344
     * @return void
345
     * @throws \Exception
346
     */
347
    public function parseArrayProperty(
348
        $property,
349
        $block,
350
        $blockName,
351
        $callback
352
    ): void {
353
        $this->parseMixedProperty($property, $block, $blockName, function($value) use ($callback) {
354
            $callback($value);
355
        });
356
    }
357
358
    /**
359
     * Parse a mixed property directive in the form of `@twillTypeProperty('value', [...])`
360
     * and pass the result to a given callback.
361
     *
362
     * @param string $property
363
     * @param string $block
364
     * @param string $blockName
365
     * @param Callable $callback  Should have the following signature: `function ($value, $options)`
366
     * @return mixed
367
     * @throws \Exception
368
     */
369
    public function parseMixedProperty(
370
        $property,
371
        $block,
372
        $blockName,
373
        $callback
374
    ) {
375
        $bladeProperty = ucfirst($property);
376
377
        foreach (['twillProp', 'twillBlock', 'twillRepeater'] as $pattern) {
378
            // Regexp modifiers:
379
            //   `s`  allows newlines as part of the `.*` match
380
            //   `U`  stops the match at the first closing parenthesis
381
            preg_match("/@{$pattern}{$bladeProperty}\((.*)\)/sU", $block, $matches);
382
383
            if (filled($matches)) {
384
                // Wrap the match in array notation and feed it to `eval` to get an actual array.
385
                // In this context, we're only interested in the first two possible values.
386
                $content = "[{$matches[1]}]";
387
                $parsedContent = eval("return {$content};");
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
388
                $value = $parsedContent[0] ?? null;
389
                $options = $parsedContent[1] ?? null;
390
391
                return $callback($value, $options);
392
            }
393
        }
394
395
        $value = $this->parseProperty($property, $block, $blockName, null);
396
397
        return $callback($value, null);
398
    }
399
400
    /**
401
     * @param $property
402
     * @param $blockName
403
     * @param null $default
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $default is correct as it would always require null to be passed?
Loading history...
404
     * @return mixed
405
     * @throws \Exception
406
     */
407
    private function parsePropertyFallback(
408
        $property,
409
        $blockName,
410
        $default = null
411
    ) {
412
        if (
413
            $value = config(
414
                "twill.block_editor.blocks.{$blockName}.{$property}"
415
            )
416
        ) {
417
            return $value;
418
        }
419
420
        if (
421
            $value = config(
422
                "twill.block_editor.repeaters.{$blockName}.{$property}"
423
            )
424
        ) {
425
            return $value;
426
        }
427
428
        if ($configBlock = collect(config("twill.block_editor.blocks"))->filter(function ($block) use ($blockName) {
429
            return Str::contains($block['component'], $blockName);
430
        })->first()) {
431
            if ($value = ($configBlock[$property] ?? null)) {
432
                return $value;
433
            }
434
        }
435
        if ($configRepeater = collect(config("twill.block_editor.repeaters"))->filter(function ($repeater) use ($blockName) {
436
            return Str::contains($repeater['component'], $blockName);
437
        })->first()) {
438
            if ($value = ($configRepeater[$property] ?? null)) {
439
                return $value;
440
            }
441
        }
442
443
        if ($property !== 'title') {
444
            return $default;
445
        }
446
447
        // Title is mandatory
448
        throw new Exception(
449
            "Block {$blockName} does not exists or the mandatory property '{$property}' " .
450
            "was not found on this block. If you are still using blocks on the twill.php " .
451
            "file, please check if the block is present and properly configured."
452
        );
453
    }
454
455
    /**
456
     * @param $block
457
     * @return bool
458
     */
459
    public function isNewFormat($block)
460
    {
461
        preg_match("/@twill(Prop|Block|Repeater).*\('(.*)'\)/", $block, $propMatches);
462
463
        return filled($propMatches);
464
    }
465
466
    /**
467
     * @return string
468
     */
469
    public function getFileName()
470
    {
471
        return $this->file ? $this->file->getFileName() : 'Custom Vue file';
472
    }
473
474
    /**
475
     * @return string
476
     * @throws \Throwable
477
     */
478
    public function render()
479
    {
480
        return BladeCompiler::render(
481
            self::removeSpecialBladeTags($this->contents),
482
            [
483
                'renderForBlocks' => true,
484
            ]
485
        );
486
    }
487
488
    /**
489
     * @param $contents
490
     * @return string
491
     */
492
    public static function removeSpecialBladeTags($contents)
493
    {
494
        return preg_replace([
495
            "/@twillProp.*\(" . self::PREG_REPLACE_INNER . "\)/sU",
496
            "/@twillBlock.*\(" . self::PREG_REPLACE_INNER . "\)/sU",
497
            "/@twillRepeater.*\(" . self::PREG_REPLACE_INNER . "\)/sU",
498
        ], '', $contents);
499
    }
500
501
    public function getBlockView($blockViewMappings = [])
502
    {
503
        $view = config('twill.block_editor.block_views_path') . '.' . $this->name;
504
505
        if (array_key_exists($this->name, $blockViewMappings)) {
506
            $view = $blockViewMappings[$this->name];
507
        }
508
509
        return $view;
510
    }
511
}
512