Passed
Push — master ( 077456...b9061e )
by Evgeniy
02:14
created

AssetManager::convertJs()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 29
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 7

Importance

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