AssetRegistrar   F
last analyzed

Complexity

Total Complexity 77

Size/Duplication

Total Lines 476
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
wmc 77
eloc 181
c 2
b 1
f 0
dl 0
loc 476
ccs 203
cts 203
cp 1
rs 2.24

18 Methods

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

208
                        /** @scrutinizer ignore-call */ 
209
                        $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...
209 4
                            $file,
210 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

210
                            /** @scrutinizer ignore-type */ $bundle->basePath,
Loading history...
211 4
                            $bundle->converterOptions,
212 4
                        );
213
214 4
                        $bundle->css[$i] = $css;
215
                    }
216
                }
217 14
            } elseif (AssetUtil::isRelative($css)) {
218 14
                $baseCss = $this->aliases->get("{$bundle->basePath}/{$css}");
219 14
                if (is_file("$baseCss")) {
220 13
                    $bundle->css[$i] = $this->converter->convert(
221 13
                        $css,
222 13
                        $bundle->basePath,
223 13
                        $bundle->converterOptions
224 13
                    );
225
                }
226
            }
227
        }
228
    }
229
230
    /**
231
     * Convert files from TypeScript and other formats into JavaScript.
232
     */
233 15
    private function convertJs(AssetBundle $bundle): void
234
    {
235
        /**
236
         * @psalm-var AssetConverterInterface $this->converter
237
         * @psalm-var string $bundle->basePath
238
         * @psalm-var ConverterOptions $bundle->converterOptions
239
         *
240
         * @var JsFile|string $js
241
         */
242 15
        foreach ($bundle->js as $i => $js) {
243 15
            if (is_array($js)) {
244 5
                $file = $js[0];
245 5
                if (AssetUtil::isRelative($file)) {
246 5
                    $baseFile = $this->aliases->get("{$bundle->basePath}/{$file}");
247 5
                    if (is_file($baseFile)) {
248 5
                        $js[0] = $this->converter->convert(
249 5
                            $file,
250 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

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