Test Failed
Push — master ( fe16bf...781931 )
by Alexander
78:08 queued 59:52
created

AssetManager::getRegisteredBundles()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 0
cts 0
cp 0
crap 2
rs 10
c 0
b 0
f 0
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 array_merge;
13
use function array_shift;
14
use function array_unshift;
15
use function is_array;
16
use function is_file;
17
use function is_int;
18
19
/**
20
 * AssetManager manages asset bundle configuration and loading.
21
 */
22
final class AssetManager
23
{
24
    /**
25
     * @var array AssetBundle[] list of the registered asset bundles.
26
     * The keys are the bundle names, and the values are the registered {@see AssetBundle} objects.
27
     *
28
     * {@see registerAssetBundle()}
29
     */
30
    private array $registeredBundles = [];
31
32
    /**
33
     * @var array The asset bundle configurations. This property is provided to customize asset bundles.
34
     * When a bundle is being loaded by {@see getBundle()}, if it has a corresponding configuration
35
     * specified here, the configuration will be applied to the bundle.
36
     *
37
     * The array keys are the asset class bundle names (without leading backslash).
38
     * If a value is false, it means the corresponding asset bundle is disabled and {@see getBundle()}
39
     * should return an instance of the specified asset bundle with empty property values.
40
     */
41
    private array $bundles = [];
42
    private array $dummyBundles = [];
43
    private array $cssFiles = [];
44
    private array $jsFiles = [];
45
    private array $jsStrings = [];
46
    private array $jsVar = [];
47
    private ?AssetConverterInterface $converter = null;
48
    private AssetPublisherInterface $publisher;
49
    private Aliases $aliases;
50
51
    public function __construct(Aliases $aliases, AssetPublisherInterface $publisher)
52
    {
53
        $this->aliases = $aliases;
54 67
        $this->publisher = $publisher;
55
    }
56 67
57 67
    /**
58 67
     * Returns the registered asset bundles.
59
     *
60
     * @return array The registered asset bundles {@see registeredBundles}.
61
     */
62
    public function getRegisteredBundles(): array
63
    {
64
        return $this->registeredBundles;
65 27
    }
66
67 27
    /**
68
     * Returns the named asset bundle.
69
     *
70
     * This method will first look for the bundle in {@see bundles}.
71
     * If not found, it will treat `$name` as the class of the asset bundle and create a new instance of it.
72
     *
73
     * @param string $name The class name of the asset bundle (without the leading backslash).
74
     *
75
     * @throws InvalidConfigException For invalid asset bundle configuration.
76
     *
77
     * @return AssetBundle The asset bundle instance.
78
     */
79
    public function getBundle(string $name): AssetBundle
80
    {
81
        if (!isset($this->bundles[$name])) {
82 34
            return $this->bundles[$name] = $this->publisher->loadBundle($name);
83
        }
84 34
85 29
        if ($this->bundles[$name] instanceof AssetBundle) {
86
            return $this->bundles[$name];
87
        }
88 17
89 1
        if (is_array($this->bundles[$name])) {
90
            return $this->bundles[$name] = $this->publisher->loadBundle($name, $this->bundles[$name]);
91
        }
92 16
93 14
        if ($this->bundles[$name] === false) {
94
            return $this->dummyBundles[$name] ??= $this->publisher->loadBundle($name, (array) (new AssetBundle()));
95
        }
96 2
97 1
        throw new InvalidConfigException("Invalid configuration of the \"{$name}\" asset bundle.");
98
    }
99
100 1
    public function getConverter(): ?AssetConverterInterface
101
    {
102
        return $this->converter;
103 1
    }
104
105 1
    public function getPublisher(): AssetPublisherInterface
106
    {
107
        return $this->publisher;
108
    }
109
110
    /**
111
     * Return config array CSS AssetBundle.
112
     *
113 16
     * @return array
114
     */
115 16
    public function getCssFiles(): array
116
    {
117
        return $this->cssFiles;
118
    }
119
120
    /**
121
     * Returns config array JS AssetBundle.
122
     *
123 25
     * @return array
124
     */
125 25
    public function getJsFiles(): array
126
    {
127
        return $this->jsFiles;
128
    }
129
130
    /**
131
     * Returns JS code blocks.
132
     *
133 1
     * @return array
134
     */
135 1
    public function getJsStrings(): array
136
    {
137
        return $this->jsStrings;
138
    }
139
140
    /**
141
     * Returns JS variables.
142
     *
143 1
     * @return array
144
     */
145 1
    public function getJsVar(): array
146
    {
147
        return $this->jsVar;
148 7
    }
149
150 7
    /**
151
     * Set the asset bundle configurations.
152
     *
153
     * When a bundle is being loaded by {@see getBundle()}, if it has a corresponding configuration
154
     * specified here, the configuration will be applied to the bundle.
155
     *
156
     * The array keys are the asset class bundle names (without leading backslash).
157
     * If a value is false, it means the corresponding asset bundle is disabled and {@see getBundle()}
158
     * should return an instance of the specified asset bundle with empty property values.
159
     *
160 67
     * @param array $bundles The asset bundle configurations.
161
     */
162 67
    public function setBundles(array $bundles): void
163 67
    {
164
        $this->bundles = $bundles;
165
    }
166
167
    /**
168
     * Sets the asset converter.
169
     *
170
     * @param AssetConverterInterface $converter The asset converter. This can be either an object implementing the
171 67
     * {@see AssetConverterInterface}, or a configuration array that can be used to create the asset converter object.
172
     */
173 67
    public function setConverter(AssetConverterInterface $converter): void
174 67
    {
175
        $this->converter = $converter;
176
    }
177
178
    /**
179
     * Generate the array configuration of the asset bundles {@see AssetBundle}.
180
     *
181 67
     * @param string[] $names
182
     * @param int|null $position
183 67
     *
184 67
     * @throws InvalidConfigException
185
     */
186
    public function register(array $names, ?int $position = null): void
187
    {
188
        foreach ($names as $name) {
189
            $this->registerAssetBundle($name, $position);
190
            $this->registerFiles($name);
191
        }
192
    }
193
194 67
    /**
195
     * Registers a CSS file.
196 67
     *
197 32
     * @param string $url The CSS file to be registered.
198 28
     * @param array $options The HTML attributes for the link tag.
199
     * @param string|null $key The key that identifies the CSS file.
200 67
     */
201
    private function registerCssFile(string $url, array $options = [], string $key = null): void
202
    {
203
        $key = $key ?: $url;
204
205
        $this->cssFiles[$key]['url'] = $url;
206
        $this->cssFiles[$key]['attributes'] = $options;
207
    }
208
209
    /**
210
     * Registers a JS file.
211
     *
212
     * @param string $url The JS file to be registered.
213 27
     * @param array $options The HTML attributes for the script tag. The following options are specially handled and
214
     * are not treated as HTML attributes:
215 27
     *
216
     * - `position`: specifies where the JS script tag should be inserted in a page. The possible values are:
217 27
     *     * {@see \Yiisoft\View\WebView::POSITION_HEAD} In the head section.
218 27
     *     * {@see \Yiisoft\View\WebView::POSITION_BEGIN} At the beginning of the body section.
219 27
     *     * {@see \Yiisoft\View\WebView::POSITION_END} At the end of the body section. This is the default value.
220
     * @param string|null $key The key that identifies the JS file.
221
     */
222
    private function registerJsFile(string $url, array $options = [], string $key = null): void
223
    {
224
        $key = $key ?: $url;
225
226
        if (!array_key_exists('position', $options)) {
227
            $options = array_merge(['position' => 3], $options);
228
        }
229
230
        $this->jsFiles[$key]['url'] = $url;
231
        $this->jsFiles[$key]['attributes'] = $options;
232
    }
233
234
    /**
235
     * Registers a JavaScript code block.
236
     *
237
     * @param string $jsString The JavaScript code block to be registered.
238 34
     * @param array $options The HTML attributes for the script tag. The following options are specially handled and
239
     * are not treated as HTML attributes:
240 34
     *
241
     * - `position`: specifies where the JS script tag should be inserted in a page. The possible values are:
242 34
     *     * {@see \Yiisoft\View\WebView::POSITION_HEAD} In the head section.
243 28
     *     * {@see \Yiisoft\View\WebView::POSITION_BEGIN} At the beginning of the body section.
244
     *     * {@see \Yiisoft\View\WebView::POSITION_END} At the end of the body section. This is the default value.
245
     * @param string|null $key The key that identifies the JS code block. If null, it will use $jsString as the key.
246 34
     * If two JS code blocks are registered with the same key, the latter will overwrite the former.
247 34
     */
248 34
    private function registerJsString(string $jsString, array $options = [], string $key = null): void
249
    {
250
        $key = $key ?: $jsString;
251
252
        if (!array_key_exists('position', $options)) {
253
            $options = array_merge(['position' => 3], $options);
254
        }
255
256
        $this->jsStrings[$key]['string'] = $jsString;
257
        $this->jsStrings[$key]['attributes'] = $options;
258
    }
259
260
    /**
261
     * Registers a JS variable.
262
     *
263
     * @param string $varName The variable name.
264
     * @param array|string $jsVar The JS code block to be registered.
265
     * @param array $options The HTML attributes for the script tag. The following options are specially handled and
266 4
     * are not treated as HTML attributes:
267
     *
268 4
     * - `position`: specifies where the JS script tag should be inserted in a page. The possible values are:
269
     *     * {@see \Yiisoft\View\WebView::POSITION_HEAD} In the head section. This is the default value.
270 4
     *     * {@see \Yiisoft\View\WebView::POSITION_BEGIN} At the beginning of the body section.
271 4
     *     * {@see \Yiisoft\View\WebView::POSITION_END} At the end of the body section.
272
     */
273
    private function registerJsVar(string $varName, $jsVar, array $options = []): void
274 4
    {
275 4
        if (!array_key_exists('position', $options)) {
276 4
            $options = array_merge(['position' => 1], $options);
277
        }
278
279
        $this->jsVar[$varName]['variables'] = $jsVar;
280
        $this->jsVar[$varName]['attributes'] = $options;
281
    }
282
283
    /**
284
     * Converter SASS, SCSS, Stylus and other formats to CSS.
285
     *
286
     * @param AssetBundle $bundle
287
     */
288
    private function convertCss(AssetBundle $bundle): void
289
    {
290
        foreach ($bundle->css as $i => $css) {
291
            if (is_array($css)) {
292
                $file = array_shift($css);
293
                if (AssetUtil::isRelative($file)) {
294
                    $css = array_merge($bundle->cssOptions, $css);
295 4
                    $baseFile = $this->aliases->get("{$bundle->basePath}/{$file}");
296
                    if (is_file($baseFile)) {
297 4
                        /**
298 4
                         * @psalm-suppress PossiblyNullArgument
299
                         * @psalm-suppress PossiblyNullReference
300
                         */
301 4
                        array_unshift($css, $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

301
                        array_unshift($css, $this->converter->/** @scrutinizer ignore-call */ 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...
302 4
                            $file,
303 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

303
                            /** @scrutinizer ignore-type */ $bundle->basePath,
Loading history...
304
                            $bundle->converterOptions,
305
                        ));
306
307
                        $bundle->css[$i] = $css;
308
                    }
309
                }
310
            } elseif (AssetUtil::isRelative($css)) {
311
                $baseCss = $this->aliases->get("{$bundle->basePath}/{$css}");
312 26
                if (is_file("$baseCss")) {
313
                    /**
314 26
                     * @psalm-suppress PossiblyNullArgument
315 18
                     * @psalm-suppress PossiblyNullReference
316 1
                     */
317 1
                    $bundle->css[$i] = $this->converter->convert(
318 1
                        $css,
319 1
                        $bundle->basePath,
320 1
                        $bundle->converterOptions
321
                    );
322
                }
323
            }
324
        }
325 1
    }
326 1
327 1
    /**
328 1
     * Convert files from TypeScript and other formats into JavaScript.
329
     *
330
     * @param AssetBundle $bundle
331 1
     */
332
    private function convertJs(AssetBundle $bundle): void
333
    {
334 18
        foreach ($bundle->js as $i => $js) {
335 18
            if (is_array($js)) {
336 18
                $file = array_shift($js);
337
                if (AssetUtil::isRelative($file)) {
338
                    $js = array_merge($bundle->jsOptions, $js);
339
                    $baseFile = $this->aliases->get("{$bundle->basePath}/{$file}");
340
                    if (is_file($baseFile)) {
341 17
                        /**
342 17
                         * @psalm-suppress PossiblyNullArgument
343 17
                         * @psalm-suppress PossiblyNullReference
344 17
                         */
345
                        array_unshift($js, $this->converter->convert(
346
                            $file,
347
                            $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

347
                            /** @scrutinizer ignore-type */ $bundle->basePath,
Loading history...
348
                            $bundle->converterOptions
349
                        ));
350 26
351
                        $bundle->js[$i] = $js;
352
                    }
353
                }
354
            } elseif (AssetUtil::isRelative($js)) {
355
                $baseJs = $this->aliases->get("{$bundle->basePath}/{$js}");
356
                if (is_file($baseJs)) {
357
                    /**
358
                     * @psalm-suppress PossiblyNullArgument
359
                     * @psalm-suppress PossiblyNullReference
360 26
                     */
361
                    $bundle->js[$i] = $this->converter->convert($js, $bundle->basePath);
362 26
                }
363 25
            }
364 2
        }
365 2
    }
366 1
367 1
    /**
368 1
     * Registers the named asset bundle.
369
     *
370
     * All dependent asset bundles will be registered.
371
     *
372
     * @param string $name The class name of the asset bundle (without the leading backslash).
373 1
     * @param int|null $position If set, this forces a minimum position for javascript files.
374 1
     * This will adjust depending assets javascript file position or fail if requirement can not be met.
375 1
     * If this is null, asset bundles position settings will not be changed.
376 1
     *
377
     * {@see registerJsFile()} For more details on javascript position.
378
     *
379 2
     * @throws RuntimeException If the asset bundle does not exist or a circular dependency is detected.
380
     */
381
    private function registerAssetBundle(string $name, int $position = null): void
382 24
    {
383 24
        if (!isset($this->registeredBundles[$name])) {
384 24
            $bundle = $this->getBundle($name);
385
386
            $this->registeredBundles[$name] = false;
387
388
            $pos = $bundle->jsOptions['position'] ?? null;
389 23
390
            foreach ($bundle->depends as $dep) {
391
                $this->registerAssetBundle($dep, $pos);
392
            }
393
394 26
            $this->registeredBundles[$name] = $bundle;
395
        } elseif ($this->registeredBundles[$name] === false) {
396
            throw new RuntimeException("A circular dependency is detected for bundle \"{$name}\".");
397
        } else {
398
            $bundle = $this->registeredBundles[$name];
399
        }
400
401
        if ($position !== null) {
402
            $pos = $bundle->jsOptions['position'] ?? null;
403
404
            if ($pos === null) {
405
                $bundle->jsOptions['position'] = $pos = $position;
406
            } elseif ($pos > $position) {
407
                throw new RuntimeException(
408
                    "An asset bundle that depends on \"{$name}\" has a higher JavaScript file " .
409
                    "position configured than \"{$name}\"."
410
                );
411
            }
412
413 32
            // update position for all dependencies
414
            foreach ($bundle->depends as $dep) {
415 32
                $this->registerAssetBundle($dep, $pos);
416 32
            }
417
        }
418 31
    }
419
420 31
    /**
421
     * Register assets from a named bundle and its dependencies.
422 31
     *
423 21
     * @param string $bundleName
424
     *
425
     * @throws InvalidConfigException
426 30
     */
427 10
    private function registerFiles(string $bundleName): void
428 1
    {
429
        if (!isset($this->registeredBundles[$bundleName])) {
430 9
            return;
431
        }
432
433 30
        $bundle = $this->registeredBundles[$bundleName];
434 11
435
        foreach ($bundle->depends as $dep) {
436 11
            $this->registerFiles($dep);
437 10
        }
438 5
439 4
        $this->registerAssetFiles($bundle);
440 4
    }
441 4
442
    /**
443
     * Registers asset files from a bundle considering dependencies.
444
     *
445
     * @param AssetBundle $bundle
446 11
     */
447 7
    private function registerAssetFiles(AssetBundle $bundle): void
448
    {
449
        if (isset($bundle->basePath, $bundle->baseUrl) && null !== $this->converter) {
450 30
            $this->convertCss($bundle);
451
            $this->convertJs($bundle);
452
        }
453
454
        foreach ($bundle->js as $js) {
455
            if (is_array($js)) {
456
                $file = array_shift($js);
457
                $options = array_merge($bundle->jsOptions, $js);
458
                $this->registerJsFile($this->publisher->getAssetUrl($bundle, $file), $options);
459
            } elseif ($js !== null) {
460 1
                $this->registerJsFile($this->publisher->getAssetUrl($bundle, $js), $bundle->jsOptions);
461
            }
462 1
        }
463 1
464 1
        foreach ($bundle->jsStrings as $key => $jsString) {
465
            $key = is_int($key) ? $jsString : $key;
466
            if (is_array($jsString)) {
467
                $string = array_shift($jsString);
468
                $this->registerJsString($string, $jsString, $key);
469
            } elseif ($jsString !== null) {
470
                $this->registerJsString($jsString, $bundle->jsOptions, $key);
471 1
            }
472
        }
473
474
        foreach ($bundle->jsVar as $key => $jsVar) {
475
            $this->registerJsVar($key, $jsVar, $jsVar);
476
        }
477
478
        foreach ($bundle->css as $css) {
479
            if (is_array($css)) {
480
                $file = array_shift($css);
481 28
                $options = array_merge($bundle->cssOptions, $css);
482
                $this->registerCssFile($this->publisher->getAssetUrl($bundle, $file), $options);
483 28
            } elseif ($css !== null) {
484
                $this->registerCssFile($this->publisher->getAssetUrl($bundle, $css), $bundle->cssOptions);
485
            }
486
        }
487 28
    }
488
}
489