Completed
Push — master ( 5ff4ab...0cf65a )
by Alexander
16s queued 10s
created

AssetManager::getPublishedUrl()   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 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
ccs 2
cts 2
cp 1
crap 1
1
<?php
2
declare(strict_types=1);
3
4
namespace Yiisoft\Assets;
5
6
use Psr\Log\LoggerInterface;
7
use Yiisoft\Aliases\Aliases;
8
use Yiisoft\Assets\Exception\InvalidConfigException;
9
10
/**
11
 * AssetManager manages asset bundle configuration and loading.
12
 *
13
 * AssetManager is configured in config/web.php. You can access that instance via $container->get(AssetManager::class).
14
 *
15
 * You can modify its configuration by adding an array to your application config under `components` as shown in the
16
 * following example:
17
 *
18
 * ```php
19
 * AssetManager::class => function (ContainerInterface $container) {
20
 *     $aliases = $container->get(Aliases::class);
21
 *     $assetConverterInterface = $container->get(AssetConverterInterface::class);
22
 *     $fileSystem = $container->get(Filesystem::class);
23
 *     $logger = $container->get(LoggerInterface::class);
24
 *
25
 *     $assetManager = new AssetManager($fileSystem, $logger);
26
 *
27
 *     $assetManager->setBasePath($aliases->get('@basePath'));
28
 *     $assetManager->setBaseUrl($aliases->get('@baseUrl'));
29
 *     $assetManager->setConverter($assetConverterInterface);
30
 *
31
 *     return $assetManager;
32
 * },
33
 * ```
34
 */
