Passed
Push — master ( f68db4...f935c2 )
by Alexander
01:36
created

AssetManager::convertJs()   B

Complexity

Conditions 7
Paths 7

Size

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

264
                            /** @scrutinizer ignore-type */ $bundle->basePath,
Loading history...
265
                            $bundle->converterOptions
266
                        ));
267
268 1
                        $bundle->css[$i] = $css;
269
                    }
270
                }
271 16
            } elseif (AssetUtil::isRelative($css)) {
272 16
                if (is_file("$bundle->basePath/$css")) {
273 6
                    $bundle->css[$i] = $this->converter->convert($css, $bundle->basePath, $bundle->converterOptions);
274
                }
275
            }
276
        }
277
278 21
        return $bundle;
279
    }
280
281
    /**
282
     * Convert files CoffeScript, TypeScript and other formats to JavaScript.
283
     *
284
     * @param AssetBundle $bundle
285
     *
286
     * @return AssetBundle
287
     */
288 21
    private function convertJs(AssetBundle $bundle): AssetBundle
289
    {
290 21
        foreach ($bundle->js as $i => $js) {
291 21
            if (\is_array($js)) {
292 2
                $file = \array_shift($js);
293 2
                if (AssetUtil::isRelative($file)) {
294 1
                    $js = \array_merge($bundle->jsOptions, $js);
295
296 1
                    if (is_file("$bundle->basePath/$file")) {
297
                        \array_unshift($js, $this->converter->convert(
298
                            $file,
299
                            $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

299
                            /** @scrutinizer ignore-type */ $bundle->basePath,
Loading history...
300
                            $bundle->converterOptions
301
                        ));
302
303 2
                        $bundle->js[$i] = $js;
304
                    }
305
                }
306 20
            } elseif (AssetUtil::isRelative($js)) {
307 20
                if (is_file("$bundle->basePath/$js")) {
308 6
                    $bundle->js[$i] = $this->converter->convert($js, $bundle->basePath);
309
                }
310
            }
311
        }
312
313 21
        return $bundle;
314
    }
315
316
    /**
317
     * Registers the named asset bundle.
318
     *
319
     * All dependent asset bundles will be registered.
320
     *
321
     * @param string $name the class name of the asset bundle (without the leading backslash)
322
     * @param int|null $position if set, this forces a minimum position for javascript files. This will adjust depending
323
     * assets javascript file position or fail if requirement can not be met. If this is null, asset
324
     * bundles position settings will not be changed.
325
     *
326
     * {@see registerJsFile()} for more details on javascript position.
327
     *
328
     * @return AssetBundle the registered asset bundle instance
329
     * @throws InvalidConfigException
330
     *
331
     * @throws \RuntimeException if the asset bundle does not exist or a circular dependency is detected
332
     */
333 26
    private function registerAssetBundle(string $name, ?int $position = null): AssetBundle
334
    {
335 26
        if (!isset($this->assetBundles[$name])) {
336 26
            $bundle = $this->getBundle($name);
337
338 24
            $this->assetBundles[$name] = false;
339
340
            // register dependencies
341 24
            $pos = $bundle->jsOptions['position'] ?? null;
342
343 24
            foreach ($bundle->depends as $dep) {
344 20
                $this->registerAssetBundle($dep, $pos);
345
            }
346
347 23
            $this->assetBundles[$name] = $bundle;
348 10
        } elseif ($this->assetBundles[$name] === false) {
349 1
            throw new \RuntimeException("A circular dependency is detected for bundle '$name'.");
350
        } else {
351 9
            $bundle = $this->assetBundles[$name];
352
        }
353
354 23
        if ($position !== null) {
355 11
            $pos = $bundle->jsOptions['position'] ?? null;
356
357 11
            if ($pos === null) {
358 10
                $bundle->jsOptions['position'] = $pos = $position;
359 5
            } elseif ($pos > $position) {
360 4
                throw new \RuntimeException(
361 4
                    "An asset bundle that depends on '$name' has a higher javascript file " .
362 4
                    "position configured than '$name'."
363
                );
364
            }
365
366
            // update position for all dependencies
367 11
            foreach ($bundle->depends as $dep) {
368 7
                $this->registerAssetBundle($dep, $pos);
369
            }
370
        }
371 23
        return $bundle;
372
    }
373
374
    /**
375
     * Loads dummy bundle by name.
376
     *
377
     * @param string $bundleName AssetBunle name
378
     *
379
     * @return AssetBundle
380
     * @throws InvalidConfigException
381
     */
382
    private function loadDummyBundle(string $bundleName): AssetBundle
383
    {
384
        if (!isset($this->dummyBundles[$bundleName])) {
385
            $this->dummyBundles[$bundleName] = $this->publisher->loadBundle($bundleName, [
386
                'sourcePath' => null,
387
                'js' => [],
388
                'css' => [],
389
                'depends' => [],
390
            ]);
391
        }
392
393
        return $this->dummyBundles[$bundleName];
394
    }
395
396
    /**
397
     * Register assets from a named bundle and its dependencies
398
     *
399
     * @param string $bundleName
400
     *
401
     * @return void
402
     */
403 21
    private function registerFiles(string $bundleName): void
404
    {
405 21
        if (!isset($this->assetBundles[$bundleName])) {
406
            return;
407
        }
408
409 21
        $bundle = $this->assetBundles[$bundleName];
410
411 21
        foreach ($bundle->depends as $dep) {
412 17
            $this->registerFiles($dep);
413
        }
414
415 21
        $this->registerAssetFiles($bundle);
416 20
    }
417
418
    /**
419
     * Registers asset files from a bundle considering dependencies
420
     */
421 21
    private function registerAssetFiles(AssetBundle $bundle): void
422
    {
423 21
        if (isset($bundle->basePath, $bundle->baseUrl) && !empty($this->converter)) {
424 21
            $this->convertCss($bundle);
425 21
            $this->convertJs($bundle);
426
        }
427
428 21
        foreach ($bundle->js as $js) {
429 21
            if (\is_array($js)) {
430 2
                $file = array_shift($js);
431 2
                $options = array_merge($bundle->jsOptions, $js);
432 2
                $this->registerJsFile($this->publisher->getAssetUrl($bundle, $file), $options);
433 20
            } elseif ($js !== null) {
434 20
                $this->registerJsFile($this->publisher->getAssetUrl($bundle, $js), $bundle->jsOptions);
435
            }
436
        }
437
438 20
        foreach ($bundle->css as $css) {
439 15
            if (\is_array($css)) {
440 1
                $file = array_shift($css);
441 1
                $options = array_merge($bundle->cssOptions, $css);
442 1
                $this->registerCssFile($this->publisher->getAssetUrl($bundle, $file), $options);
443 15
            } elseif ($css !== null) {
444 15
                $this->registerCssFile($this->publisher->getAssetUrl($bundle, $css), $bundle->cssOptions);
445
            }
446
        }
447 20
    }
448
}
449