Passed
Push — master ( a1416a...fd5900 )
by Alexander
01:40
created

AssetManager::getForceCopy()   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 0
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
    /**
38
     * @var Aliases
39
     */
40
    private Aliases $aliases;
41
42
    /**
43
     * @var array AssetBundle[] list of the registered asset bundles. The keys are the bundle names, and the values
44
     * are the registered {@see AssetBundle} objects.
45
     *
46
     * {@see registerAssetBundle()}
47
     */
48
    private array $assetBundles = [];
49
50
    /**
51
     * @var bool whether to append a timestamp to the URL of every published asset. When this is true, the URL of a
52
     * published asset may look like `/path/to/asset?v=timestamp`, where `timestamp` is the last modification time of
53
     * the published asset file. You normally would want to set this property to true when you have enabled HTTP caching
54
     * for assets, because it allows you to bust caching when the assets are updated.
55
     */
56
    private bool $appendTimestamp = false;
57
58
    /**
59
     * @var array mapping from source asset files (keys) to target asset files (values).
60
     *
61
     * This property is provided to support fixing incorrect asset file paths in some asset bundles. When an asset
62
     * bundle is registered with a view, each relative asset file in its {@see AssetBundle::css|css} and
63
     * {@see AssetBundle::js|js} arrays will be examined against this map. If any of the keys is found to be the last
64
     * part of an asset file (which is prefixed with {@see AssetBundle::sourcePath} if available), the corresponding
65
     * value will replace the asset and be registered with the view. For example, an asset file `my/path/to/jquery.js`
66
     * matches a key `jquery.js`.
67
     *
68
     * Note that the target asset files should be absolute URLs, domain relative URLs (starting from '/') or paths
69
     * relative to {@see baseUrl} and {@see basePath}.
70
     *
71
     * In the following example, any assets ending with `jquery.min.js` will be replaced with `jquery/dist/jquery.js`
72
     * which is relative to {@see baseUrl} and {@see basePath}.
73
     *
74
     * ```php
75
     * [
76
     *     'jquery.min.js' => 'jquery/dist/jquery.js',
77
     * ]
78
     * ```
79
     */
80
    private array $assetMap = [];
81
82
    /**
83
     * @var AssetPublisher published assets
84
     */
85
    private AssetPublisher $publisher;
86
87
    /**
88
     * @var string|null the root directory storing the published asset files.
89
     */
90
    private ?string $basePath = null;
91
92
    /**
93
     * @var string|null the base URL through which the published asset files can be accessed.
94
     */
95
    private ?string $baseUrl = null;
96
97
    /**
98
     * @var array list of asset bundle configurations. This property is provided to customize asset bundles.
99
     * When a bundle is being loaded by {@see getBundle()}, if it has a corresponding configuration specified here, the
100
     * configuration will be applied to the bundle.
101
     *
102
     * The array keys are the asset bundle names, which typically are asset bundle class names without leading
103
     * backslash. The array values are the corresponding configurations. If a value is false, it means the corresponding
104
     * asset bundle is disabled and {@see getBundle()} should return null.
105
     *
106
     * If this property is false, it means the whole asset bundle feature is disabled and {@see {getBundle()} will
107
     * always return null.
108
     */
109
    private array $bundles = [];
110
111
    /**
112
     * AssetConverter component.
113
     *
114
     * @var AssetConverterInterface $converter
115
     */
116
    private AssetConverterInterface $converter;
117
118
    /**
119
     * @var array the registered CSS files.
120
     *
121
     * {@see registerCssFile()}
122
     */
123
    private array $cssFiles = [];
124
125
    /**
126
     * @var int the permission to be set for newly generated asset directories. This value will be used by PHP chmod()
127
     * function. No umask will be applied. Defaults to 0775, meaning the directory is read-writable by owner
128
     * and group, but read-only for other users.
129
     */
130
    private int $dirMode = 0775;
131
132
    /**
133
     * @var array $dummyBundles
134
     */
135
    private array $dummyBundles;
136
137
    /**
138
     * @var int the permission to be set for newly published asset files. This value will be used by PHP chmod()
139
     * function. No umask will be applied. If not set, the permission will be determined by the current
140
     * environment.
141
     */
142
    private int $fileMode = 0755;
143
144
    /**
145
     * @var bool whether the directory being published should be copied even if it is found in the target directory.
146
     * This option is used only when publishing a directory. You may want to set this to be `true` during the
147
     * development stage to make sure the published directory is always up-to-date. Do not set this to true
148
     * on production servers as it will significantly degrade the performance.
149
     */
150
    private bool $forceCopy = false;
151
152
    /**
153
     * @var callable a callback that will be called to produce hash for asset directory generation. The signature of the
154
     * callback should be as follows:
155
     *
156
     * ```
157
     * function ($path)
158
     * ```
159
     *
160
     * where `$path` is the asset path. Note that the `$path` can be either directory where the asset files reside or a
161
     * single file. For a CSS file that uses relative path in `url()`, the hash implementation should use the directory
162
     * path of the file instead of the file path to include the relative asset files in the copying.
163
     *
164
     * If this is not set, the asset manager will use the default CRC32 and filemtime in the `hash` method.
165
     *
166
     * Example of an implementation using MD4 hash:
167
     *
168
     * ```php
169
     * function ($path) {
170
     *     return hash('md4', $path);
171
     * }
172
     * ```
173
     */
