Passed
Push — master ( 5a3114...9212c9 )
by Alexander
06:49
created

AssetManager   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 433
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 120
dl 0
loc 433
ccs 0
cts 198
cp 0
rs 5.04
c 3
b 0
f 0
wmc 57

19 Methods

Rating   Name   Duplication   Size   Complexity  
A setConverter() 0 3 1
B convertJs() 0 26 7
A getPublisher() 0 3 1
A getAssetBundles() 0 3 1
A registerFiles() 0 13 3
A register() 0 5 2
A getConverter() 0 3 1
A getCssFiles() 0 3 1
A getJsFiles() 0 3 1
B registerAssetFiles() 0 24 9
A getBundle() 0 19 5
B convertCss() 0 26 7
A registerCssFile() 0 6 2
A loadDummyBundle() 0 12 2
A registerJsFile() 0 10 3
A setBundles() 0 3 1
A __construct() 0 3 1
A setPublisher() 0 3 1
B registerAssetBundle() 0 39 8

How to fix   Complexity   

Complex Class

Complex classes like AssetManager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AssetManager, and based on these observations, apply Extract Interface, too.

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

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

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