Passed
Push — master ( 400214...18db24 )
by Alexander
02:09
created

AssetManager::convertJs()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 34
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 7.7656

Importance

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

318
                        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...
319
                            $file,
320
                            $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

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

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