Passed
Push — master ( f73c85...40da57 )
by Alexander
02:09
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\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 AssetConverterInterface $converter;
47
    private AssetPublisherInterface $publisher;
48
49
    /**
50
     * Registers the asset manager being used by this view object.
51
     *
52
     * @return array the asset manager. Defaults to the "assetManager" application component.
53
     */
54 25
    public function getAssetBundles(): array
55
    {
56 25
        return $this->assetBundles;
57
    }
58
59
    /**
60
     * Returns the named asset bundle.
61
     *
62
     * This method will first look for the bundle in {@see bundles()}. If not found, it will treat `$name` as the class
63
     * of the asset bundle and create a new instance of it.
64
     *
65
     * @param string $name the class name of the asset bundle (without the leading backslash).
66
     *
67
     * @throws InvalidConfigException
68
     *
69
     * @return AssetBundle the asset bundle instance
70
     */
71 30
    public function getBundle(string $name): AssetBundle
72
    {
73 30
        if (!isset($this->bundles[$name])) {
74 26
            return $this->bundles[$name] = $this->publisher->loadBundle($name, []);
75
        }
76
77 15
        if ($this->bundles[$name] instanceof AssetBundle) {
78
            return $this->bundles[$name];
79
        }
80
81 15
        if (is_array($this->bundles[$name])) {
82 15
            return $this->bundles[$name] = $this->publisher->loadBundle($name, $this->bundles[$name]);
83
        }
84
85
        if ($this->bundles[$name] === false) {
86
            return $this->loadDummyBundle($name);
87
        }
88
89
        throw new InvalidConfigException("Invalid asset bundle configuration: $name");
90
    }
91
92 1
    public function getConverter(): AssetConverterInterface
93
    {
94 1
        return $this->converter;
95
    }
96
97
    /**
98
     * Return config array CSS AssetBundle.
99
     *
100
     * @return array
101
     */
102 15
    public function getCssFiles(): array
103
    {
104 15
        return $this->cssFiles;
105
    }
106
107
    /**
108
     * Return config array JS AssetBundle.
109
     *
110
     * @return array
111
     */
112 24
    public function getJsFiles(): array
113
    {
114 24
        return $this->jsFiles;
115
    }
116
117 7
    public function getPublisher(): AssetPublisherInterface
118
    {
119 7
        return $this->publisher;
120
    }
121
122
    /**
123
     * This property is provided to customize asset bundles.
124
     *
125
     * @param array $value
126
     *
127
     * {@see bundles}
128
     */
129 59
    public function setBundles(array $value): void
130
    {
131 59
        $this->bundles = $value;
132 59
    }
133
134
    /**
135
     * AssetConverter component.
136
     *
137
     * @param AssetConverterInterface $value the asset converter. This can be eitheran object implementing the
138
     * {@see AssetConverterInterface}, or a configuration array that can be used to create the asset converter object.
139
     */
140 59
    public function setConverter(AssetConverterInterface $value): void
141
    {
142 59
        $this->converter = $value;
143 59
    }
144
145
    /**
146
     * AssetPublisher component.
147
     *
148
     * @param AssetPublisherInterface $value
149
     *
150
     * {@see publisher}
151
     */
152 59
    public function setPublisher(AssetPublisherInterface $value): void
153
    {
154 59
        $this->publisher = $value;
155 59
    }
156
157
    /**
158
     * Generate the array configuration of the AssetBundles
159
     *
160
     * @param array $names
161
     * @param integer|null $position
162
     *
163
     * @throws InvalidConfigException
164
     */
165 59
    public function register(array $names, ?int $position = null): void
166
    {
167 59
        foreach ($names as $name) {
168 28
            $this->registerAssetBundle($name, $position);
169 23
            $this->registerFiles($name);
170
        }
171 59
    }
172
173
    /**
174
     * Registers a CSS file.
175
     *
176
     * This method should be used for simple registration of CSS files. If you want to use features of
177
     * {@see AssetManager} like appending timestamps to the URL and file publishing options, use {@see AssetBundle}
178
     * and {@see registerAssetBundle()} instead.
179
     *
180
     * @param string $url the CSS file to be registered.
181
     * @param array $options the HTML attributes for the link tag.
182
     * @param string|null $key
183
     */
