Passed
Push — master ( ffccba...8b9472 )
by Alexander
03:13 queued 48s
created

AssetManager::getType()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 1
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 6
rs 10
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