Passed
Pull Request — 2.x (#1308)
by Harings
07:46
created

Block   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 497
Duplicated Lines 0 %

Test Coverage

Coverage 82.93%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 160
c 4
b 0
f 0
dl 0
loc 497
ccs 68
cts 82
cp 0.8293
rs 8.8798
wmc 44

21 Methods

Rating   Name   Duplication   Size   Complexity  
A render() 0 6 1
A getForType() 0 9 2
A getData() 0 3 1
A getForComponent() 0 9 2
A make() 0 13 2
A __construct() 0 22 3
A isNewFormat() 0 5 1
A setSource() 0 5 1
A toShortList() 0 8 1
A getBlockView() 0 9 2
A parseArrayProperty() 0 8 1
A getFileName() 0 3 2
A getRules() 0 2 1
A parseMixedProperty() 0 29 3
A makeName() 0 3 1
B parsePropertyFallback() 0 45 8
A parse() 0 28 3
A parseProperty() 0 17 3
A getRulesForTranslatedFields() 0 2 1
A toList() 0 19 4
A removeSpecialBladeTags() 0 7 1

How to fix   Complexity   

Complex Class

Complex classes like Block often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Block, and based on these observations, apply Extract Interface, too.

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
    /**
24
     * @var string
25
     */
26
    public $title;
27
28
    /**
29
     * @var string
30
     */
31
    public $titleField;
32
33
    /**
34
     * @var boolean
35
     */
36
    public $hideTitlePrefix;
37
38
    /**
39
     * @var string
40
     */
41
    public $trigger;
42
43
    /**
44
     * @var string
45
     */
46
    public $source;
47
48
    /**
49
     * @var string
50
     */
51
    public $name;
52
53
    /**
54
     * @var string
55
     */
56
    public $group;
57
58
    /**
59
     * @var string
60
     */
61
    public $type;
62
63
    /**
64
     * @var string
65
     */
66
    public $icon;
67
68
    /**
69
     * @var boolean
70
     */
71
    public $compiled;
72
73
    /**
74
     * @var string
75
     */
76
    public $component;
77
78
    /**
79
     * @var integer
80
     */
81
    public $max = 999;
82
83
    /**
84
     * @var boolean
85
     */
86
    public $isNewFormat;
87
88
    /**
89
     * @var \Symfony\Component\Finder\SplFileInfo
90
     */
91
    public $file;
92
93
    /**
94
     * @var string
95
     */
96
    public $fileName;
97 69
98
    /**
99 69
     * @var string
100
     */
101 69
    public $contents;
102
103 69
    /**
104
     * @var string
105 69
     */
106
    public $rules = [];
107 69
108 69
    /**
109 69
     * @var string
110
     */
111
    public $rulesForTranslatedFields = [];
112 69
113 69
    /**
114
     * Make a block instance out of arguments.
115
     *
116
     * @param $file
117
     * @param $type
118 69
     * @param $source
119 69
     * @param $name
120
     * @return static
121
     */
122
    public static function make($file, $type, $source, $name = null): self {
123
        $name = $name ?? Str::before(
124
                $file->getFilename(),
125 69
                '.blade.php'
126
            );
127 69
128
        $transformed = Str::studly($name) . 'Block';
129 69
        $className = "\App\Twill\Block\\$transformed";
130
        if (class_exists($className)) {
131
            return new $className($file, $type, $source, $name);
132
        }
133
134
        return new self($file, $type, $source, $name);
135 11
    }
136
137 11
    public static function getForType(string $type, bool $repeater = false): self {
138 11
        if ($repeater) {
139 11
            $blocksList = app(BlockCollection::class)->getRepeaterList();
140 11
        } else {
141 11
            $blocksList = app(BlockCollection::class)->getBlockList();
142 11
        }
143 11
144 11
        return $blocksList->first(function(self $blockConfig) use ($type) {
145 11
            return $blockConfig->name === $type;
146 11
        });
147 11
    }
148 11
149 11
    public static function getForComponent(string $type, bool $repeater = false): self {
150
        if ($repeater) {
151
            $blocksList = app(BlockCollection::class)->getRepeaterList();
152
        } else {
153
            $blocksList = app(BlockCollection::class)->getBlockList();
154
        }
155
156
        return $blocksList->first(function(self $blockConfig) use ($type) {
157
            return $blockConfig->component === $type;
158
        });
159
    }
160
161
    /**
162
     * Block constructor.
163
     * @param $file
164
     * @param $type
165
     * @param $source
166
     * @param $name
167
     * @throws \Exception
168
     */
169
    public function __construct($file, $type, $source, $name = null)
170
    {
171 3
        $this->file = $file;
172
173 3
        $this->type = $type;
174
175
        $this->source = $source;
176
177
        $this->fileName = $this->getFilename();
178
179
        $this->name = $name ?? Str::before(
180 69
            $this->file->getFilename(),
181
            '.blade.php'
182 69
        );
183
184 69
        if ($type === self::TYPE_BLOCK
185 69
            && config('twill.block_editor.repeaters.' . $this->name) !== null
186 69
        ) {
187 69
            $this->type = self::TYPE_REPEATER;
188 69
        }
189 69
190 69
        $this->parse();
191 69
    }
192 69
193
    /**
194 69
     * @param $source
195
     * @return $this
196
     */
197
    public function setSource($source)
198
    {
199
        $this->source = $source;
200
201
        return $this;
202
    }
203
204
    public function getData(array $data): array
205 69
    {
206
        return $data;
207
    }
208
209
    /**
210
     * @return \Illuminate\Support\Collection
211 69
     */
212
    public function toList()
213 69
    {
214 69
        return collect([
215
            'title' => $this->title,
216 69
            'titleField' => $this->titleField,
217 69
            'hideTitlePrefix' => $this->hideTitlePrefix,
218
            'trigger' => $this->trigger,
219
            'name' => $this->name,
220
            'group' => $this->group,
221
            'type' => $this->type,
222 69
            'icon' => $this->icon,
223 69
            'compiled' => $this->compiled ? 'yes' : '-',
224
            'source' => $this->source,
225
            'new_format' => $this->isNewFormat ? 'yes' : '-',
226
            'file' => $this->getFilename(),
227
            'component' => $this->component,
228
            'rules' => $this->getRules(),
229
            'rulesForTranslatedFields' => $this->getRulesForTranslatedFields(),
230 69
            'max' => $this->type === self::TYPE_REPEATER ? $this->max : null,
231 69
        ]);
232
    }
233
234
    /**
235
     * @return \Illuminate\Support\Collection
236
     */
237 69
    public function toShortList()
238 69
    {
239
        return collect([
240
            'title' => $this->title,
241
            'name' => $this->name,
242
            'group' => $this->group,
243
            'type' => $this->type,
244
            'icon' => $this->icon,
245
        ]);
246
    }
247
248
    /**
249
     * @param $name
250
     * @return string
251
     */
252
    public function makeName($name)
253 69
    {
254
        return Str::kebab($name);
255 69
    }
256
257 69
    /**
258
     * @return $this
259
     * @throws \Exception
260
     */
261
    public function parse()
262
    {
263 69
        $contents = $this->file ? file_get_contents((string) $this->file->getPathName()) : '';
264
265 69
        $this->title = $this->parseProperty('title', $contents, $this->name);
266
        $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

266
        $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...
267
        $this->max = (int) $this->parseProperty('max', $contents, $this->name, 999);
268
        $this->group = $this->parseProperty('group', $contents, $this->name, 'app');
269
        $this->icon = $this->parseProperty('icon', $contents, $this->name, 'text');
270
        $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

270
        $this->compiled = (boolean) $this->parseProperty('compiled', $contents, $this->name, /** @scrutinizer ignore-type */ false);
Loading history...
271
        $this->component = $this->parseProperty('component', $contents, $this->name, "a17-block-{$this->name}");
272 5
        $this->isNewFormat = $this->isNewFormat($contents);
273
        $this->contents = $contents;
274 5
275 5
        $this->parseArrayProperty('ValidationRules', $contents, $this->name, function ($value) {
276
            $this->rules = $value ?? [];
0 ignored issues
show
Documentation Bug introduced by
It seems like $value ?? array() can also be of type array. However, the property $rules is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
277 5
        });
278
279
        $this->parseArrayProperty('ValidationRulesForTranslatedFields', $contents, $this->name, function ($value) {
280
            $this->rulesForTranslatedFields = $value ?? [];
0 ignored issues
show
Documentation Bug introduced by
It seems like $value ?? array() can also be of type array. However, the property $rulesForTranslatedFields is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
281
        });
282
283
        $this->parseMixedProperty('titleField', $contents, $this->name, function ($value, $options) {
284
            $this->titleField = $value;
285
            $this->hideTitlePrefix = (boolean) ($options['hidePrefix'] ?? false);
286 5
        });
287
288 5
        return $this;
289 5
    }
290
291
    /**
292 5
     * Checks both the blade file or helper class for validation rules. Returns in order the first one with data.
293
     */
294
    public function getRules(): array {
295
        return $this->rules;
296
    }
297
298
    /**
299
     * Checks both the blade file or helper class for validation rules. Returns in order the first one with data.
300
     */
301
    public function getRulesForTranslatedFields(): array {
302
        return $this->rulesForTranslatedFields;
303
    }
304
305
    /**
306
     * Parse a string property directive in the form of `@twillTypeProperty('value')`.
307
     *
308
     * @param string $property
309
     * @param string $block
310
     * @param string $blockName
311
     * @param string|null $default
312
     * @return string
313
     * @throws \Exception
314
     */
315
    public function parseProperty(
316
        $property,
317
        $block,
318
        $blockName,
319
        $default = null
320
    ) {
321
        $bladeProperty = ucfirst($property);
322
323
        foreach (['twillProp', 'twillBlock', 'twillRepeater'] as $pattern) {
324
            preg_match("/@{$pattern}{$bladeProperty}\('(.*)'\)/", $block, $matches);
325
326
            if (filled($matches)) {
327
                return $matches[1];
328
            }
329
        }
330
331
        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

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