174
    private $hashCallback;
175
176
    /**
177
     * @var bool whether to use symbolic link to publish asset files. Defaults to false, meaning asset files are copied
178
     * to {@see basePath}. Using symbolic links has the benefit that the published assets will always be
179
     * consistent with the source assets and there is no copy operation required. This is especially useful
180
     * during development.
181
     *
182
     * However, there are special requirements for hosting environments in order to use symbolic links. In particular,
183
     * symbolic links are supported only on Linux/Unix, and Windows Vista/2008 or greater.
184
     *
185
     * Moreover, some Web servers need to be properly configured so that the linked assets are accessible to Web users.
186
     * For example, for Apache Web server, the following configuration directive should be added for the Web folder:
187
     *
188
     * ```apache
189
     * Options FollowSymLinks
190
     * ```
191
     */
192
    private bool $linkAssets = false;
193
194
    /**
195
     * @var array the registered JS files.
196
     *
197
     * {@see registerJsFile()}
198
     */
199
    private array $jsFiles = [];
200
201
    private LoggerInterface $logger;
202
203 54
    public function __construct(Aliases $aliases, LoggerInterface $logger)
204
    {
205 54
        $this->aliases = $aliases;
206 54
        $this->logger = $logger;
207 54
    }
208
209 27
    public function getAliases(): Aliases
210
    {
211 27
        return $this->aliases;
212
    }
213
214 20
    public function getAssetMap(): array
215
    {
216 20
        return $this->assetMap;
217
    }
218
219 17
    public function getAppendTimestamp(): bool
220
    {
221 17
        return $this->appendTimestamp;
222
    }
223
224
    /**
225
     * Registers the asset manager being used by this view object.
226
     *
227
     * @return array the asset manager. Defaults to the "assetManager" application component.
228
     */
229 22
    public function getAssetBundles(): array
230
    {
231 22
        return $this->assetBundles;
232
    }
233
234 2
    public function getBasePath(): ?string
235
    {
236 2
        if (!empty($this->basePath)) {
237 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...
238
        }
239
240 2
        return $this->basePath;
241
    }
242
243 1
    public function getBaseUrl(): ?string
244
    {
245 1
        if (!empty($this->baseUrl)) {
246
            $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...
247
        }
248
249 1
        return $this->baseUrl;
250
    }
251
252
    /**
253
     * Returns the named asset bundle.
254
     *
255
     * This method will first look for the bundle in {@see bundles()}. If not found, it will treat `$name` as the class
256
     * of the asset bundle and create a new instance of it.
257
     *
258
     * @param string $name the class name of the asset bundle (without the leading backslash).
259
     *
260
     * @return AssetBundle the asset bundle instance
261
     *
262
     * @throws \InvalidArgumentException
263
     * @throws InvalidConfigException
264
     */
265 27
    public function getBundle(string $name): AssetBundle
266
    {
267 27
        if (!isset($this->bundles[$name])) {
268 25
            return $this->bundles[$name] = $this->publisher->loadBundle($this, $name, []);
269
        }
270
271 13
        if ($this->bundles[$name] instanceof AssetBundle) {
272
            return $this->bundles[$name];
273
        }
274
275 13
        if (\is_array($this->bundles[$name])) {
276 13
            return $this->bundles[$name] = $this->publisher->loadBundle($this, $name, $this->bundles[$name]);
277
        }
278
279
        if ($this->bundles[$name] === false) {
280
            return $this->loadDummyBundle($name);
281
        }
282
283
        throw new \InvalidArgumentException("Invalid asset bundle configuration: $name");
284
    }
285
286
    /**
287
     * Returns the asset converter.
288
     *
289
     * @return AssetConverterInterface the asset converter.
290
     */
291 1
    public function getConverter(): AssetConverterInterface
292
    {
293 1
        return $this->converter;
294
    }
295
296 14
    public function getCssFiles(): array
297
    {
298 14
        return $this->cssFiles;
299
    }
300
301 7
    public function getDirMode(): int
302
    {
303 7
        return $this->dirMode;
304
    }
305
306 7
    public function getFileMode(): int
307
    {
308 7
        return $this->fileMode;
309
    }
310
311 7
    public function getForceCopy(): bool
312
    {
313 7
        return $this->forceCopy;
314
    }
315
316 23
    public function getJsFiles(): array
317
    {
318 23
        return $this->jsFiles;
319
    }
320
321 7
    public function getLinkAssets(): bool
322
    {
323 7
        return $this->linkAssets;
324
    }
325
326 7
    public function getHashCallback(): ?callable
327
    {
328 7
        return $this->hashCallback;
329
    }
330
331 2
    public function getPublish(): AssetPublisher
332
    {
333 2
        return $this->publisher;
334
    }
335
336 2
    public function getPublishedPath(?string $sourcePath): ?string
337
    {
338 2
        return $this->publisher->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

338
        return $this->publisher->getPublishedPath(/** @scrutinizer ignore-type */ $sourcePath);
Loading history...
339
    }
340
341 2
    public function getPublishedUrl(?string $sourcePath): ?string
342
    {
343 2
        return $this->publisher->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

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