Passed
Pull Request — master (#80)
by Evgeniy
02:54
created

AssetRegistrar   F

Complexity

Total Complexity 77

Size/Duplication

Total Lines 486
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 176
c 2
b 0
f 0
dl 0
loc 486
ccs 184
cts 184
cp 1
rs 2.24
wmc 77

19 Methods

Rating   Name   Duplication   Size   Complexity  
A registerJsString() 0 21 6
A registerCssString() 0 22 6
A getJsFiles() 0 3 1
B convertCss() 0 32 7
B registerCssFile() 0 40 9
A registerJsVarByConfig() 0 25 4
A withLoader() 0 5 1
A __construct() 0 4 1
A getCssFiles() 0 3 1
C register() 0 44 13
B registerJsFile() 0 40 9
A withConverter() 0 5 1
A getType() 0 3 2
B convertJs() 0 29 7
A getJsStrings() 0 3 1
A getCssStrings() 0 3 1
A registerJsVar() 0 9 2
A getJsVars() 0 3 1
A mergeOptionsWithArray() 0 15 4

How to fix   Complexity   

Complex Class

Complex classes like AssetRegistrar 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 AssetRegistrar, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Assets;
6
7
use Yiisoft\Aliases\Aliases;
8
use Yiisoft\Assets\Exception\InvalidConfigException;
9
10
use function array_key_exists;
11
use function array_values;
12
use function get_class;
13
use function gettype;
14
use function is_array;
15
use function is_int;
16
use function is_object;
17
use function is_string;
18
use function sprintf;
19
20
/**
21
 * AssetRegistrar registers asset files, code blocks and variables from a bundle considering dependencies.
22
 *
23
 * @internal
24
 *
25
 * @psalm-type CssFile = array{0:string,1?:int}&array
26
 * @psalm-type CssString = array{0:mixed,1?:int}&array
27
 * @psalm-type JsFile = array{0:string,1?:int}&array
28
 * @psalm-type JsString = array{0:mixed,1?:int}&array
29
 */
30
final class AssetRegistrar
31
{
32
    private Aliases $aliases;
33
    private AssetLoaderInterface $loader;
34
    private ?AssetConverterInterface $converter = null;
35
36
    /**
37
     * @psalm-var CssFile[]
38
     */
39
    private array $cssFiles = [];
40
41
    /**
42
     * @psalm-var CssString[]
43
     */
44
    private array $cssStrings = [];
45
46
    /**
47
     * @psalm-var JsFile[]
48
     */
49
    private array $jsFiles = [];
50
51
    /**
52
     * @psalm-var JsString[]
53
     */
54
    private array $jsStrings = [];
55
56
    private array $jsVars = [];
57
58 95
    public function __construct(Aliases $aliases, AssetLoaderInterface $loader)
59
    {
60 95
        $this->aliases = $aliases;
61 95
        $this->loader = $loader;
62 95
    }
63
64
    /**
65
     * Return config array CSS AssetBundle.
66
     *
67
     * @psalm-return CssFile[]
68
     */
69 12
    public function getCssFiles(): array
70
    {
71 12
        return $this->cssFiles;
72
    }
73
74
    /**
75
     * Returns CSS blocks.
76
     *
77
     * @return array
78
     * @psalm-return CssString[]
79
     */
80 2
    public function getCssStrings(): array
81
    {
82 2
        return $this->cssStrings;
83
    }
84
85
    /**
86
     * Returns config array JS AssetBundle.
87
     *
88
     * @psalm-return JsFile[]
89
     */
90 22
    public function getJsFiles(): array
91
    {
92 22
        return $this->jsFiles;
93
    }
94
95
    /**
96
     * Returns JS code blocks.
97
     *
98
     * @return array
99
     * @psalm-return JsString[]
100
     */
101 3
    public function getJsStrings(): array
102
    {
103 3
        return $this->jsStrings;
104
    }
105
106
    /**
107
     * Returns JS variables.
108
     *
109
     * @return array
110
     */
111 3
    public function getJsVars(): array
112
    {
113 3
        return array_values($this->jsVars);
114
    }
115
116
    /**
117
     * Returns a new instance with the specified converter.
118
     *
119
     * @param AssetConverterInterface $converter
120
     *
121
     * @return self
122
     */
123 95
    public function withConverter(AssetConverterInterface $converter): self
124
    {
125 95
        $new = clone $this;
126 95
        $new->converter = $converter;
127 95
        return $new;
128
    }
129
130
    /**
131
     * Returns a new instance with the specified loader.
132
     *
133
     * @param AssetLoaderInterface $loader
134
     *
135
     * @return self
136
     */
137 5
    public function withLoader(AssetLoaderInterface $loader): self
138
    {
139 5
        $new = clone $this;
140 5
        $new->loader = $loader;
141 5
        return $new;
142
    }
143
144
    /**
145
     * Registers asset files from a bundle considering dependencies.
146
     *
147
     * @param AssetBundle $bundle
148
     *
149
     * @throws InvalidConfigException If asset files are not found.
150
     */
151 58
    public function register(AssetBundle $bundle): void
152
    {
153 58
        if (isset($bundle->basePath, $bundle->baseUrl) && $this->converter !== null) {
154 15
            $this->convertCss($bundle);
155 15
            $this->convertJs($bundle);
156
        }
157
158 58
        foreach ($bundle->js as $key => $js) {
159 49
            $this->registerJsFile(
160 49
                $bundle,
161 49
                is_string($key) ? $key : null,
162
                $js,
163
            );
164
        }
165
166 52
        foreach ($bundle->jsStrings as $key => $jsString) {
167 7
            $this->registerJsString(
168 7
                $bundle,
169 7
                is_string($key) ? $key : null,
170
                $jsString,
171
            );
172
        }
173
174 51
        foreach ($bundle->jsVars as $name => $jsVar) {
175 9
            if (is_string($name)) {
176 6
                $this->registerJsVar($name, $jsVar, $bundle->jsPosition);
177
            } else {
178 9
                $this->registerJsVarByConfig($jsVar, $bundle->jsPosition);
179
            }
180
        }
181
182 48
        foreach ($bundle->css as $key => $css) {
183 36
            $this->registerCssFile(
184 36
                $bundle,
185 36
                is_string($key) ? $key : null,
186
                $css,
187
            );
188
        }
189
190 45
        foreach ($bundle->cssStrings as $key => $cssString) {
191 6
            $this->registerCssString(
192 6
                $bundle,
193 6
                is_string($key) ? $key : null,
194
                $cssString,
195
            );
196
        }
197 45
    }
198
199
    /**
200
     * Converter SASS, SCSS, Stylus and other formats to CSS.
201
     *
202
     * @param AssetBundle $bundle
203
     */
204 15
    private function convertCss(AssetBundle $bundle): void
205
    {
206 15
        foreach ($bundle->css as $i => $css) {
207 14
            if (is_array($css)) {
208 4
                $file = $css[0];
209 4
                if (AssetUtil::isRelative($file)) {
210 4
                    $baseFile = $this->aliases->get("{$bundle->basePath}/{$file}");
211 4
                    if (is_file($baseFile)) {
212
                        /**
213
                         * @psalm-suppress PossiblyNullArgument
214
                         * @psalm-suppress PossiblyNullReference
215
                         */
216 4
                        $css[0] = $this->converter->convert(
0 ignored issues
show
Bug introduced by
The method convert() does not exist on null. ( Ignorable by Annotation )

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

216
                        /** @scrutinizer ignore-call */ 
217
                        $css[0] = $this->converter->convert(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
217 4
                            $file,
218 4
                            $bundle->basePath,
0 ignored issues
show
Bug introduced by
It seems like $bundle->basePath can also be of type null; however, parameter $basePath of Yiisoft\Assets\AssetConverterInterface::convert() 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

218
                            /** @scrutinizer ignore-type */ $bundle->basePath,
Loading history...
219 4
                            $bundle->converterOptions,
220
                        );
221
222 4
                        $bundle->css[$i] = $css;
223
                    }
224
                }
225 14
            } elseif (AssetUtil::isRelative($css)) {
226 14
                $baseCss = $this->aliases->get("{$bundle->basePath}/{$css}");
227 14
                if (is_file("$baseCss")) {
228
                    /**
229
                     * @psalm-suppress PossiblyNullArgument
230
                     * @psalm-suppress PossiblyNullReference
231
                     */
232 13
                    $bundle->css[$i] = $this->converter->convert(
233 13
                        $css,
234 13
                        $bundle->basePath,
235 13
                        $bundle->converterOptions
236
                    );
237
                }
238
            }
239
        }
240 15
    }
241
242
    /**
243
     * Convert files from TypeScript and other formats into JavaScript.
244
     *
245
     * @param AssetBundle $bundle
246
     */
247 15
    private function convertJs(AssetBundle $bundle): void
248
    {
249 15
        foreach ($bundle->js as $i => $js) {
250 15
            if (is_array($js)) {
251 5
                $file = $js[0];
252 5
                if (AssetUtil::isRelative($file)) {
253 5
                    $baseFile = $this->aliases->get("{$bundle->basePath}/{$file}");
254 5
                    if (is_file($baseFile)) {
255
                        /**
256
                         * @psalm-suppress PossiblyNullArgument
257
                         * @psalm-suppress PossiblyNullReference
258
                         */
259 5
                        $js[0] = $this->converter->convert(
260 5
                            $file,
261 5
                            $bundle->basePath,
0 ignored issues
show
Bug introduced by
It seems like $bundle->basePath can also be of type null; however, parameter $basePath of Yiisoft\Assets\AssetConverterInterface::convert() 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

261
                            /** @scrutinizer ignore-type */ $bundle->basePath,
Loading history...
262 5
                            $bundle->converterOptions
263
                        );
264
265 5
                        $bundle->js[$i] = $js;
266
                    }
267
                }
268 15
            } elseif (AssetUtil::isRelative($js)) {
269 15
                $baseJs = $this->aliases->get("{$bundle->basePath}/{$js}");
270 15
                if (is_file($baseJs)) {
271
                    /**
272
                     * @psalm-suppress PossiblyNullArgument
273
                     * @psalm-suppress PossiblyNullReference
274
                     */
275 14
                    $bundle->js[$i] = $this->converter->convert($js, $bundle->basePath);
276
                }
277
            }
278
        }
279 15
    }
280
281
    /**
282
     * Registers a CSS file.
283
     *
284
     * @param array|string $css
285
     *
286
     * @throws InvalidConfigException
287
     */
288 36
    private function registerCssFile(AssetBundle $bundle, ?string $key, $css): void
289
    {
290 36
        if (is_array($css)) {
291 10
            if (!array_key_exists(0, $css)) {
292 1
                throw new InvalidConfigException('Do not set in array CSS URL.');
293
            }
294 9
            $url = $css[0];
295
        } else {
296 33
            $url = $css;
297
        }
298
299 35
        if (!is_string($url)) {
300 1
            throw new InvalidConfigException(
301 1
                sprintf(
302 1
                    'CSS file should be string. Got %s.',
303 1
                    $this->getType($url),
304
                )
305
            );
306
        }
307
308 34
        if ($url === '') {
309 1
            throw new InvalidConfigException('CSS file should be non empty string.');
310
        }
311
312 33
        $url = $this->loader->getAssetUrl($bundle, $url);
313
314 33
        if (is_array($css)) {
315 7
            $css[0] = $url;
316
        } else {
317 33
            $css = [$url];
318
        }
319
320 33
        if ($bundle->cssPosition !== null && !isset($css[1])) {
321 3
            $css[1] = $bundle->cssPosition;
322
        }
323
324
        /** @psalm-var CssFile */
325 33
        $css = $this->mergeOptionsWithArray($bundle->cssOptions, $css);
326
327 33
        $this->cssFiles[$key ?: $url] = $css;
328 33
    }
329
330
    /**
331
     * Registers a CSS string.
332
     *
333
     * @param mixed $cssString
334
     *
335
     * @throws InvalidConfigException
336
     */
337 6
    private function registerCssString(AssetBundle $bundle, ?string $key, $cssString): void
338
    {
339 6
        if (is_array($cssString)) {
340 6
            $config = $cssString;
341 6
            if (!array_key_exists(0, $config)) {
342 6
                throw new InvalidConfigException('CSS string do not set in array.');
343
            }
344
        } else {
345 6
            $config = [$cssString];
346
        }
347
348 6
        if ($bundle->cssPosition !== null && !isset($config[1])) {
349 1
            $config[1] = $bundle->cssPosition;
350
        }
351
352
        /** @psalm-var CssString */
353 6
        $config = $this->mergeOptionsWithArray($bundle->cssOptions, $config);
354
355 6
        if ($key === null) {
356 6
            $this->cssStrings[] = $config;
357
        } else {
358 6
            $this->cssStrings[$key] = $config;
359
        }
360 6
    }
361
362
    /**
363
     * Registers a JS file.
364
     *
365
     * @param array|string $js
366
     *
367
     * @throws InvalidConfigException
368
     */
369 49
    private function registerJsFile(AssetBundle $bundle, ?string $key, $js): void
370
    {
371 49
        if (is_array($js)) {
372 12
            if (!array_key_exists(0, $js)) {
373 1
                throw new InvalidConfigException('Do not set in array JS URL.');
374
            }
375 11
            $url = $js[0];
376
        } else {
377 45
            $url = $js;
378
        }
379
380 48
        if (!is_string($url)) {
381 1
            throw new InvalidConfigException(
382 1
                sprintf(
383 1
                    'JS file should be string. Got %s.',
384 1
                    $this->getType($url),
385
                )
386
            );
387
        }
388
389 47
        if ($url === '') {
390 1
            throw new InvalidConfigException('JS file should be non empty string.');
391
        }
392
393 46
        $url = $this->loader->getAssetUrl($bundle, $url);
394
395 43
        if (is_array($js)) {
396 9
            $js[0] = $url;
397
        } else {
398 42
            $js = [$url];
399
        }
400
401 43
        if ($bundle->jsPosition !== null && !isset($js[1])) {
402 10
            $js[1] = $bundle->jsPosition;
403
        }
404
405
        /** @psalm-var JsFile */
406 43
        $js = $this->mergeOptionsWithArray($bundle->jsOptions, $js);
407
408 43
        $this->jsFiles[$key ?: $url] = $js;
409 43
    }
410
411
    /**
412
     * Registers a JS string.
413
     *
414
     * @param array|string $jsString
415
     *
416
     * @throws InvalidConfigException
417
     */
418 7
    private function registerJsString(AssetBundle $bundle, ?string $key, $jsString): void
419
    {
420 7
        if (is_array($jsString)) {
421 6
            if (!array_key_exists(0, $jsString)) {
422 6
                throw new InvalidConfigException('JavaScript string do not set in array.');
423
            }
424
        } else {
425 7
            $jsString = [$jsString];
426
        }
427
428 7
        if ($bundle->jsPosition !== null && !isset($jsString[1])) {
429 1
            $jsString[1] = $bundle->jsPosition;
430
        }
431
432
        /** @psalm-var JsString */
433 7
        $jsString = $this->mergeOptionsWithArray($bundle->jsOptions, $jsString);
434
435 6
        if ($key === null) {
436 6
            $this->jsStrings[] = $jsString;
437
        } else {
438 6
            $this->jsStrings[$key] = $jsString;
439
        }
440 6
    }
441
442
    /**
443
     * Registers a JavaScript variable.
444
     *
445
     * @param mixed $value
446
     */
447 6
    private function registerJsVar(string $name, $value, ?int $position): void
448
    {
449 6
        $config = [$name, $value];
450
451 6
        if ($position !== null) {
452 6
            $config[2] = $position;
453
        }
454
455 6
        $this->jsVars[$name] = $config;
456 6
    }
457
458
    /**
459
     * Registers a JavaScript variable by config.
460
     *
461
     * @throws InvalidConfigException
462
     */
463 9
    private function registerJsVarByConfig(array $config, ?int $bundleJsPosition): void
464
    {
465 9
        if (!array_key_exists(0, $config)) {
466 1
            throw new InvalidConfigException('Do not set JavaScript variable name.');
467
        }
468 8
        $name = $config[0];
469
470 8
        if (!is_string($name)) {
471 1
            throw new InvalidConfigException(
472 1
                sprintf(
473 1
                    'JavaScript variable name should be string. Got %s.',
474 1
                    $this->getType($name),
475
                )
476
            );
477
        }
478
479 7
        if (!array_key_exists(1, $config)) {
480 1
            throw new InvalidConfigException('Do not set JavaScript variable value.');
481
        }
482
        /** @var mixed */
483 6
        $value = $config[1];
484
485 6
        $position = $config[2] ?? $bundleJsPosition;
486
487 6
        $this->registerJsVar($name, $value, $position);
488 6
    }
489
490
    /**
491
     * @throws InvalidConfigException
492
     */
493 44
    private function mergeOptionsWithArray(array $options, array $array): array
494
    {
495 44
        foreach ($options as $key => $value) {
496 9
            if (is_int($key)) {
497 1
                throw new InvalidConfigException(
498 1
                    'JavaScript or CSS options should be list of key/value pairs with string keys. Got integer key.'
499
                );
500
            }
501
502 8
            if (!array_key_exists($key, $array)) {
503 8
                $array[$key] = $value;
504
            }
505
        }
506
507 43
        return $array;
508
    }
509
510
    /**
511
     * @param mixed $value
512
     */
513 3
    private function getType($value): string
514
    {
515 3
        return is_object($value) ? get_class($value) : gettype($value);
516
    }
517
}
518