AssetManager::getJsStrings()   A
last analyzed

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