35
final class AssetManager
36
{
37
    private Aliases $aliases;
38
39
    /**
40
     * @var array AssetBundle[] list of the registered asset bundles. The keys are the bundle names, and the values
41
     * are the registered {@see AssetBundle} objects.
42
     *
43
     * {@see registerAssetBundle()}
44
     */
45
    private array $assetBundles = [];
46
47
    /**
48
     * @var bool whether to append a timestamp to the URL of every published asset. When this is true, the URL of a
49
     * published asset may look like `/path/to/asset?v=timestamp`, where `timestamp` is the last modification time of
50
     * the published asset file. You normally would want to set this property to true when you have enabled HTTP caching
51
     * for assets, because it allows you to bust caching when the assets are updated.
52
     */
53
    private bool $appendTimestamp = false;
54
55
    /**
56
     * @var array mapping from source asset files (keys) to target asset files (values).
57
     *
58
     * This property is provided to support fixing incorrect asset file paths in some asset bundles. When an asset
59
     * bundle is registered with a view, each relative asset file in its {@see AssetBundle::css|css} and
60
     * {@see AssetBundle::js|js} arrays will be examined against this map. If any of the keys is found to be the last
61
     * part of an asset file (which is prefixed with {@see AssetBundle::sourcePath} if available), the corresponding
62
     * value will replace the asset and be registered with the view. For example, an asset file `my/path/to/jquery.js`
63
     * matches a key `jquery.js`.
64
     *
65
     * Note that the target asset files should be absolute URLs, domain relative URLs (starting from '/') or paths
66
     * relative to {@see baseUrl} and {@see basePath}.
67
     *
68
     * In the following example, any assets ending with `jquery.min.js` will be replaced with `jquery/dist/jquery.js`
69
     * which is relative to {@see baseUrl} and {@see basePath}.
70
     *
71
     * ```php
72
     * [
73
     *     'jquery.min.js' => 'jquery/dist/jquery.js',
74
     * ]
75
     * ```
76
     */
77
    private array $assetMap = [];
78
79
    /**
80
     * @var AssetPublisher published assets
81
     */
82
    private AssetPublisher $publish;
83
84
    /**
85
     * @var string|null the root directory storing the published asset files.
86
     */
87
    private ?string $basePath = null;
88
89
    /**
90
     * @var string|null the base URL through which the published asset files can be accessed.
91
     */
92
    private ?string $baseUrl = null;
93
94
    /**
95
     * @var array list of asset bundle configurations. This property is provided to customize asset bundles.
96
     * When a bundle is being loaded by {@see getBundle()}, if it has a corresponding configuration specified here, the
97
     * configuration will be applied to the bundle.
98
     *
99
     * The array keys are the asset bundle names, which typically are asset bundle class names without leading
100
     * backslash. The array values are the corresponding configurations. If a value is false, it means the corresponding
101
     * asset bundle is disabled and {@see getBundle()} should return null.
102
     *
103
     * If this property is false, it means the whole asset bundle feature is disabled and {@see {getBundle()} will
104
     * always return null.
105
     */
106
    private array $bundles = [];
107
108
    /**
109
     * AssetConverter component.
110
     *
111
     * @var AssetConverterInterface $converter
112
     */
113
    private AssetConverterInterface $converter;
114
115
    /**
116
     * @var array the registered CSS files.
117
     *
118
     * {@see registerCssFile()}
119
     */
120
    private array $cssFiles = [];
121
122
    /**
123
     * @var int the permission to be set for newly generated asset directories. This value will be used by PHP chmod()
124
     * function. No umask will be applied. Defaults to 0775, meaning the directory is read-writable by owner
125
     * and group, but read-only for other users.
126
     */
127
    private int $dirMode = 0775;
128
129
    /**
130
     * @var array $dummyBundles
131
     */
132
    private array $dummyBundles;
133
134
    /**
135
     * @var int the permission to be set for newly published asset files. This value will be used by PHP chmod()
136
     * function. No umask will be applied. If not set, the permission will be determined by the current
137
     * environment.
138
     */
139
    private int $fileMode = 0755;
140
141
    /**
142
     * @var bool whether the directory being published should be copied even if it is found in the target directory.
143
     * This option is used only when publishing a directory. You may want to set this to be `true` during the
144
     * development stage to make sure the published directory is always up-to-date. Do not set this to true
145
     * on production servers as it will significantly degrade the performance.
146
     */
147
    private bool $forceCopy = false;
148
149
    /**
150
     * @var callable a callback that will be called to produce hash for asset directory generation. The signature of the
151
     * callback should be as follows:
152
     *
153
     * ```
154
     * function ($path)
155
     * ```
156
     *
157
     * where `$path` is the asset path. Note that the `$path` can be either directory where the asset files reside or a
158
     * single file. For a CSS file that uses relative path in `url()`, the hash implementation should use the directory
159
     * path of the file instead of the file path to include the relative asset files in the copying.
160
     *
161
     * If this is not set, the asset manager will use the default CRC32 and filemtime in the `hash` method.
162
     *
163
     * Example of an implementation using MD4 hash:
164
     *
165
     * ```php
166
     * function ($path) {
167
     *     return hash('md4', $path);
168
     * }
169
     * ```
170
     */
171
    private $hashCallback;
172
173
    /**
174
     * @var bool whether to use symbolic link to publish asset files. Defaults to false, meaning asset files are copied
175
     * to {@see basePath}. Using symbolic links has the benefit that the published assets will always be
176
     * consistent with the source assets and there is no copy operation required. This is especially useful
177
     * during development.
178
     *
179
     * However, there are special requirements for hosting environments in order to use symbolic links. In particular,
180
     * symbolic links are supported only on Linux/Unix, and Windows Vista/2008 or greater.
181
     *
182
     * Moreover, some Web servers need to be properly configured so that the linked assets are accessible to Web users.
183
     * For example, for Apache Web server, the following configuration directive should be added for the Web folder:
184
     *
185
     * ```apache
186
     * Options FollowSymLinks
187
     * ```
188
     */
189
    private bool $linkAssets = false;
190
191
    /**
192
     * @var array the registered JS files.
193
     *
194
     * {@see registerJsFile()}
195
     */
196
    private array $jsFiles = [];
197
198
    private LoggerInterface $logger;
199
200 54
    public function __construct(Aliases $aliases, LoggerInterface $logger)
201
    {
202 54
        $this->aliases = $aliases;
203 54
        $this->logger = $logger;
204 54
        $this->publish = $this->getPublish();
205 54
    }
206
207 27
    public function getAliases(): Aliases
208
    {
209 27
        return $this->aliases;
210
    }
211
212 20
    public function getAssetMap(): array
213
    {
214 20
        return $this->assetMap;
215
    }
216
217 17
    public function getAppendTimestamp(): bool
218
    {
219 17
        return $this->appendTimestamp;
220
    }
221
222
    /**
223
     * Registers the asset manager being used by this view object.
224
     *
225
     * @return array the asset manager. Defaults to the "assetManager" application component.
226
     */
227 22
    public function getAssetBundles(): array
228
    {
229 22
        return $this->assetBundles;
230
    }
231
232 2
    public function getBasePath(): ?string
233
    {
234 2
        if (!empty($this->basePath)) {
235 1
            $this->basePath = $this->aliases->get($this->basePath);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->aliases->get($this->basePath) can also be of type boolean. However, the property $basePath is declared as type null|string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
236
        }
237
238 2
        return $this->basePath;
239
    }
240
241 1
    public function getBaseUrl(): ?string
242
    {
243 1
        if (!empty($this->baseUrl)) {
244
            $this->baseUrl = $this->aliases->get($this->baseUrl);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->aliases->get($this->baseUrl) can also be of type boolean. However, the property $baseUrl is declared as type null|string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
245
        }
246
247 1
        return $this->baseUrl;
248
    }
249
250
    /**
251
     * Returns the named asset bundle.
252
     *
253
     * This method will first look for the bundle in {@see bundles()}. If not found, it will treat `$name` as the class
254
     * of the asset bundle and create a new instance of it.
255
     *
256
     * @param string $name the class name of the asset bundle (without the leading backslash).
257
     *
258
     * @return AssetBundle the asset bundle instance
259
     *
260
     * @throws \InvalidArgumentException
261
     * @throws InvalidConfigException
262
     */
263 27
    public function getBundle(string $name): AssetBundle
264
    {
265 27
        if (!isset($this->bundles[$name])) {
266 25
            return $this->bundles[$name] = $this->publish->loadBundle($name, []);
267
        }
268
269 13
        if ($this->bundles[$name] instanceof AssetBundle) {
270
            return $this->bundles[$name];
271
        }
272
273 13
        if (\is_array($this->bundles[$name])) {
274 13
            return $this->bundles[$name] = $this->publish->loadBundle($name, $this->bundles[$name]);
275
        }
276
277
        if ($this->bundles[$name] === false) {
278
            return $this->loadDummyBundle($name);
279
        }
280
281
        throw new \InvalidArgumentException("Invalid asset bundle configuration: $name");
282
    }
283
284
    /**
285
     * Returns the asset converter.
286
     *
287
     * @return AssetConverterInterface the asset converter.
288
     */
289 1
    public function getConverter(): AssetConverterInterface
290
    {
291 1
        if (empty($this->converter)) {
292 1
            $this->converter = new AssetConverter($this->aliases, $this->logger);
293
        }
294
295 1
        return $this->converter;
296
    }
297
298 14
    public function getCssFiles(): array
299
    {
300 14
        return $this->cssFiles;
301
    }
302
303 7
    public function getDirMode(): int
304
    {
305 7
        return $this->dirMode;
306
    }
307
308 7
    public function getFileMode(): int
309
    {
310 7
        return $this->fileMode;
311
    }
312
313 7
    public function getForceCopy(): bool
314
    {
315 7
        return $this->forceCopy;
316
    }
317
318 23
    public function getJsFiles(): array
319
    {
320 23
        return $this->jsFiles;
321
    }
322
323 7
    public function getLinkAssets(): bool
324
    {
325 7
        return $this->linkAssets;
326
    }
327
328 7
    public function getHashCallback(): ?callable
329
    {
330 7
        return $this->hashCallback;
331
    }
332
333 54
    public function getPublish(): AssetPublisher
334
    {
335 54
        if (empty($this->publish)) {
336 54
            $this->publish = new AssetPublisher($this);
337
        }
338
339 54
        return $this->publish;
340
    }
341
342 2
    public function getPublishedPath(?string $sourcePath): ?string
343
    {
344 2
        return $this->publish->getPublishedPath($sourcePath);
0 ignored issues
show
Bug introduced by
It seems like $sourcePath can also be of type null; however, parameter $sourcePath of Yiisoft\Assets\AssetPublisher::getPublishedPath() 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

344
        return $this->publish->getPublishedPath(/** @scrutinizer ignore-type */ $sourcePath);
Loading history...
345
    }
346
347 2
    public function getPublishedUrl(?string $sourcePath): ?string
348
    {
349 2
        return $this->publish->getPublishedUrl($sourcePath);
0 ignored issues
show
Bug introduced by
It seems like $sourcePath can also be of type null; however, parameter $sourcePath of Yiisoft\Assets\AssetPublisher::getPublishedUrl() 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

349
        return $this->publish->getPublishedUrl(/** @scrutinizer ignore-type */ $sourcePath);
Loading history...
350
    }
351
352
    /**
353
     * Set appendTimestamp.
354
     *
355
     * @param bool $value
356
     *
357
     * @return void
358
     *
359
     * {@see appendTimestamp}
360
     */
361 21
    public function setAppendTimestamp(bool $value): void
362
    {
363 21
        $this->appendTimestamp = $value;
364 21
    }
365
366
    /**
367
     * Set assetMap.
368
     *
369
     * @param array $value
370
     *
371
     * @return void
372
     *
373
     * {@see assetMap}
374
     */
375 1
    public function setAssetMap(array $value): void
376
    {
377 1
        $this->assetMap = $value;
378 1
    }
379
380
    /**
381
     * Set basePath.
382
     *
383
     * @param string|null $value
384
     *
385
     * @return void
386
     *
387
     * {@see basePath}
388
     */
389 3
    public function setBasePath(?string $value): void
390
    {
391 3
        $this->basePath = $value;
392 3
    }
393
394
    /**
395
     * Set baseUrl.
396
     *
397
     * @param string|null $value
398
     *
399
     * @return void
400
     *
401
     * {@see baseUrl}
402
     */
403 1
    public function setBaseUrl(?string $value): void
404
    {
405 1
        $this->baseUrl = $value;
406 1
    }
407
408
409
    /**
410
     * Set bundles.
411
     *
412
     * @param array $value
413
     *
414
     * @return void
415
     *
416
     * {@see bundles}
417
     */
418 13
    public function setBundles(array $value): void
419
    {
420 13
        $this->bundles = $value;
421 13
    }
422
423
    /**
424
     * Sets the asset converter.
425
     *
426
     * @param AssetConverterInterface $value the asset converter. This can be eitheran object implementing the
427
     * {@see AssetConverterInterface}, or a configuration array that can be used
428
     * to create the asset converter object.
429
     */
430
    public function setConverter(AssetConverterInterface $value): void
431
    {
432
        $this->converter = $value;
433
    }
434
435
    /**
436
     * Set hashCallback.
437
     *
438
     * @param callable $value
439
     *
440
     * @return void
441
     *
442
     * {@see hashCallback}
443
     */
444 1
    public function setHashCallback(callable $value): void
445
    {
446 1
        $this->hashCallback = $value;
447 1
    }
448
449 25
    public function register(array $names, ?int $position = null): void
450
    {
451 25
        foreach ($names as $name) {
452 25
            $this->registerAssetBundle($name, $position);
453 20
            $this->registerFiles($name);
454
        }
455 17
    }
456
457
    /**
458
     * Registers a CSS file.
459
     *
460
     * This method should be used for simple registration of CSS files. If you want to use features of
461
     * {@see AssetManager} like appending timestamps to the URL and file publishing options, use {@see AssetBundle}
462
     * and {@see registerAssetBundle()} instead.
463
     *
464
     * @param string $url the CSS file to be registered.
465
     * @param array $options the HTML attributes for the link tag. Please refer to {@see \Yiisoft\Html\Html::cssFile()}
466
     * for the supported options. The following options are specially handled and are not treated as HTML
467
     * attributes:
468
     *
469
     *   - `depends`: array, specifies the names of the asset bundles that this CSS file depends on.
470
     *
471
     * @param string $key the key that identifies the CSS script file. If null, it will use $url as the key. If two CSS
472
     * files are registered with the same key, the latter will overwrite the former.
473
     *
474
     * @return void
475
     */
476 24
    public function registerCssFile(string $url, array $options = [], string $key = null): void
477
    {
478 24
        $key = $key ?: $url;
479
480 24
        $this->cssFiles[$key]['url'] = $url;
481 24
        $this->cssFiles[$key]['attributes'] = $options;
482 24
    }
483
484
    /**
485
     * Registers a JS file.
486
     *
487
     * This method should be used for simple registration of JS files. If you want to use features of
488
     * {@see AssetManager} like appending timestamps to the URL and file publishing options, use {@see AssetBundle}
489
     * and {@see registerAssetBundle()} instead.
490
     *
491
     * @param string $url the JS file to be registered.
492
     * @param array $options the HTML attributes for the script tag. The following options are specially handled and
493
     * are not treated as HTML attributes:
494
     *
495
     * - `depends`: array, specifies the names of the asset bundles that this JS file depends on.
496
     * - `position`: specifies where the JS script tag should be inserted in a page. The possible values are:
497
     *     * [[POS_HEAD]]: in the head section
498
     *     * [[POS_BEGIN]]: at the beginning of the body section
499
     *     * [[POS_END]]: at the end of the body section. This is the default value.
500
     *
501
     * Please refer to {@see \Yiisoft\Html\Html::jsFile()} for other supported options.
502
     *
503
     * @param string $key the key that identifies the JS script file. If null, it will use $url as the key. If two JS
504
     * files are registered with the same key at the same position, the latter will overwrite the former.
505
     * Note that position option takes precedence, thus files registered with the same key, but different
506
     * position option will not override each other.
507
     *
508
     * @return void
509
     */
510 29
    public function registerJsFile(string $url, array $options = [], string $key = null): void
511
    {
512 29
        $key = $key ?: $url;
513
514 29
        if (!\array_key_exists('position', $options)) {
515 24
            $options = array_merge(['position' => 3], $options);
516
        }
517
518 29
        $this->jsFiles[$key]['url'] = $url;
519 29
        $this->jsFiles[$key]['attributes'] = $options;
520 29
    }
521
522
    /**
523
     * Registers the named asset bundle.
524
     *
525
     * All dependent asset bundles will be registered.
526
     *
527
     * @param string $name the class name of the asset bundle (without the leading backslash)
528
     * @param int|null $position if set, this forces a minimum position for javascript files. This will adjust depending
529
     * assets javascript file position or fail if requirement can not be met. If this is null, asset
530
     * bundles position settings will not be changed.
531
     *
532
     * {@see registerJsFile()} for more details on javascript position.
533
     *
534
     * @return AssetBundle the registered asset bundle instance
535
     * @throws InvalidConfigException
536
     *
537
     * @throws \RuntimeException if the asset bundle does not exist or a circular dependency is detected
538
     */
539 25
    private function registerAssetBundle(string $name, ?int $position = null): AssetBundle
540
    {
541 25
        if (!isset($this->assetBundles[$name])) {
542 25
            $bundle = $this->getBundle($name);
543
544 23
            $this->assetBundles[$name] = false;
545
546
            // register dependencies
547 23
            $pos = $bundle->jsOptions['position'] ?? null;
548
549 23
            foreach ($bundle->depends as $dep) {
550 19
                $this->registerAssetBundle($dep, $pos);
551
            }
552
553 22
            $this->assetBundles[$name] = $bundle;
554 9
        } elseif ($this->assetBundles[$name] === false) {
555 1
            throw new \RuntimeException("A circular dependency is detected for bundle '$name'.");
556
        } else {
557 8
            $bundle = $this->assetBundles[$name];
558
        }
559
560 22
        if ($position !== null) {
561 10
            $pos = $bundle->jsOptions['position'] ?? null;
562
563 10
            if ($pos === null) {
564 10
                $bundle->jsOptions['position'] = $pos = $position;
565 4
            } elseif ($pos > $position) {
566 4
                throw new \RuntimeException(
567 4
                    "An asset bundle that depends on '$name' has a higher javascript file " .
568 4
                    "position configured than '$name'."
569
                );
570
            }
571
572
            // update position for all dependencies
573 10
            foreach ($bundle->depends as $dep) {
574 6
                $this->registerAssetBundle($dep, $pos);
575
            }
576
        }
577 22
        return $bundle;
578
    }
579
580
    /**
581
     * Loads dummy bundle by name.
582
     *
583
     * @param string $name AssetBunle name class.
584
     *
585
     * @return AssetBundle
586
     * @throws InvalidConfigException
587
     */
588
    private function loadDummyBundle(string $name): AssetBundle
589
    {
590
        if (!isset($this->dummyBundles[$name])) {
591
            $this->dummyBundles[$name] = $this->publish->loadBundle($name, [
592
                'sourcePath' => null,
593
                'js' => [],
594
                'css' => [],
595
                'depends' => [],
596
            ]);
597
        }
598
599
        return $this->dummyBundles[$name];
600
    }
601
602 20
    private function registerFiles(string $name): void
603
    {
604 20
        if (!isset($this->assetBundles[$name])) {
605
            return;
606
        }
607
608 20
        $bundle = $this->assetBundles[$name];
609
610 20
        foreach ($bundle->depends as $dep) {
611 16
            $this->registerFiles($dep);
612
        }
613
614 20
        $this->publish->registerAssetFiles($bundle);
615 19
    }
616
}
617