Passed
Push — master ( 18db24...82ddf4 )
by Alexander
02:21
created

AssetManager::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

325
                        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...
326 1
                            $file,
327 1
                            $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

327
                            /** @scrutinizer ignore-type */ $bundle->basePath,
Loading history...
328 1
                            $bundle->converterOptions
329
                        ));
330
331 1
                        $bundle->css[$i] = $css;
332
                    }
333
                }
334 18
            } elseif (AssetUtil::isRelative($css)) {
335 18
                $baseCss = $this->aliases->get("$bundle->basePath/$css");
336 18
                if (is_file("$baseCss")) {
337
                    /**
338
                     * @psalm-suppress PossiblyNullArgument
339
                     * @psalm-suppress PossiblyNullReference
340
                     */
341 17
                    $bundle->css[$i] = $this->converter->convert(
342 17
                        $css,
343 17
                        $bundle->basePath,
344 17
                        $bundle->converterOptions
345
                    );
346
                }
347
            }
348
        }
349
350 27
        return $bundle;
351
    }
352
353
    /**
354
     * Convert files from TypeScript and other formats into JavaScript.
355
     *
356
     * @param AssetBundle $bundle
357
     *
358
     * @return AssetBundle
359
     */
360 27
    private function convertJs(AssetBundle $bundle): AssetBundle
361
    {
362 27
        foreach ($bundle->js as $i => $js) {
363 26
            if (is_array($js)) {
364 2
                $file = array_shift($js);
365 2
                if (AssetUtil::isRelative($file)) {
366 1
                    $js = array_merge($bundle->jsOptions, $js);
367 1
                    $baseFile = $this->aliases->get("$bundle->basePath/$file");
368 1
                    if (is_file($baseFile)) {
369
                        /**
370
                         * @psalm-suppress PossiblyNullArgument
371
                         * @psalm-suppress PossiblyNullReference
372
                         */
373 1
                        array_unshift($js, $this->converter->convert(
374 1
                            $file,
375 1
                            $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

375
                            /** @scrutinizer ignore-type */ $bundle->basePath,
Loading history...
376 1
                            $bundle->converterOptions
377
                        ));
378
379 2
                        $bundle->js[$i] = $js;
380
                    }
381
                }
382 25
            } elseif (AssetUtil::isRelative($js)) {
383 25
                $baseJs = $this->aliases->get("$bundle->basePath/$js");
384 25
                if (is_file($baseJs)) {
385
                    /**
386
                     * @psalm-suppress PossiblyNullArgument
387
                     * @psalm-suppress PossiblyNullReference
388
                     */
389 24
                    $bundle->js[$i] = $this->converter->convert($js, $bundle->basePath);
390
                }
391
            }
392
        }
393
394 27
        return $bundle;
395
    }
396
397
    /**
398
     * Registers the named asset bundle.
399
     *
400
     * All dependent asset bundles will be registered.
401
     *
402
     * @param string $name the class name of the asset bundle (without the leading backslash)
403
     * @param int|null $position if set, this forces a minimum position for javascript files. This will adjust depending
404
     * assets javascript file position or fail if requirement can not be met. If this is null, asset
405
     * bundles position settings will not be changed.
406
     *
407
     * {@see registerJsFile()} for more details on javascript position.
408
     *
409
     * @throws RuntimeException if the asset bundle does not exist or a circular dependency
410
     * is detected.
411
     *
412
     * @return AssetBundle the registered asset bundle instance.
413
     */
414 33
    private function registerAssetBundle(string $name, int $position = null): AssetBundle