184 26
    public function registerCssFile(string $url, array $options = [], string $key = null): void
185
    {
186 26
        $key = $key ?: $url;
187
188 26
        $this->cssFiles[$key]['url'] = $url;
189 26
        $this->cssFiles[$key]['attributes'] = $options;
190 26
    }
191
192
    /**
193
     * Registers a JS file.
194
     *
195
     * This method should be used for simple registration of JS files. If you want to use features of
196
     * {@see AssetManager} like appending timestamps to the URL and file publishing options, use {@see AssetBundle}
197
     * and {@see registerAssetBundle()} instead.
198
     *
199
     * @param string $url the JS file to be registered.
200
     * @param array $options the HTML attributes for the script tag. The following options are specially handled and
201
     * are not treated as HTML attributes:
202
     *
203
     * - `position`: specifies where the JS script tag should be inserted in a page. The possible values are:
204
     *     * {@see \Yiisoft\View\WebView::POSITION_HEAD} in the head section
205
     *     * {@see \Yiisoft\View\WebView::POSITION_BEGIN} at the beginning of the body section
206
     *     * {@see \Yiisoft\View\WebView::POSITION_END} at the end of the body section. This is the default value.
207
     * @param string|null $key
208
     */
209 32
    public function registerJsFile(string $url, array $options = [], string $key = null): void
210
    {
211 32
        $key = $key ?: $url;
212
213 32
        if (!array_key_exists('position', $options)) {
214 26
            $options = array_merge(['position' => 3], $options);
215
        }
216
217 32
        $this->jsFiles[$key]['url'] = $url;
218 32
        $this->jsFiles[$key]['attributes'] = $options;
219 32
    }
220
221
    /**
222
     * Converter SASS, SCSS, Stylus and other formats to CSS.
223
     *
224
     * @param AssetBundle $bundle
225
     *
226
     * @return AssetBundle
227
     */
228 23
    private function convertCss(AssetBundle $bundle): AssetBundle
229
    {
230 23
        foreach ($bundle->css as $i => $css) {
231 16
            if (is_array($css)) {
232 1
                $file = array_shift($css);
233 1
                if (AssetUtil::isRelative($file)) {
234 1
                    $css = array_merge($bundle->cssOptions, $css);
235
236 1
                    if (is_file("$bundle->basePath/$file")) {
237
                        array_unshift($css, $this->converter->convert(
238
                            $file,
239
                            $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

239
                            /** @scrutinizer ignore-type */ $bundle->basePath,
Loading history...
240
                            $bundle->converterOptions
241
                        ));
242
243 1
                        $bundle->css[$i] = $css;
244
                    }
245
                }
246 16
            } elseif (AssetUtil::isRelative($css)) {
247 16
                if (is_file("$bundle->basePath/$css")) {
248 6
                    $bundle->css[$i] = $this->converter->convert(
249 6
                        $css,
250 6
                        $bundle->basePath,
251 6
                        $bundle->converterOptions
252
                    );
253
                }
254
            }
255
        }
256
257 23
        return $bundle;
258
    }
259
260
    /**
261
     * Convert files CoffeScript, TypeScript and other formats to JavaScript.
262
     *
263
     * @param AssetBundle $bundle
264
     *
265
     * @return AssetBundle
266
     */
267 23
    private function convertJs(AssetBundle $bundle): AssetBundle
268
    {
269 23
        foreach ($bundle->js as $i => $js) {
270 23
            if (is_array($js)) {
271 2
                $file = array_shift($js);
272 2
                if (AssetUtil::isRelative($file)) {
273 1
                    $js = array_merge($bundle->jsOptions, $js);
274
275 1
                    if (is_file("$bundle->basePath/$file")) {
276
                        array_unshift($js, $this->converter->convert(
277
                            $file,
278
                            $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

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