Passed
Pull Request — master (#74)
by Alexander
02:31
created

AssetManager::registerAssetBundle()   C

Complexity

Conditions 13
Paths 47

Size

Total Lines 45
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 13.4931

Importance

Changes 5
Bugs 0 Features 0
Metric Value
cc 13
eloc 28
c 5
b 0
f 0
nc 47
nop 3
dl 0
loc 45
ccs 24
cts 28
cp 0.8571
crap 13.4931
rs 6.6166

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Assets;
6
7
use RuntimeException;
8
use Yiisoft\Aliases\Aliases;
9
use Yiisoft\Assets\Exception\InvalidConfigException;
10
11
use function array_key_exists;
12
use function get_class;
13
use function gettype;
14
use function in_array;
15
use function is_array;
16
use function is_file;
17
use function is_int;
18
use function is_object;
19
use function is_string;
20
21
/**
22
 * AssetManager manages asset bundle configuration and loading.
23
 *
24
 * @psalm-type CssFile = array{0:string,1?:int}&array
25
 * @psalm-type CssString = array{0:mixed,1?:int}&array
26
 * @psalm-type JsFile = array{0:string,1?:int}&array
27
 * @psalm-type JsString = array{0:mixed,1?:int}&array
28
 */
29
final class AssetManager
30
{
31
    /**
32
     * @var string[] List of names of allowed asset bundles. If the array is empty, then any asset bundles are allowed.
33
     */
34
    private array $allowedBundleNames;
35
36
    /**
37
     * @var array The asset bundle configurations. This property is provided to customize asset bundles.
38
     */
39
    private array $customizedBundles;
40
41
    /**
42
     * @var array AssetBundle[] list of the registered asset bundles.
43
     * The keys are the bundle names, and the values are the registered {@see AssetBundle} objects.
44
     *
45
     * {@see registerAssetBundle()}
46
     */
47
    private array $registeredBundles = [];
48
49
    private array $loadedBundles = [];
50
    private array $dummyBundles = [];
51
52
    /**
53
     * @psalm-var CssFile[]
54
     */
55
    private array $cssFiles = [];
56
57
    /**
58
     * @psalm-var CssString[]
59
     */
60
    private array $cssStrings = [];
61
62
    /**
63
     * @psalm-var JsFile[]
64
     */
65
    private array $jsFiles = [];
66
67
    /**
68
     * @psalm-var JsString[]
69
     */
70
    private array $jsStrings = [];
71
72
    private array $jsVars = [];
73
    private ?AssetConverterInterface $converter = null;
74
    private ?AssetPublisherInterface $publisher = null;
75
    private AssetLoaderInterface $loader;
76
    private Aliases $aliases;
77
78
    /**
79
     * @param Aliases $aliases The aliases instance.
80
     * @param AssetLoaderInterface $loader The loader instance.
81
     * @param string[] $allowedBundleNames List of names of allowed asset bundles. If the array is empty, then any
82
     * asset bundles are allowed. If the names of allowed asset bundles were specified, only these asset bundles
83
     * or their dependencies can be registered {@see register()} and obtained {@see getBundle()}. Also, specifying
84
     * names allows to export {@see export()} asset bundles automatically without first registering them manually.
85
     * @param array $customizedBundles The asset bundle configurations. Provided to customize asset bundles.
86
     * When a bundle is being loaded by {@see getBundle()}, if it has a corresponding configuration specified
87
     * here, the configuration will be applied to the bundle. The array keys are the asset class bundle names
88
     * (without leading backslash). If a value is false, it means the corresponding asset bundle is disabled
89
     * and {@see getBundle()} should return an instance of the specified asset bundle with empty property values.
90
     */
91 80
    public function __construct(
92
        Aliases $aliases,
93
        AssetLoaderInterface $loader,
94
        array $allowedBundleNames = [],
95
        array $customizedBundles = []
96
    ) {
97 80
        $this->aliases = $aliases;
98 80
        $this->loader = $loader;
99 80
        $this->allowedBundleNames = $allowedBundleNames;
100 80
        $this->customizedBundles = $customizedBundles;
101 80
    }
102
103
    /**
104
     * Returns a cloned named asset bundle.
105
     *
106
     * This method will first look for the bundle in {@see $customizedBundles}.
107
     * If not found, it will treat `$name` as the class of the asset bundle and create a new instance of it.
108
     * If `$name` is not a class name, an {@see AssetBundle} instance will be created.
109
     *
110
     * Cloning is used to prevent an asset bundle instance from being modified in a non-context of the asset manager.
111
     *
112
     * @param string $name The class name of the asset bundle (without the leading backslash).
113
     *
114
     * @throws InvalidConfigException For invalid asset bundle configuration.
115
     *
116
     * @return AssetBundle The asset bundle instance.
117
     */
118 9
    public function getBundle(string $name): AssetBundle
119
    {
120 9
        if (!empty($this->allowedBundleNames)) {
121 4
            $this->checkAllowedBundleName($name);
122
        }
123
124 9
        $bundle = $this->loadBundle($name);
125 9
        $bundle = $this->publishBundle($bundle);
126
127 9
        return clone $bundle;
128
    }
129
130
    /**
131
     * Returns the actual URL for the specified asset.
132
     *
133
     * @param string $name The asset bundle name.
134
     * @param string $path The asset path.
135
     *
136
     * @throws InvalidConfigException If asset files are not found.
137
     *
138
     * @return string The actual URL for the specified asset.
139
     */
140 1
    public function getAssetUrl(string $name, string $path): string
141
    {
142 1
        return $this->loader->getAssetUrl($this->getBundle($name), $path);
143
    }
144
145
    /**
146
     * Return config array CSS AssetBundle.
147
     *
148
     * @psalm-return CssFile[]
149
     */
150 12
    public function getCssFiles(): array
151
    {
152 12
        return $this->cssFiles;
153
    }
154
155
    /**
156
     * Returns CSS blocks.
157
     *
158
     * @return array
159
     * @psalm-return CssString[]
160
     */
161 2
    public function getCssStrings(): array
162
    {
163 2
        return $this->cssStrings;
164
    }
165
166
    /**
167
     * Returns config array JS AssetBundle.
168
     *
169
     * @psalm-return JsFile[]
170
     */
171 22
    public function getJsFiles(): array
172
    {
173 22
        return $this->jsFiles;
174
    }
175
176
    /**
177
     * Returns JS code blocks.
178
     *
179
     * @return array
180
     * @psalm-return JsString[]
181
     */
182 3
    public function getJsStrings(): array
183
    {
184 3
        return $this->jsStrings;
185
    }
186
187
    /**
188
     * Returns JS variables.
189
     *
190
     * @return array
191
     */
192 3
    public function getJsVars(): array
193
    {
194 3
        return array_values($this->jsVars);
195
    }
196
197
    /**
198
     * Returns a new instance with the specified converter.
199
     *
200
     * @param AssetConverterInterface $converter
201
     *
202
     * @return self
203
     */
204 80
    public function withConverter(AssetConverterInterface $converter): self
205
    {
206 80
        $new = clone $this;
207 80
        $new->converter = $converter;
208 80
        return $new;
209
    }
210
211
    /**
212
     * Returns a new instance with the specified loader.
213
     *
214
     * @param AssetLoaderInterface $loader
215
     *
216
     * @return self
217
     */
218 5
    public function withLoader(AssetLoaderInterface $loader): self
219
    {
220 5
        $new = clone $this;
221 5
        $new->loader = $loader;
222 5
        return $new;
223
    }
224
225
    /**
226
     * Returns a new instance with the specified publisher.
227
     *
228
     * @param AssetPublisherInterface $publisher
229
     *
230
     * @return self
231
     */
232 80
    public function withPublisher(AssetPublisherInterface $publisher): self
233
    {
234 80
        $new = clone $this;
235 80
        $new->publisher = $publisher;
236 80
        return $new;
237
    }
238
239
    /**
240
     * Exports registered asset bundles.
241
     *
242
     * When using the allowed asset bundles, the export result will always be the same,
243
     * since the asset bundles are registered before the export. If do not use the allowed asset bundles mode,
244
     * must register {@see register()} all the required asset bundles before exporting.
245
     *
246
     * @param AssetExporterInterface $exporter The exporter instance.
247
     *
248
     * @throws InvalidConfigException If an error occurs during registration when using allowed asset bundles.
249
     * @throws RuntimeException If no asset bundles were registered or an error occurred during the export.
250
     */
251 8
    public function export(AssetExporterInterface $exporter): void
252
    {
253 8
        if (!empty($this->allowedBundleNames)) {
254 3
            $this->registerAllAllowed();
255
        }
256
257 8
        if (empty($this->registeredBundles)) {
258 1
            throw new RuntimeException('Not a single asset bundle was registered.');
259
        }
260
261 7
        $exporter->export($this->registeredBundles);
262 7
    }
263
264
    /**
265
     * Registers asset bundles by names.
266
     *
267
     * @param string[] $names
268
     *
269
     * @throws InvalidConfigException
270
     * @throws RuntimeException
271
     */
272 80
    public function register(array $names, ?int $jsPosition = null, ?int $cssPosition = null): void
273
    {
274 80
        if (!empty($this->allowedBundleNames)) {
275 3
            foreach ($names as $name) {
276 3
                $this->checkAllowedBundleName($name);
277
            }
278
        }
279
280 80
        foreach ($names as $name) {
281 46
            $this->registerAssetBundle($name, $jsPosition, $cssPosition);
282 42
            $this->registerFiles($name);
283
        }
284 80
    }
285
286
    /**
287
     * Registers all allowed asset bundles.
288
     *
289
     * @throws InvalidConfigException
290
     * @throws RuntimeException
291
     */
292 5
    public function registerAllAllowed(): void
293
    {
294 5
        if (empty($this->allowedBundleNames)) {
295 1
            throw new RuntimeException('The allowed names of the asset bundles were not set.');
296
        }
297
298 4
        foreach ($this->allowedBundleNames as $name) {
299 4
            $this->registerAssetBundle($name);
300 4
            $this->registerFiles($name);
301
        }
302 4
    }
303
304
    /**
305
     * Returns whether the asset bundle is registered.
306
     *
307
     * @param string $name The class name of the asset bundle (without the leading backslash).
308
     *
309
     * @return bool Whether the asset bundle is registered.
310
     */
311 4
    public function isRegisteredBundle(string $name): bool
312
    {
313 4
        return isset($this->registeredBundles[$name]);
314
    }
315
316
    /**
317
     * Converter SASS, SCSS, Stylus and other formats to CSS.
318
     *
319
     * @param AssetBundle $bundle
320
     */
321 15
    private function convertCss(AssetBundle $bundle): void
322
    {
323 15
        foreach ($bundle->css as $i => $css) {
324 14
            if (is_array($css)) {
325 4
                $file = $css[0];
326 4
                if (AssetUtil::isRelative($file)) {
327 4
                    $baseFile = $this->aliases->get("{$bundle->basePath}/{$file}");
328 4
                    if (is_file($baseFile)) {
329
                        /**
330
                         * @psalm-suppress PossiblyNullArgument
331
                         * @psalm-suppress PossiblyNullReference
332
                         */
333 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

333
                        /** @scrutinizer ignore-call */ 
334
                        $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...
334 4
                            $file,
335 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

335
                            /** @scrutinizer ignore-type */ $bundle->basePath,
Loading history...
336 4
                            $bundle->converterOptions,
337
                        );
338
339 4
                        $bundle->css[$i] = $css;
340
                    }
341
                }
342 14
            } elseif (AssetUtil::isRelative($css)) {
343 14
                $baseCss = $this->aliases->get("{$bundle->basePath}/{$css}");
344 14
                if (is_file("$baseCss")) {
345
                    /**
346
                     * @psalm-suppress PossiblyNullArgument
347
                     * @psalm-suppress PossiblyNullReference
348
                     */
349 13
                    $bundle->css[$i] = $this->converter->convert(
350 13
                        $css,
351 13
                        $bundle->basePath,
352 13
                        $bundle->converterOptions
353
                    );
354
                }
355
            }
356
        }
357 15
    }
358
359
    /**
360
     * Convert files from TypeScript and other formats into JavaScript.
361
     *
362
     * @param AssetBundle $bundle
363
     */
364 15
    private function convertJs(AssetBundle $bundle): void
365
    {
366 15
        foreach ($bundle->js as $i => $js) {
367 15
            if (is_array($js)) {
368 5
                $file = $js[0];
369 5
                if (AssetUtil::isRelative($file)) {
370 5
                    $baseFile = $this->aliases->get("{$bundle->basePath}/{$file}");
371 5
                    if (is_file($baseFile)) {
372
                        /**
373
                         * @psalm-suppress PossiblyNullArgument
374
                         * @psalm-suppress PossiblyNullReference
375
                         */
376 5
                        $js[0] = $this->converter->convert(
377 5
                            $file,
378 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

378
                            /** @scrutinizer ignore-type */ $bundle->basePath,
Loading history...
379 5
                            $bundle->converterOptions
380
                        );
381
382 5
                        $bundle->js[$i] = $js;
383
                    }
384
                }
385 15
            } elseif (AssetUtil::isRelative($js)) {
386 15
                $baseJs = $this->aliases->get("{$bundle->basePath}/{$js}");
387 15
                if (is_file($baseJs)) {
388
                    /**
389
                     * @psalm-suppress PossiblyNullArgument
390
                     * @psalm-suppress PossiblyNullReference
391
                     */
392 14
                    $bundle->js[$i] = $this->converter->convert($js, $bundle->basePath);
393
                }
394
            }
395
        }
396 15
    }
397
398
    /**
399
     * Registers the named asset bundle.
400
     *
401
     * All dependent asset bundles will be registered.
402
     *
403
     * @param string $name The class name of the asset bundle (without the leading backslash).
404
     * @param int|null $jsPosition If set, this forces a minimum position for javascript files.
405
     * This will adjust depending assets javascript file position or fail if requirement can not be met.
406
     * If this is null, asset bundles position settings will not be changed.
407
     *
408
     * {@see registerJsFile()} For more details on javascript position.
409
     *
410
     * @throws InvalidConfigException If the asset or the asset file paths to be published does not exist.
411
     * @throws RuntimeException If the asset bundle does not exist or a circular dependency is detected.
412
     */
413 50
    private function registerAssetBundle(string $name, ?int $jsPosition = null, ?int $cssPosition = null): void
414
    {
415 50
        if (!isset($this->registeredBundles[$name])) {
416 50
            $bundle = $this->publishBundle($this->loadBundle($name));
417
418 49
            $this->registeredBundles[$name] = false;
419
420 49
            foreach ($bundle->depends as $dep) {
421 31
                $this->registerAssetBundle($dep, $bundle->jsPosition, $bundle->cssPosition);
422
            }
423
424 48
            unset($this->registeredBundles[$name]);
425 48
            $this->registeredBundles[$name] = $bundle;
426 12
        } elseif ($this->registeredBundles[$name] === false) {
427 1
            throw new RuntimeException("A circular dependency is detected for bundle \"{$name}\".");
428
        } else {
429 11
            $bundle = $this->registeredBundles[$name];
430
        }
431
432 48
        if ($jsPosition !== null || $cssPosition !== null) {
433 12
            if ($jsPosition !== null) {
434 12
                if ($bundle->jsPosition === null) {
435 11
                    $bundle->jsPosition = $jsPosition;
436 5
                } elseif ($bundle->jsPosition > $jsPosition) {
437 4
                    throw new RuntimeException(
438 4
                        "An asset bundle that depends on \"{$name}\" has a higher JavaScript file " .
439 4
                        "position configured than \"{$name}\"."
440
                    );
441
                }
442
            }
443
444 12
            if ($cssPosition !== null) {
445 1
                if ($bundle->cssPosition === null) {
446 1
                    $bundle->cssPosition = $cssPosition;
447
                } elseif ($bundle->cssPosition > $cssPosition) {
448
                    throw new RuntimeException(
449
                        "An asset bundle that depends on \"{$name}\" has a higher CSS file " .
450
                        "position configured than \"{$name}\"."
451
                    );
452
                }
453
            }
454
455
            // update position for all dependencies
456 12
            foreach ($bundle->depends as $dep) {
457 7
                $this->registerAssetBundle($dep, $bundle->jsPosition, $bundle->cssPosition);
458
            }
459
        }
460 48
    }
461
462
    /**
463
     * Register assets from a named bundle and its dependencies.
464
     *
465
     * @param string $bundleName The asset bundle name.
466
     *
467
     * @throws InvalidConfigException If asset files are not found.
468
     */
469 46
    private function registerFiles(string $bundleName): void
470
    {
471 46
        $bundle = $this->registeredBundles[$bundleName];
472
473 46
        foreach ($bundle->depends as $dep) {
474 28
            $this->registerFiles($dep);
475
        }
476
477 46
        $this->registerAssetFiles($bundle);
478 43
    }
479
480
    /**
481
     * Registers asset files from a bundle considering dependencies.
482
     *
483
     * @param AssetBundle $bundle
484
     *
485
     * @throws InvalidConfigException If asset files are not found.
486
     */
487 46
    private function registerAssetFiles(AssetBundle $bundle): void
488
    {
489 46
        if (isset($bundle->basePath, $bundle->baseUrl) && null !== $this->converter) {
490 15
            $this->convertCss($bundle);
491 15
            $this->convertJs($bundle);
492
        }
493
494 46
        foreach ($bundle->js as $key => $js) {
495 44
            $this->registerJsFile(
496 44
                $bundle,
497 44
                is_string($key) ? $key : null,
498
                $js,
499
            );
500
        }
501 43
        foreach ($bundle->jsStrings as $key => $jsString) {
502 6
            $this->registerJsString(
503 6
                $bundle,
504 6
                is_string($key) ? $key : null,
505
                $jsString,
506
            );
507
        }
508 43
        foreach ($bundle->jsVars as $name => $jsVar) {
509 6
            if (is_string($name)) {
510 6
                $this->registerJsVar($name, $jsVar, $bundle->jsPosition);
511
            } else {
512 6
                $this->registerJsVarByConfig($jsVar, $bundle->jsPosition);
513
            }
514
        }
515
516 43
        foreach ($bundle->css as $key => $css) {
517 31
            $this->registerCssFile(
518 31
                $bundle,
519 31
                is_string($key) ? $key : null,
520
                $css,
521
            );
522
        }
523 43
        foreach ($bundle->cssStrings as $key => $cssString) {
524 6
            $this->registerCssString(
525 6
                $bundle,
526 6
                is_string($key) ? $key : null,
527
                $cssString,
528
            );
529
        }
530 43
    }
531
532
    /**
533
     * Registers a CSS file.
534
     *
535
     * @param array|string $css
536
     *
537
     * @throws InvalidConfigException
538
     */
539 31
    private function registerCssFile(AssetBundle $bundle, ?string $key, $css): void
540
    {
541 31
        if (is_array($css)) {
542 7
            if (!array_key_exists(0, $css)) {
543
                throw new InvalidConfigException('Do not set in array CSS URL.');
544
            }
545 7
            $url = $css[0];
546
        } else {
547 31
            $url = $css;
548
        }
549
550 31
        if (!is_string($url)) {
551
            throw new InvalidConfigException(
552
                sprintf(
553
                    'CSS file should be string. Got %s.',
554
                    $this->getType($url),
555
                )
556
            );
557
        }
558
559 31
        if ($url === '') {
560
            throw new InvalidConfigException('CSS file should be non empty string.');
561
        }
562
563 31
        $url = $this->loader->getAssetUrl($bundle, $url);
564
565 31
        if (is_array($css)) {
566 7
            $css[0] = $url;
567
        } else {
568 31
            $css = [$url];
569
        }
570
571 31
        if ($bundle->cssPosition !== null && !isset($css[1])) {
572 1
            $css[1] = $bundle->cssPosition;
573
        }
574
575
        /** @psalm-var CssFile */
576 31
        $css = $this->mergeWithReverseOrder($bundle->cssOptions, $css);
577
578 31
        $this->cssFiles[$key ?: $url] = $css;
579 31
    }
580
581
    /**
582
     * Registers a CSS string.
583
     *
584
     * @param mixed $cssString
585
     *
586
     * @throws InvalidConfigException
587
     */
588 6
    private function registerCssString(AssetBundle $bundle, ?string $key, $cssString): void
589
    {
590 6
        if (is_array($cssString)) {
591 6
            $config = $cssString;
592 6
            if (!array_key_exists(0, $config)) {
593 6
                throw new InvalidConfigException('CSS string do not set in array.');
594
            }
595
        } else {
596 6
            $config = [$cssString];
597
        }
598
599 6
        if ($bundle->cssPosition !== null && !isset($config[1])) {
600 1
            $config[1] = $bundle->cssPosition;
601
        }
602
603
        /** @psalm-var CssString */
604 6
        $config = $this->mergeWithReverseOrder($bundle->cssOptions, $config);
605
606 6
        if ($key === null) {
607 6
            $this->cssStrings[] = $config;
608
        } else {
609 6
            $this->cssStrings[$key] = $config;
610
        }
611 6
    }
612
613
    /**
614
     * Registers a JS file.
615
     *
616
     * @param array|string $js
617
     *
618
     * @throws InvalidConfigException
619
     */
620 44
    private function registerJsFile(AssetBundle $bundle, ?string $key, $js): void
621
    {
622 44
        if (is_array($js)) {
623 9
            if (!array_key_exists(0, $js)) {
624
                throw new InvalidConfigException('Do not set in array JS URL.');
625
            }
626 9
            $url = $js[0];
627
        } else {
628 43
            $url = $js;
629
        }
630
631 44
        if (!is_string($url)) {
632
            throw new InvalidConfigException(
633
                sprintf(
634
                    'JS file should be string. Got %s.',
635
                    $this->getType($url),
636
                )
637
            );
638
        }
639
640 44
        if ($url === '') {
641
            throw new InvalidConfigException('JS file should be non empty string.');
642
        }
643
644 44
        $url = $this->loader->getAssetUrl($bundle, $url);
645
646 41
        if (is_array($js)) {
647 9
            $js[0] = $url;
648
        } else {
649 40
            $js = [$url];
650
        }
651
652 41
        if ($bundle->jsPosition !== null && !isset($js[1])) {
653 10
            $js[1] = $bundle->jsPosition;
654
        }
655
656
        /** @psalm-var JsFile */
657 41
        $js = $this->mergeWithReverseOrder($bundle->jsOptions, $js);
658
659 41
        $this->jsFiles[$key ?: $url] = $js;
660 41
    }
661
662
    /**
663
     * Registers a JS string.
664
     *
665
     * @param array|string $jsString
666
     *
667
     * @throws InvalidConfigException
668
     */
669 6
    private function registerJsString(AssetBundle $bundle, ?string $key, $jsString): void
670
    {
671 6
        if (is_array($jsString)) {
672 6
            if (!array_key_exists(0, $jsString)) {
673 6
                throw new InvalidConfigException('JavaScript string do not set in array.');
674
            }
675
        } else {
676 6
            $jsString = [$jsString];
677
        }
678
679 6
        if ($bundle->jsPosition !== null && !isset($jsString[1])) {
680 1
            $jsString[1] = $bundle->jsPosition;
681
        }
682
683
        /** @psalm-var JsString */
684 6
        $jsString = $this->mergeWithReverseOrder($bundle->jsOptions, $jsString);
685
686 6
        if ($key === null) {
687 6
            $this->jsStrings[] = $jsString;
688
        } else {
689 6
            $this->jsStrings[$key] = $jsString;
690
        }
691 6
    }
692
693
    /**
694
     * Registers a JavaScript variable.
695
     *
696
     * @param mixed $value
697
     */
698 6
    private function registerJsVar(string $name, $value, ?int $position): void
699
    {
700 6
        $config = [$name, $value];
701
702 6
        if ($position !== null) {
703 6
            $config[2] = $position;
704
        }
705
706 6
        $this->jsVars[$name] = $config;
707 6
    }
708
709
    /**
710
     * Registers a JavaScript variable by config.
711
     *
712
     * @throws InvalidConfigException
713
     */
714 6
    private function registerJsVarByConfig(array $config, ?int $bundleJsPosition): void
715
    {
716 6
        if (!array_key_exists(0, $config)) {
717
            throw new InvalidConfigException('Do not set JavaScript variable name.');
718
        }
719 6
        $name = $config[0];
720
721 6
        if (!is_string($name)) {
722
            throw new InvalidConfigException(
723
                sprintf(
724
                    'JavaScript variable name should be string. Got %s.',
725
                    $this->getType($name),
726
                )
727
            );
728
        }
729
730 6
        if (!array_key_exists(1, $config)) {
731
            throw new InvalidConfigException('Do not set JavaScript variable value.');
732
        }
733
        /** @var mixed */
734 6
        $value = $config[1];
735
736 6
        $position = $config[2] ?? $bundleJsPosition;
737
738 6
        $this->registerJsVar($name, $value, $position);
739 6
    }
740
741
    /**
742
     * Loads an asset bundle class by name.
743
     *
744
     * @param string $name The asset bundle name.
745
     *
746
     * @throws InvalidConfigException For invalid asset bundle configuration.
747
     *
748
     * @return AssetBundle The asset bundle instance.
749
     */
750 53
    private function loadBundle(string $name): AssetBundle
751
    {
752 53
        if (isset($this->loadedBundles[$name])) {
753 8
            return $this->loadedBundles[$name];
754
        }
755
756 53
        if (!isset($this->customizedBundles[$name])) {
757 48
            return $this->loadedBundles[$name] = $this->loader->loadBundle($name);
758
        }
759
760 20
        if ($this->customizedBundles[$name] instanceof AssetBundle) {
761 1
            return $this->loadedBundles[$name] = $this->customizedBundles[$name];
762
        }
763
764 19
        if (is_array($this->customizedBundles[$name])) {
765 17
            return $this->loadedBundles[$name] = $this->loader->loadBundle($name, $this->customizedBundles[$name]);
766
        }
767
768 2
        if ($this->customizedBundles[$name] === false) {
769 1
            return $this->dummyBundles[$name] ??= $this->loader->loadBundle($name, (array) (new AssetBundle()));
770
        }
771
772 1
        throw new InvalidConfigException("Invalid configuration of the \"{$name}\" asset bundle.");
773
    }
774
775
    /**
776
     * Publishes a asset bundle.
777
     *
778
     * @param AssetBundle $bundle The asset bundle to publish.
779
     *
780
     * @throws InvalidConfigException If the asset or the asset file paths to be published does not exist.
781
     *
782
     * @return AssetBundle The published asset bundle.
783
     */
784 52
    private function publishBundle(AssetBundle $bundle): AssetBundle
785
    {
786 52
        if (!$bundle->cdn && $this->publisher !== null && !empty($bundle->sourcePath)) {
787 13
            [$bundle->basePath, $bundle->baseUrl] = $this->publisher->publish($bundle);
788
        }
789
790 52
        return $bundle;
791
    }
792
793
    /**
794
     * Checks whether asset bundle are allowed by name {@see $allowedBundleNames}.
795
     *
796
     * @param string $name The asset bundle name to check.
797
     *
798
     * @throws InvalidConfigException For invalid asset bundle configuration.
799
     * @throws RuntimeException If The asset bundle name is not allowed.
800
     */
801 4
    public function checkAllowedBundleName(string $name): void
802
    {
803 4
        if (isset($this->loadedBundles[$name]) || in_array($name, $this->allowedBundleNames, true)) {
804 4
            return;
805
        }
806
807 3
        foreach ($this->allowedBundleNames as $bundleName) {
808 3
            if ($this->isAllowedBundleDependencies($name, $this->loadBundle($bundleName))) {
809 1
                return;
810
            }
811
        }
812
813 3
        throw new RuntimeException("The \"{$name}\" asset bundle is not allowed.");
814
    }
815
816
    /**
817
     * Recursively checks whether the asset bundle name is allowed in dependencies.
818
     *
819
     * @param string $name The asset bundle name to check.
820
     * @param AssetBundle $bundle The asset bundle to check.
821
     *
822
     * @throws InvalidConfigException For invalid asset bundle configuration.
823
     *
824
     * @return bool Whether the asset bundle name is allowed in dependencies.
825
     */
826 3
    private function isAllowedBundleDependencies(string $name, AssetBundle $bundle): bool
827
    {
828 3
        foreach ($bundle->depends as $depend) {
829 2
            if ($name === $depend || $this->isAllowedBundleDependencies($name, $this->loadBundle($depend))) {
830 1
                return true;
831
            }
832
        }
833
834 3
        return false;
835
    }
836
837 41
    private function mergeWithReverseOrder(array $a, array $b): array
838
    {
839 41
        foreach ($a as $key => $value) {
840 8
            if (is_int($key)) {
841
                $b[] = $value;
842 8
            } elseif (!array_key_exists($key, $b)) {
843 8
                $b[$key] = $value;
844
            }
845
        }
846 41
        return $b;
847
    }
848
849
    /**
850
     * @param mixed $value
851
     */
852
    private function getType($value): string
853
    {
854
        return is_object($value) ? get_class($value) : gettype($value);
855
    }
856
}
857