415
    {
416 33
        if (!isset($this->assetBundles[$name])) {
417 33
            $bundle = $this->getBundle($name);
418
419 30
            $this->assetBundles[$name] = false;
420
421 30
            $pos = $bundle->jsOptions['position'] ?? null;
422
423 30
            foreach ($bundle->depends as $dep) {
424 22
                $this->registerAssetBundle($dep, $pos);
425
            }
426
427 29
            $this->assetBundles[$name] = $bundle;
428 10
        } elseif ($this->assetBundles[$name] === false) {
429 1
            throw new RuntimeException("A circular dependency is detected for bundle '$name'.");
430
        } else {
431 9
            $bundle = $this->assetBundles[$name];
432
        }
433
434 29
        if ($position !== null) {
435 11
            $pos = $bundle->jsOptions['position'] ?? null;
436
437 11
            if ($pos === null) {
438 10
                $bundle->jsOptions['position'] = $pos = $position;
439 5
            } elseif ($pos > $position) {
440 4
                throw new RuntimeException(
441 4
                    "An asset bundle that depends on '$name' has a higher javascript file " .
442 4
                    "position configured than '$name'."
443
                );
444
            }
445
446
            // update position for all dependencies
447 11
            foreach ($bundle->depends as $dep) {
448 7
                $this->registerAssetBundle($dep, $pos);
449
            }
450
        }
451 29
        return $bundle;
452
    }
453
454
    /**
455
     * Loads dummy bundle by name.
456
     *
457
     * @param string $bundleName AssetBunle name
458
     *
459
     * @return AssetBundle
460
     */
461 1
    private function loadDummyBundle(string $bundleName): AssetBundle
462
    {
463 1
        if (!isset($this->dummyBundles[$bundleName])) {
464 1
            $this->dummyBundles[$bundleName] = $this->publisher->loadBundle($bundleName, [
465 1
                'sourcePath' => null,
466
                'js' => [],
467
                'css' => [],
468
                'depends' => [],
469
            ]);
470
        }
471
472 1
        return $this->dummyBundles[$bundleName];
473
    }
474
475
    /**
476
     * Register assets from a named bundle and its dependencies
477
     *
478
     * @param string $bundleName
479
     *
480
     * @throws InvalidConfigException
481
     */
482 27
    private function registerFiles(string $bundleName): void
483
    {
484 27
        if (!isset($this->assetBundles[$bundleName])) {
485
            return;
486
        }
487
488 27
        $bundle = $this->assetBundles[$bundleName];
489
490 27
        foreach ($bundle->depends as $dep) {
491 19
            $this->registerFiles($dep);
492
        }
493
494 27
        $this->registerAssetFiles($bundle);
495 26
    }
496
497
    /**
498
     * Registers asset files from a bundle considering dependencies
499
     *
500
     * @param AssetBundle $bundle
501
     */
502 27
    private function registerAssetFiles(AssetBundle $bundle): void
503
    {
504 27
        if (isset($bundle->basePath, $bundle->baseUrl) && null !== $this->converter) {
505 27
            $this->convertCss($bundle);
506 27
            $this->convertJs($bundle);
507
        }
508
509 27
        foreach ($bundle->js as $js) {
510 26
            if (is_array($js)) {
511 2
                $file = array_shift($js);
512 2
                $options = array_merge($bundle->jsOptions, $js);
513 2
                $this->registerJsFile($this->publisher->getAssetUrl($bundle, $file), $options);
514 25
            } elseif ($js !== null) {
515 25
                $this->registerJsFile($this->publisher->getAssetUrl($bundle, $js), $bundle->jsOptions);
516
            }
517
        }
518
519 26
        foreach ($bundle->jsStrings as $key => $jsString) {
520 5
            $key = is_int($key) ? $jsString : $key;
521 5
            if (\is_array($jsString)) {
522 5
                $string = array_shift($jsString);
523 5
                $this->registerJsString($string, $jsString, $key);
524 5
            } elseif ($jsString !== null) {
525 5
                $this->registerJsString($jsString, $bundle->jsOptions, $key);
526
            }
527
        }
528
529 26
        foreach ($bundle->jsVar as $key => $jsVar) {
530 5
            $this->registerJsVar($key, $jsVar, $jsVar);
531
        }
532
533 26
        foreach ($bundle->css as $css) {
534 18
            if (is_array($css)) {
535 1
                $file = array_shift($css);
536 1
                $options = array_merge($bundle->cssOptions, $css);
537 1
                $this->registerCssFile($this->publisher->getAssetUrl($bundle, $file), $options);
538 18
            } elseif ($css !== null) {
539 18
                $this->registerCssFile($this->publisher->getAssetUrl($bundle, $css), $bundle->cssOptions);
540
            }
541
        }
542 26
    }
543
}
544