Passed
Pull Request — master (#80)
by Evgeniy
02:28
created

AssetManager   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 444
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 108
dl 0
loc 444
ccs 125
cts 125
cp 1
rs 5.04
c 3
b 0
f 0
wmc 57

21 Methods

Rating   Name   Duplication   Size   Complexity  
A withPublisher() 0 5 1
A registerFiles() 0 9 2
A getJsStrings() 0 3 1
A registerAllAllowed() 0 9 3
A loadBundle() 0 23 6
A register() 0 11 4
A isRegisteredBundle() 0 3 1
A getCssFiles() 0 3 1
A getJsFiles() 0 3 1
A getBundle() 0 10 2
A withLoader() 0 6 1
A getJsVars() 0 3 1
A isAllowedBundleDependencies() 0 9 4
A getCssStrings() 0 3 1
A export() 0 11 3
A getAssetUrl() 0 3 1
A publishBundle() 0 7 4
A withConverter() 0 5 1
A __construct() 0 11 1
C registerAssetBundle() 0 45 13
A checkAllowedBundleName() 0 13 5

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 RuntimeException;
8
use Yiisoft\Aliases\Aliases;
9
use Yiisoft\Assets\Exception\InvalidConfigException;
10
11
use function in_array;
12
use function is_array;
13
14
/**
15
 * AssetManager manages asset bundle configuration and loading.
16
 *
17
 * @psalm-type CssFile = array{0:string,1?:int}&array
18
 * @psalm-type CssString = array{0:mixed,1?:int}&array
19
 * @psalm-type JsFile = array{0:string,1?:int}&array
20
 * @psalm-type JsString = array{0:mixed,1?:int}&array
21
 */
22
final class AssetManager
23
{
24
    /**
25
     * @var string[] List of names of allowed asset bundles. If the array is empty, then any asset bundles are allowed.
26
     */
27
    private array $allowedBundleNames;
28
29
    /**
30
     * @var array The asset bundle configurations. This property is provided to customize asset bundles.
31
     */
32
    private array $customizedBundles;
33
34
    /**
35
     * @var array AssetBundle[] list of the registered asset bundles.
36
     * The keys are the bundle names, and the values are the registered {@see AssetBundle} objects.
37
     *
38
     * {@see registerAssetBundle()}
39
     */
40
    private array $registeredBundles = [];
41
42
    private array $loadedBundles = [];
43
    private array $dummyBundles = [];
44
45
    private ?AssetPublisherInterface $publisher = null;
46
    private AssetLoaderInterface $loader;
47
    private AssetRegistrar $registrar;
48
    private Aliases $aliases;
49
50
    /**
51
     * @param Aliases $aliases The aliases instance.
52
     * @param AssetLoaderInterface $loader The loader instance.
53
     * @param string[] $allowedBundleNames List of names of allowed asset bundles. If the array is empty, then any
54
     * asset bundles are allowed. If the names of allowed asset bundles were specified, only these asset bundles
55
     * or their dependencies can be registered {@see register()} and obtained {@see getBundle()}. Also, specifying
56
     * names allows to export {@see export()} asset bundles automatically without first registering them manually.
57
     * @param array $customizedBundles The asset bundle configurations. Provided to customize asset bundles.
58
     * When a bundle is being loaded by {@see getBundle()}, if it has a corresponding configuration specified
59
     * here, the configuration will be applied to the bundle. The array keys are the asset class bundle names
60
     * (without leading backslash). If a value is false, it means the corresponding asset bundle is disabled
61
     * and {@see getBundle()} should return an instance of the specified asset bundle with empty property values.
62
     */
63 95
    public function __construct(
64
        Aliases $aliases,
65
        AssetLoaderInterface $loader,
66
        array $allowedBundleNames = [],
67
        array $customizedBundles = []
68
    ) {
69 95
        $this->aliases = $aliases;
70 95
        $this->loader = $loader;
71 95
        $this->allowedBundleNames = $allowedBundleNames;
72 95
        $this->customizedBundles = $customizedBundles;
73 95
        $this->registrar = new AssetRegistrar($this->aliases, $this->loader);
74 95
    }
75
76
    /**
77
     * Returns a cloned named asset bundle.
78
     *
79
     * This method will first look for the bundle in {@see $customizedBundles}.
80
     * If not found, it will treat `$name` as the class of the asset bundle and create a new instance of it.
81
     * If `$name` is not a class name, an {@see AssetBundle} instance will be created.
82
     *
83
     * Cloning is used to prevent an asset bundle instance from being modified in a non-context of the asset manager.
84
     *
85
     * @param string $name The class name of the asset bundle (without the leading backslash).
86
     *
87
     * @throws InvalidConfigException For invalid asset bundle configuration.
88
     *
89
     * @return AssetBundle The asset bundle instance.
90
     */
91 9
    public function getBundle(string $name): AssetBundle
92
    {
93 9
        if (!empty($this->allowedBundleNames)) {
94 4
            $this->checkAllowedBundleName($name);
95
        }
96
97 9
        $bundle = $this->loadBundle($name);
98 9
        $bundle = $this->publishBundle($bundle);
99
100 9
        return clone $bundle;
101
    }
102
103
    /**
104
     * Returns the actual URL for the specified asset.
105
     *
106
     * @param string $name The asset bundle name.
107
     * @param string $path The asset path.
108
     *
109
     * @throws InvalidConfigException If asset files are not found.
110
     *
111
     * @return string The actual URL for the specified asset.
112
     */
113 1
    public function getAssetUrl(string $name, string $path): string
114
    {
115 1
        return $this->loader->getAssetUrl($this->getBundle($name), $path);
116
    }
117
118
    /**
119
     * Return config array CSS AssetBundle.
120
     *
121
     * @psalm-return CssFile[]
122
     */
123 12
    public function getCssFiles(): array
124
    {
125 12
        return $this->registrar->getCssFiles();
126
    }
127
128
    /**
129
     * Returns CSS blocks.
130
     *
131
     * @return array
132
     * @psalm-return CssString[]
133
     */
134 2
    public function getCssStrings(): array
135
    {
136 2
        return $this->registrar->getCssStrings();
137
    }
138
139
    /**
140
     * Returns config array JS AssetBundle.
141
     *
142
     * @psalm-return JsFile[]
143
     */
144 22
    public function getJsFiles(): array
145
    {
146 22
        return $this->registrar->getJsFiles();
147
    }
148
149
    /**
150
     * Returns JS code blocks.
151
     *
152
     * @return array
153
     * @psalm-return JsString[]
154
     */
155 3
    public function getJsStrings(): array
156
    {
157 3
        return $this->registrar->getJsStrings();
158
    }
159
160
    /**
161
     * Returns JS variables.
162
     *
163
     * @return array
164
     */
165 3
    public function getJsVars(): array
166
    {
167 3
        return $this->registrar->getJsVars();
168
    }
169
170
    /**
171
     * Returns a new instance with the specified converter.
172
     *
173
     * @param AssetConverterInterface $converter
174
     *
175
     * @return self
176
     */
177 95
    public function withConverter(AssetConverterInterface $converter): self
178
    {
179 95
        $new = clone $this;
180 95
        $new->registrar = $new->registrar->withConverter($converter);
181 95
        return $new;
182
    }
183
184
    /**
185
     * Returns a new instance with the specified loader.
186
     *
187
     * @param AssetLoaderInterface $loader
188
     *
189
     * @return self
190
     */
191 5
    public function withLoader(AssetLoaderInterface $loader): self
192
    {
193 5
        $new = clone $this;
194 5
        $new->loader = $loader;
195 5
        $new->registrar = $new->registrar->withLoader($new->loader);
196 5
        return $new;
197
    }
198
199
    /**
200
     * Returns a new instance with the specified publisher.
201
     *
202
     * @param AssetPublisherInterface $publisher
203
     *
204
     * @return self
205
     */
206 95
    public function withPublisher(AssetPublisherInterface $publisher): self
207
    {
208 95
        $new = clone $this;
209 95
        $new->publisher = $publisher;
210 95
        return $new;
211
    }
212
213
    /**
214
     * Exports registered asset bundles.
215
     *
216
     * When using the allowed asset bundles, the export result will always be the same,
217
     * since the asset bundles are registered before the export. If do not use the allowed asset bundles mode,
218
     * must register {@see register()} all the required asset bundles before exporting.
219
     *
220
     * @param AssetExporterInterface $exporter The exporter instance.
221
     *
222
     * @throws InvalidConfigException If an error occurs during registration when using allowed asset bundles.
223
     * @throws RuntimeException If no asset bundles were registered or an error occurred during the export.
224
     */
225 8
    public function export(AssetExporterInterface $exporter): void
226
    {
227 8
        if (!empty($this->allowedBundleNames)) {
228 3
            $this->registerAllAllowed();
229
        }
230
231 8
        if (empty($this->registeredBundles)) {
232 1
            throw new RuntimeException('Not a single asset bundle was registered.');
233
        }
234
235 7
        $exporter->export($this->registeredBundles);
236 7
    }
237
238
    /**
239
     * Registers asset bundles by names.
240
     *
241
     * @param string[] $names
242
     *
243
     * @throws InvalidConfigException
244
     * @throws RuntimeException
245
     */
246 95
    public function register(array $names, ?int $jsPosition = null, ?int $cssPosition = null): void
247
    {
248 95
        if (!empty($this->allowedBundleNames)) {
249 3
            foreach ($names as $name) {
250 3
                $this->checkAllowedBundleName($name);
251
            }
252
        }
253
254 95
        foreach ($names as $name) {
255 60
            $this->registerAssetBundle($name, $jsPosition, $cssPosition);
256 54
            $this->registerFiles($name);
257
        }
258 95
    }
259
260
    /**
261
     * Registers all allowed asset bundles.
262
     *
263
     * @throws InvalidConfigException
264
     * @throws RuntimeException
265
     */
266 5
    public function registerAllAllowed(): void
267
    {
268 5
        if (empty($this->allowedBundleNames)) {
269 1
            throw new RuntimeException('The allowed names of the asset bundles were not set.');
270
        }
271
272 4
        foreach ($this->allowedBundleNames as $name) {
273 4
            $this->registerAssetBundle($name);
274 4
            $this->registerFiles($name);
275
        }
276 4
    }
277
278
    /**
279
     * Returns whether the asset bundle is registered.
280
     *
281
     * @param string $name The class name of the asset bundle (without the leading backslash).
282
     *
283
     * @return bool Whether the asset bundle is registered.
284
     */
285 4
    public function isRegisteredBundle(string $name): bool
286
    {
287 4
        return isset($this->registeredBundles[$name]);
288
    }
289
290
    /**
291
     * Registers the named asset bundle.
292
     *
293
     * All dependent asset bundles will be registered.
294
     *
295
     * @param string $name The class name of the asset bundle (without the leading backslash).
296
     * @param int|null $jsPosition If set, this forces a minimum position for javascript files.
297
     * This will adjust depending assets javascript file position or fail if requirement can not be met.
298
     * If this is null, asset bundles position settings will not be changed.
299
     *
300
     * {@see AssetRegistrar::registerJsFile()} For more details on javascript position.
301
     *
302
     * @throws InvalidConfigException If the asset or the asset file paths to be published does not exist.
303
     * @throws RuntimeException If the asset bundle does not exist or a circular dependency is detected.
304
     */
305 64
    private function registerAssetBundle(string $name, ?int $jsPosition = null, ?int $cssPosition = null): void
306
    {
307 64
        if (!isset($this->registeredBundles[$name])) {
308 64
            $bundle = $this->publishBundle($this->loadBundle($name));
309
310 63
            $this->registeredBundles[$name] = false;
311
312 63
            foreach ($bundle->depends as $dep) {
313 35
                $this->registerAssetBundle($dep, $bundle->jsPosition, $bundle->cssPosition);
314
            }
315
316 62
            unset($this->registeredBundles[$name]);
317 62
            $this->registeredBundles[$name] = $bundle;
318 14
        } elseif ($this->registeredBundles[$name] === false) {
319 1
            throw new RuntimeException("A circular dependency is detected for bundle \"{$name}\".");
320
        } else {
321 13
            $bundle = $this->registeredBundles[$name];
322
        }
323
324 62
        if ($jsPosition !== null || $cssPosition !== null) {
325 16
            if ($jsPosition !== null) {
326 12
                if ($bundle->jsPosition === null) {
327 11
                    $bundle->jsPosition = $jsPosition;
328 5
                } elseif ($bundle->jsPosition > $jsPosition) {
329 4
                    throw new RuntimeException(
330 4
                        "An asset bundle that depends on \"{$name}\" has a higher JavaScript file " .
331 4
                        "position configured than \"{$name}\"."
332
                    );
333
                }
334
            }
335
336 16
            if ($cssPosition !== null) {
337 5
                if ($bundle->cssPosition === null) {
338 1
                    $bundle->cssPosition = $cssPosition;
339 4
                } elseif ($bundle->cssPosition > $cssPosition) {
340 4
                    throw new RuntimeException(
341 4
                        "An asset bundle that depends on \"{$name}\" has a higher CSS file " .
342 4
                        "position configured than \"{$name}\"."
343
                    );
344
                }
345
            }
346
347
            // update position for all dependencies
348 12
            foreach ($bundle->depends as $dep) {
349 7
                $this->registerAssetBundle($dep, $bundle->jsPosition, $bundle->cssPosition);
350
            }
351
        }
352 60
    }
353
354
    /**
355
     * Register assets from a named bundle and its dependencies.
356
     *
357
     * @param string $bundleName The asset bundle name.
358
     *
359
     * @throws InvalidConfigException If asset files are not found.
360
     */
361 58
    private function registerFiles(string $bundleName): void
362
    {
363 58
        $bundle = $this->registeredBundles[$bundleName];
364
365 58
        foreach ($bundle->depends as $dep) {
366 28
            $this->registerFiles($dep);
367
        }
368
369 58
        $this->registrar->register($bundle);
370 45
    }
371
372
    /**
373
     * Loads an asset bundle class by name.
374
     *
375
     * @param string $name The asset bundle name.
376
     *
377
     * @throws InvalidConfigException For invalid asset bundle configuration.
378
     *
379
     * @return AssetBundle The asset bundle instance.
380
     */
381 67
    private function loadBundle(string $name): AssetBundle
382
    {
383 67
        if (isset($this->loadedBundles[$name])) {
384 8
            return $this->loadedBundles[$name];
385
        }
386
387 67
        if (!isset($this->customizedBundles[$name])) {
388 58
            return $this->loadedBundles[$name] = $this->loader->loadBundle($name);
389
        }
390
391 24
        if ($this->customizedBundles[$name] instanceof AssetBundle) {
392 1
            return $this->loadedBundles[$name] = $this->customizedBundles[$name];
393
        }
394
395 23
        if (is_array($this->customizedBundles[$name])) {
396 21
            return $this->loadedBundles[$name] = $this->loader->loadBundle($name, $this->customizedBundles[$name]);
397
        }
398
399 2
        if ($this->customizedBundles[$name] === false) {
400 1
            return $this->dummyBundles[$name] ??= $this->loader->loadBundle($name, (array) (new AssetBundle()));
401
        }
402
403 1
        throw new InvalidConfigException("Invalid configuration of the \"{$name}\" asset bundle.");
404
    }
405
406
    /**
407
     * Publishes a asset bundle.
408
     *
409
     * @param AssetBundle $bundle The asset bundle to publish.
410
     *
411
     * @throws InvalidConfigException If the asset or the asset file paths to be published does not exist.
412
     *
413
     * @return AssetBundle The published asset bundle.
414
     */
415 66
    private function publishBundle(AssetBundle $bundle): AssetBundle
416
    {
417 66
        if (!$bundle->cdn && $this->publisher !== null && !empty($bundle->sourcePath)) {
418 13
            [$bundle->basePath, $bundle->baseUrl] = $this->publisher->publish($bundle);
419
        }
420
421 66
        return $bundle;
422
    }
423
424
    /**
425
     * Checks whether asset bundle are allowed by name {@see $allowedBundleNames}.
426
     *
427
     * @param string $name The asset bundle name to check.
428
     *
429
     * @throws InvalidConfigException For invalid asset bundle configuration.
430
     * @throws RuntimeException If The asset bundle name is not allowed.
431
     */
432 4
    public function checkAllowedBundleName(string $name): void
433
    {
434 4
        if (isset($this->loadedBundles[$name]) || in_array($name, $this->allowedBundleNames, true)) {
435 4
            return;
436
        }
437
438 3
        foreach ($this->allowedBundleNames as $bundleName) {
439 3
            if ($this->isAllowedBundleDependencies($name, $this->loadBundle($bundleName))) {
440 1
                return;
441
            }
442
        }
443
444 3
        throw new RuntimeException("The \"{$name}\" asset bundle is not allowed.");
445
    }
446
447
    /**
448
     * Recursively checks whether the asset bundle name is allowed in dependencies.
449
     *
450
     * @param string $name The asset bundle name to check.
451
     * @param AssetBundle $bundle The asset bundle to check.
452
     *
453
     * @throws InvalidConfigException For invalid asset bundle configuration.
454
     *
455
     * @return bool Whether the asset bundle name is allowed in dependencies.
456
     */
457 3
    private function isAllowedBundleDependencies(string $name, AssetBundle $bundle): bool
458
    {
459 3
        foreach ($bundle->depends as $depend) {
460 2
            if ($name === $depend || $this->isAllowedBundleDependencies($name, $this->loadBundle($depend))) {
461 1
                return true;
462
            }
463
        }
464
465 3
        return false;
466
    }
467
}
468