Completed
Pull Request — master (#50)
by Alexander
01:46
created

AssetManager::setBundles()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
ccs 0
cts 2
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 2
1
<?php
2
declare(strict_types=1);
3
4
namespace Yiisoft\Asset;
5
6
use Psr\Log\LoggerInterface;
7
use Yiisoft\Aliases\Aliases;
8
use Yiisoft\Files\FileHelper;
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
 * ```
20
 */
21
class AssetManager
22
{
23
    /**
24
     * @var callable a PHP callback that is called after a sub-directory or file is successfully copied. This option is
25
     * used only when publishing a directory. The signature of the callback is the same as for
26
     * {@see {beforeCopy}.
27
     * This is passed as a parameter `afterCopy` to {@see \Yiisoft\Files\FileHelper::copyDirectory()}.
28
     */
29
    private $afterCopy;
30
31
    /**
32
     * Aliases component.
33
     *
34
     * @var Aliases $aliase
35
     */
36
    private $aliases;
37
38
    /**
39
     * directory path node_modules.
40
     *
41
     * @var array $alternatives
42
     */
43
    private $alternatives = [
44
        '@npm' => '@root/node_modules',
45
    ];
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
50
     * time of the published asset file. You normally would want to set this property to true when you have
51
     * enabled HTTP caching for assets, because it allows you to bust caching when the assets are updated.
52
     */
53
    private $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
     * You may also use aliases while specifying map value, for example:
78
     *
79
     * ```php
80
     * [
81
     *     'jquery.min.js' => '@web/js/jquery/jquery.js',
82
     * ]
83
     * ```
84
     */
85
    private $assetMap = [];
86
87
    /**
88
     * @var string the root directory storing the published asset files.
89
     */
90
    private $basePath = '@public/assets';
91
92
    /**
93
     * @var string the base URL through which the published asset files can be accessed.
94
     */
95
    private $baseUrl = '@web/assets';
96
97
    /**
98
     * @var callable a PHP callback that is called before copying each sub-directory or file. This option is used only
99
     * when publishing a directory. If the callback returns false, the copy operation for the
100
     * sub-directory or file will be cancelled.
101
     *
102
     * The signature of the callback should be: `function ($from, $to)`, where `$from` is the sub-directory or file to
103
     * be copied from, while `$to` is the copy target.
104
     *
105
     * This is passed as a parameter `beforeCopy` to {@see \Yiisoft\Files\FileHelper::copyDirectory()}.
106
     */
107
    private $beforeCopy;
108
109
    /**
110
     * @var array|bool list of asset bundle configurations. This property is provided to customize asset bundles.
111
     * When a bundle is being loaded by {@see getBundle()}, if it has a corresponding configuration
112
     * specified here, the configuration will be applied to the bundle.
113
     *
114
     * The array keys are the asset bundle names, which typically are asset bundle class names without leading
115
     * backslash. The array values are the corresponding configurations. If a value is false, it means the corresponding
116
     * asset bundle is disabled and {@see getBundle()} should return null.
117
     *
118
     * If this property is false, it means the whole asset bundle feature is disabled and {@see {getBundle()} will
119
     * always return null.
120
     *
121
     * The following example shows how to disable the bootstrap css file used by Bootstrap widgets (because you want to
122
     * use your own styles):
123
     *
124
     * ```php
125
     * [
126
     *     \Yiisoft\Bootstrap4\BootstrapAsset::class => [
127
     *         'css' => [],
128
     *     ],
129
     * ]
130
     * ```
131
     */
132
    private $bundles = [];
133
134
    /**
135
     * AssetConverter component.
136
     *
137
     * @var AssetConverterInterface $converter
138
     */
139
    private $converter;
140
141
    /**
142
     * @var int the permission to be set for newly generated asset directories. This value will be used by PHP chmod()
143
     * function. No umask will be applied. Defaults to 0775, meaning the directory is read-writable by owner
144
     * and group, but read-only for other users.
145
     */
146
    private $dirMode = 0775;
147
148
    /**
149
     * @var AssetBundle $dummyBundles
150
     */
151
    private $dummyBundles;
152
153
    /**
154
     * @var int the permission to be set for newly published asset files. This value will be used by PHP chmod()
155
     * function. No umask will be applied. If not set, the permission will be determined by the current
156
     * environment.
157
     */
158
    private $fileMode;
159
160
    /**
161
     * @var bool whether the directory being published should be copied even if it is found in the target directory.
162
     * This option is used only when publishing a directory. You may want to set this to be `true` during the
163
     * development stage to make sure the published directory is always up-to-date. Do not set this to true
164
     * on production servers as it will significantly degrade the performance.
165
     */
166
    private $forceCopy = false;
167
168
    /**
169
     * @var callable a callback that will be called to produce hash for asset directory generation. The signature of the
170
     * callback should be as follows:
171
     *
172
     * ```
173
     * function ($path)
174
     * ```
175
     *
176
     * where `$path` is the asset path. Note that the `$path` can be either directory where the asset files reside or a
177
     * single file. For a CSS file that uses relative path in `url()`, the hash implementation should use the directory
178
     * path of the file instead of the file path to include the relative asset files in the copying.
179
     *
180
     * If this is not set, the asset manager will use the default CRC32 and filemtime in the `hash` method.
181
     *
182
     * Example of an implementation using MD4 hash:
183
     *
184
     * ```php
185
     * function ($path) {
186
     *     return hash('md4', $path);
187
     * }
188
     * ```
189
     */
190
    private $hashCallback;
191
192
    /**
193
     * @var bool whether to use symbolic link to publish asset files. Defaults to false, meaning asset files are copied
194
     * to {@see basePath}. Using symbolic links has the benefit that the published assets will always be
195
     * consistent with the source assets and there is no copy operation required. This is especially useful
196
     * during development.
197
     *
198
     * However, there are special requirements for hosting environments in order to use symbolic links. In particular,
199
     * symbolic links are supported only on Linux/Unix, and Windows Vista/2008 or greater.
200
     *
201
     * Moreover, some Web servers need to be properly configured so that the linked assets are accessible to Web users.
202
     * For example, for Apache Web server, the following configuration directive should be added for the Web folder:
203
     *
204
     * ```apache
205
     * Options FollowSymLinks
206
     * ```
207
     */
208
    private $linkAssets = true;
209
210
    /**
211
     * @var LoggerInterface $logger
212
     */
213
    private $logger;
214
215
    /**
216
     * @var array published assets
217
     */
218
    private $published = [];
219
220
    /**
221
     * @var string $realBasePath
222
     */
223
    private $realBasePath;
224
225
    /**
226
     * AssetManager constructor.
227
     *
228
     * @param Aliases $aliases
229
     */
230 51
    public function __construct(Aliases $aliases, LoggerInterface $logger)
231
    {
232 51
        $this->aliases = $aliases;
233 51
        $this->logger = $logger;
234 51
        $this->setDefaultPaths();
235
    }
236
237
    /**
238
     * Returns the actual URL for the specified asset.
239
     * The actual URL is obtained by prepending either {@see AssetBundle::$baseUrl} or {@see AssetManager::$baseUrl} to
240
     * the given asset path.
241
     *
242
     * @param AssetBundle $bundle the asset bundle which the asset file belongs to
243
     * @param string $asset the asset path. This should be one of the assets listed in {@see AssetBundle::$js} or
244
     * {@see AssetBundle::$css}.
245
     *
246
     * @return string the actual URL for the specified asset.
247
     */
248 8
    public function getAssetUrl(AssetBundle $bundle, string $asset): string
249
    {
250 8
        if (($actualAsset = $this->resolveAsset($bundle, $asset)) !== false) {
251
            if (strncmp((string)$actualAsset, '@web/', 5) === 0) {
252
                $asset = substr((string)$actualAsset, 5);
253
                $basePath = $this->aliases->get('@public');
254
                $baseUrl = $this->aliases->get('@web');
255
            } else {
256
                $asset = $this->aliases->get($actualAsset);
0 ignored issues
show
Bug introduced by
It seems like $actualAsset can also be of type true; however, parameter $alias of Yiisoft\Aliases\Aliases::get() 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

256
                $asset = $this->aliases->get(/** @scrutinizer ignore-type */ $actualAsset);
Loading history...
257
                $basePath = $this->getRealBasePath();
258
                $baseUrl = $this->baseUrl;
259
            }
260
        } else {
261 8
            $basePath = $this->aliases->get($bundle->basePath);
262 8
            $baseUrl = $this->aliases->get($bundle->baseUrl);
263
        }
264
265 8
        if (!$this->isRelative($asset) || strncmp($asset, '/', 1) === 0) {
266
            return $asset;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $asset could return the type boolean which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
267
        }
268
269 8
        if ($this->appendTimestamp && ($timestamp = @filemtime("$basePath/$asset")) > 0) {
270
            return "$baseUrl/$asset?v=$timestamp";
271
        }
272
273 8
        return "$baseUrl/$asset";
274
    }
275
276
    /**
277
     * Returns the actual file path for the specified asset.
278
     *
279
     * @param AssetBundle $bundle the asset bundle which the asset file belongs to
280
     * @param string $asset the asset path. This should be one of the assets listed in {@see AssetBundle::$js} or
281
     * {@see AssetBundle::$css}.
282
     *
283
     * @return false|string the actual file path, or `false` if the asset is specified as an absolute URL
284
     */
285
    public function getAssetPath(AssetBundle $bundle, string $asset)
286
    {
287
        if (($actualAsset = $this->resolveAsset($bundle, $asset)) !== false) {
288
            return $this->isRelative((string)$actualAsset) ? $this->getRealBasePath() . '/' . $actualAsset : false;
0 ignored issues
show
Bug introduced by
Are you sure $actualAsset of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

288
            return $this->isRelative((string)$actualAsset) ? $this->getRealBasePath() . '/' . /** @scrutinizer ignore-type */ $actualAsset : false;
Loading history...
289
        }
290
291
        return $this->isRelative($asset) ? $bundle->basePath . '/' . $asset : false;
292
    }
293
294
    /**
295
     * Returns the named asset bundle.
296
     *
297
     * This method will first look for the bundle in {@see bundles()}. If not found, it will treat `$name` as the class
298
     * of the asset bundle and create a new instance of it.
299
     *
300
     * @param string $name the class name of the asset bundle (without the leading backslash)
301
     * @param bool $publish whether to publish the asset files in the asset bundle before it is returned. If you set
302
     * this false, you must manually call `AssetBundle::publish()` to publish the asset files.
303
     *
304
     * @return AssetBundle the asset bundle instance
305
     *
306
     * @throws \InvalidArgumentException
307
     */
308 12
    public function getBundle(string $name, bool $publish = true): AssetBundle
309
    {
310 12
        if ($this->bundles === false) {
311
            return $this->loadDummyBundle($name);
312
        }
313
314 12
        if (!isset($this->bundles[$name])) {
315 12
            return $this->bundles[$name] = $this->loadBundle($name, [], $publish);
316
        }
317
318
        if ($this->bundles[$name] instanceof AssetBundle) {
319
            return $this->bundles[$name];
320
        }
321
322
        if (is_array($this->bundles[$name])) {
323
            return $this->bundles[$name] = $this->loadBundle($name, $this->bundles[$name], $publish);
324
        }
325
326
        if ($this->bundles[$name] === false) {
327
            return $this->loadDummyBundle($name);
328
        }
329
330
        throw new \InvalidArgumentException("Invalid asset bundle configuration: $name");
331
    }
332
333
    /**
334
     * Returns the asset converter.
335
     *
336
     * @return AssetConverterInterface the asset converter.
337
     */
338 13
    public function getConverter(): AssetConverterInterface
339
    {
340 13
        if ($this->converter === null) {
341 13
            $this->converter = new AssetConverter($this->aliases, $this->logger);
342 11
        } elseif (is_array($this->converter) || is_string($this->converter)) {
0 ignored issues
show
introduced by
The condition is_string($this->converter) is always false.
Loading history...
343
            if (is_array($this->converter) && !isset($this->converter['__class'])) {
344
                $this->converter['__class'] = AssetConverter::class;
345
            }
346
            $this->converter = new $this->converter($this->aliases, $this->logger);
347
        }
348
349 13
        return $this->converter;
350
    }
351
352
    /**
353
     * Returns the published path of a file path.
354
     *
355
     * This method does not perform any publishing. It merely tells you if the file or directory is published, where it
356
     * will go.
357
     *
358
     * @param string $path directory or file path being published
359
     *
360
     * @return string|null string the published file path. Null if the file or directory does not exist
361
     */
362
    public function getPublishedPath(string $path): ?string
363
    {
364
        $path = $this->aliases->get($path);
365
366
        if (isset($this->published[$path])) {
367
            return $this->published[$path][0];
368
        }
369
        if (($path = realpath($path)) !== false) {
370
            return $this->getRealBasePath() . DIRECTORY_SEPARATOR . $this->hash($path) . (is_file($path) ?
371
                    DIRECTORY_SEPARATOR . basename($path) : '');
372
        }
373
374
        return null;
375
    }
376
377
    /**
378
     * Returns the URL of a published file path.
379
     *
380
     * This method does not perform any publishing. It merely tells you if the file path is published, what the URL will
381
     * be to access it.
382
     *
383
     * @param string $path directory or file path being published
384
     *
385
     * @return string|null string the published URL for the file or directory. Null if the file or directory does not
386
     *                     exist.
387
     */
388
    public function getPublishedUrl(string $path): ?string
389
    {
390
        if (isset($this->published[$path])) {
391
            return $this->published[$path][1];
392
        }
393
        if (($path = realpath($path)) !== false) {
394
            return $this->baseUrl . '/' . $this->hash($path) . (is_file($path) ? '/' . basename($path) : '');
395
        }
396
397
        return null;
398
    }
399
400
    /**
401
     * Get RealBasePath.
402
     *
403
     * @return bool|string
404
     */
405 2
    public function getRealBasePath(): string
406
    {
407 2
        if ($this->realBasePath === null) {
408 2
            $this->realBasePath = (string)$this->prepareBasePath($this->basePath);
409
        }
410
411 2
        return $this->realBasePath;
412
    }
413
414
    /**
415
     * prepareBasePath
416
     *
417
     * @param string $basePath
418
     *
419
     * @return string|bool
420
     * @throws \InvalidArgumentException
421
     *
422
     */
423 2
    public function prepareBasePath(string $basePath)
424
    {
425 2
        $basePath = $this->aliases->get($basePath);
426
427 2
        if (!is_dir($basePath)) {
428
            throw new \InvalidArgumentException("The directory does not exist: {$basePath}");
429
        }
430
431 2
        if (!is_writable($basePath)) {
432
            throw new \InvalidArgumentException("The directory is not writable by the Web process: {$basePath}");
433
        }
434
435 2
        return realpath($basePath);
436
    }
437
438
    /**
439
     * Publishes a file or a directory.
440
     *
441
     * This method will copy the specified file or directory to {@see basePath} so that it can be accessed via the Web
442
     * server.
443
     *
444
     * If the asset is a file, its file modification time will be checked to avoid unnecessary file copying.
445
     *
446
     * If the asset is a directory, all files and subdirectories under it will be published recursively. Note, in case
447
     * $forceCopy is false the method only checks the existence of the target directory to avoid repetitive copying
448
     * (which is very expensive).
449
     *
450
     * By default, when publishing a directory, subdirectories and files whose name starts with a dot "." will NOT be
451
     * published. If you want to change this behavior, you may specify the "beforeCopy" option as explained in the
452
     * `$options` parameter.
453
     *
454
     * Note: On rare scenario, a race condition can develop that will lead to a  one-time-manifestation of a
455
     * non-critical problem in the creation of the directory that holds the published assets. This problem can be
456
     * avoided altogether by 'requesting' in advance all the resources that are supposed to trigger a 'publish()' call,
457
     * and doing that in the application deployment phase, before system goes live. See more in the following
458
     * discussion: http://code.google.com/p/yii/issues/detail?id=2579
459
     *
460
     * @param string $path the asset (file or directory) to be published
461
     * @param array $options the options to be applied when publishing a directory. The following options are
462
     * supported:
463
     *
464
     * - only: array, list of patterns that the file paths should match if they want to be copied.
465
     * - except: array, list of patterns that the files or directories should match if they want to be excluded from
466
     *   being copied.
467
     * - caseSensitive: boolean, whether patterns specified at "only" or "except" should be case sensitive. Defaults to
468
     *   true.
469
     * - beforeCopy: callback, a PHP callback that is called before copying each sub-directory or file.
470
     *   This overrides {@see beforeCopy} if set.
471
     * - afterCopy: callback, a PHP callback that is called after a sub-directory or file is successfully copied. This
472
     *   overrides {@seee afterCopy} if set.
473
     * - forceCopy: boolean, whether the directory being published should be copied even if it is found in the target
474
     *   directory. This option is used only when publishing a directory. This overrides {@see forceCopy} if set.
475
     *
476
     * @return array the path (directory or file path) and the URL that the asset is published as.
477
     * @throws \InvalidArgumentException if the asset to be published does not exist.
478
     *
479
     */
480 2
    public function publish(string $path, array $options = []): array
481
    {
482 2
        $path = $this->aliases->get($path);
483
484 2
        if (isset($this->published[$path])) {
485
            return $this->published[$path];
486
        }
487
488 2
        if (!is_string($path) || ($src = realpath($path)) === false) {
489
            throw new \InvalidArgumentException("The file or directory to be published does not exist: $path");
490
        }
491
492 2
        if (is_file($src)) {
493
            return $this->published[$path] = $this->publishFile($src);
494
        }
495
496 2
        return $this->published[$path] = $this->publishDirectory($src, $options);
497
    }
498
499
    /**
500
     * Set afterCopy.
501
     *
502
     * @param callable $value
503
     *
504
     * @return void
505
     *
506
     * {@see afterCopy}
507
     */
508
    public function setAfterCopy(callable $value): void
509
    {
510
        $this->afterCopy = $value;
511
    }
512
513
    /**
514
     * Set alternatives.
515
     *
516
     * @param array $value
517
     *
518
     * @return void
519
     *
520
     * {@see alternatives}
521
     */
522
    public function setAlternatives(array $value): void
523
    {
524
        $this->alternatives = $value;
525
        $this->setAlternativesAlias();
526
    }
527
528
    /**
529
     * Set appendTimestamp.
530
     *
531
     * @param bool $value
532
     *
533
     * @return void
534
     *
535
     * {@see appendTimestamp}
536
     */
537 20
    public function setAppendTimestamp(bool $value): void
538
    {
539 20
        $this->appendTimestamp = $value;
540
    }
541
542
    /**
543
     * Set assetMap.
544
     *
545
     * @param array $value
546
     *
547
     * @return void
548
     *
549
     * {@see assetMap}
550
     */
551
    public function setAssetMap(array $value): void
552
    {
553
        $this->assetMap = $value;
554
    }
555
556
    /**
557
     * Set basePath.
558
     *
559
     * @param string $value
560
     *
561
     * @return void
562
     *
563
     * {@see basePath}
564
     */
565
    public function setBasePath(string $value): void
566
    {
567
        $this->basePath = $value;
568
    }
569
570
    /**
571
     * Set baseUrl.
572
     *
573
     * @param string $value
574
     *
575
     * @return void
576
     *
577
     * {@see baseUrl}
578
     */
579
    public function setBaseUrl(string $value): void
580
    {
581
        $this->baseUrl = $value;
582
    }
583
584
    /**
585
     * Set beforeCopy.
586
     *
587
     * @param callable $value
588
     *
589
     * @return void
590
     *
591
     * {@see beforeCopy}
592
     */
593
    public function setBeforeCopy(callable $value): void
594
    {
595
        $this->beforeCopy = $value;
596
    }
597
598
    /**
599
     * Set bundles.
600
     *
601
     * @param array $value
602
     *
603
     * @return void
604
     *
605
     * {@see beforeCopy}
606
     */
607
    public function setBundles(array $value): void
608
    {
609
        $this->bundles = $value;
610
    }
611
612
    /**
613
     * Sets the asset converter.
614
     *
615
     * @param AssetConverterInterface $value the asset converter. This can be eitheran object implementing the
616
     * {@see AssetConverterInterface}, or a configuration array that can be used
617
     * to create the asset converter object.
618
     */
619
    public function setConverter(AssetConverterInterface $value): void
620
    {
621
        $this->converter = $value;
622
    }
623
624
    /**
625
     * Set dirMode.
626
     *
627
     * @param int $value
628
     *
629
     * @return void
630
     *
631
     * {@see dirMode}
632
     */
633
    public function setDirMode(int $value): void
634
    {
635
        $this->dirMode = $value;
636
    }
637
638
    /**
639
     * Set fileMode.
640
     *
641
     * @param int $value
642
     *
643
     * @return void
644
     *
645
     * {@see fileMode}
646
     */
647
    public function setFileMode(int $value): void
648
    {
649
        $this->fileMode = $value;
650
    }
651
652
    /**
653
     * Set hashCallback.
654
     *
655
     * @param callable $value
656
     *
657
     * @return void
658
     *
659
     * {@see hashCallback}
660
     */
661 1
    public function setHashCallback(callable $value): void
662
    {
663 1
        $this->hashCallback = $value;
664
    }
665
666
    /**
667
     * Set linkAssets.
668
     *
669
     * @param bool $value
670
     *
671
     * @return void
672
     *
673
     * {@see linkAssets}
674
     */
675 2
    public function setLinkAssets(bool $value): void
676
    {
677 2
        $this->linkAssets = $value;
678
    }
679
680
    /**
681
     * Returns a value indicating whether a URL is relative.
682
     * A relative URL does not have host info part.
683
     * @param string $url the URL to be checked
684
     * @return bool whether the URL is relative
685
     */
686 8
    protected function isRelative($url)
687
    {
688 8
        return strncmp($url, '//', 2) && strpos($url, '://') === false;
689
    }
690
691
    /**
692
     * Generate a CRC32 hash for the directory path. Collisions are higher than MD5 but generates a much smaller hash
693
     * string.
694
     *
695
     * @param string $path string to be hashed.
696
     *
697
     * @return string hashed string.
698
     */
699 2
    protected function hash(string $path): string
700
    {
701 2
        if (is_callable($this->hashCallback)) {
702 1
            return call_user_func($this->hashCallback, $path);
703
        }
704 1
        $path = (is_file($path) ? dirname($path) : $path) . filemtime($path);
705
706 1
        return sprintf('%x', crc32($path . '|' . $this->linkAssets));
707
    }
708
709
    /**
710
     * Loads asset bundle class by name.
711
     *
712
     * @param string $name bundle name
713
     * @param array $config bundle object configuration
714
     * @param bool $publish if bundle should be published
715
     *
716
     * @return AssetBundle
717
     */
718 12
    protected function loadBundle(string $name, array $config = [], bool $publish = true): AssetBundle
0 ignored issues
show
Unused Code introduced by
The parameter $config is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

718
    protected function loadBundle(string $name, /** @scrutinizer ignore-unused */ array $config = [], bool $publish = true): AssetBundle

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
719
    {
720
        /** @var AssetBundle $bundle */
721 12
        $bundle = new $name();
722
723 12
        if ($publish) {
724 12
            $bundle->publish($this);
725
        }
726
727 12
        return $bundle;
728
    }
729
730
    /**
731
     * Loads dummy bundle by name.
732
     *
733
     * @param string $name
734
     *
735
     * @return AssetBundle
736
     */
737
    protected function loadDummyBundle(string $name): AssetBundle
738
    {
739
        if (!isset($this->dummyBundles[$name])) {
740
            $this->dummyBundles[$name] = $this->loadBundle($name, [
741
                'sourcePath' => null,
742
                'js' => [],
743
                'css' => [],
744
                'depends' => [],
745
            ]);
746
        }
747
748
        return $this->dummyBundles[$name];
749
    }
750
751
    /**
752
     * Publishes a file.
753
     *
754
     * @param string $src the asset file to be published
755
     *
756
     * @return array the path and the URL that the asset is published as.
757
     * @throws \Exception if the asset to be published does not exist.
758
     *
759
     */
760
    protected function publishFile(string $src): array
761
    {
762
        $dir = $this->hash($src);
763
        $fileName = basename($src);
764
        $dstDir = $this->getRealBasePath() . DIRECTORY_SEPARATOR . $dir;
765
        $dstFile = $dstDir . DIRECTORY_SEPARATOR . $fileName;
766
767
        if (!is_dir($dstDir)) {
768
            FileHelper::createDirectory($dstDir, $this->dirMode);
769
        }
770
771
        if ($this->linkAssets) {
772
            if (!is_file($dstFile)) {
773
                try { // fix #6226 symlinking multi threaded
774
                    symlink($src, $dstFile);
775
                } catch (\Exception $e) {
776
                    if (!is_file($dstFile)) {
777
                        throw $e;
778
                    }
779
                }
780
            }
781
        } elseif (@filemtime($dstFile) < @filemtime($src)) {
782
            copy($src, $dstFile);
783
            if ($this->fileMode !== null) {
784
                @chmod($dstFile, $this->fileMode);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for chmod(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

784
                /** @scrutinizer ignore-unhandled */ @chmod($dstFile, $this->fileMode);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
785
            }
786
        }
787
788
        return [$dstFile, $this->baseUrl . "/$dir/$fileName"];
789
    }
790
791
    /**
792
     * Publishes a directory.
793
     *
794
     * @param string $src the asset directory to be published
795
     * @param array $options the options to be applied when publishing a directory. The following options are
796
     * supported:
797
     *
798
     * - only: array, list of patterns that the file paths should match if they want to be copied.
799
     * - except: array, list of patterns that the files or directories should match if they want to be excluded from
800
     *   being copied.
801
     * - caseSensitive: boolean, whether patterns specified at "only" or "except" should be case sensitive. Defaults
802
     *   to true.
803
     * - beforeCopy: callback, a PHP callback that is called before copying each sub-directory or file. This overrides
804
     *   {@see beforeCopy} if set.
805
     * - afterCopy: callback, a PHP callback that is called after a sub-directory or file is successfully copied. This
806
     *   overrides {@see afterCopy} if set.
807
     * - forceCopy: boolean, whether the directory being published should be copied even if it is found in the target
808
     *   directory. This option is used only when publishing a directory. This overrides {@see forceCopy} if set.
809
     *
810
     * @return array the path directory and the URL that the asset is published as.
811
     * @throws \Exception if the asset to be published does not exist.
812
     *
813
     */
814 2
    protected function publishDirectory(string $src, array $options): array
815
    {
816 2
        $dir = $this->hash($src);
817 2
        $dstDir = $this->getRealBasePath() . DIRECTORY_SEPARATOR . $dir;
818
819 2
        if ($this->linkAssets) {
820 1
            if (!is_dir($dstDir)) {
821 1
                FileHelper::createDirectory(dirname($dstDir), $this->dirMode);
822
823
                try { // fix #6226 symlinking multi threaded
824 1
                    symlink($src, $dstDir);
825
                } catch (\Exception $e) {
826
                    if (!is_dir($dstDir)) {
827 1
                        throw $e;
828
                    }
829
                }
830
            }
831 1
        } elseif (!empty($options['forceCopy']) || ($this->forceCopy && !isset($options['forceCopy'])) || !is_dir($dstDir)) {
832 1
            $opts = array_merge(
833 1
                $options,
834
                [
835 1
                    'dirMode' => $this->dirMode,
836 1
                    'fileMode' => $this->fileMode,
837
                    'copyEmptyDirectories' => false,
838
                ]
839
            );
840
841 1
            if (!isset($opts['beforeCopy'])) {
842 1
                if ($this->beforeCopy !== null) {
843
                    $opts['beforeCopy'] = $this->beforeCopy;
844
                } else {
845
                    $opts['beforeCopy'] = static function ($from, $to) {
0 ignored issues
show
Unused Code introduced by
The parameter $to is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

845
                    $opts['beforeCopy'] = static function ($from, /** @scrutinizer ignore-unused */ $to) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
846
                        return strncmp(basename($from), '.', 1) !== 0;
847
                    };
848
                }
849
            }
850
851 1
            if (!isset($opts['afterCopy']) && $this->afterCopy !== null) {
852
                $opts['afterCopy'] = $this->afterCopy;
853
            }
854
855 1
            FileHelper::copyDirectory($src, $dstDir, $opts);
856
        }
857
858
859 2
        return [$dstDir, $this->baseUrl . '/' . $dir];
860
    }
861
862
    /**
863
     * @param AssetBundle $bundle
864
     * @param string $asset
865
     *
866
     * @return string|bool
867
     */
868 8
    protected function resolveAsset(AssetBundle $bundle, string $asset)
869
    {
870 8
        if (isset($this->assetMap[$asset])) {
871
            return $this->assetMap[$asset];
872
        }
873
874 8
        if ($bundle->sourcePath !== null && $this->isRelative($asset)) {
875
            $asset = $bundle->sourcePath . '/' . $asset;
876
        }
877
878 8
        $n = mb_strlen($asset, 'utf-8');
879
880 8
        foreach ($this->assetMap as $from => $to) {
881
            $n2 = mb_strlen($from, 'utf-8');
882
            if ($n2 <= $n && substr_compare($asset, $from, $n - $n2, $n2) === 0) {
883
                return $to;
884
            }
885
        }
886
887 8
        return false;
888
    }
889
890
    /**
891
     * Set default paths asset manager.
892
     *
893
     * @return void
894
     */
895 51
    private function setDefaultPaths(): void
896
    {
897 51
        $this->setAlternativesAlias();
898
899 51
        $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 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...
900
901 51
        if (!is_dir($this->basePath)) {
902
            throw new \InvalidArgumentException("The directory does not exist: {$this->basePath}");
903
        }
904
905 51
        $this->basePath = (string)realpath($this->basePath);
906 51
        $this->baseUrl = rtrim($this->aliases->get($this->baseUrl), '/');
907
    }
908
909
    /**
910
     * Set alternatives aliases.
911
     *
912
     * @return void
913
     */
914 51
    protected function setAlternativesAlias(): void
915
    {
916 51
        foreach ($this->alternatives as $alias => $path) {
917 51
            $this->aliases->set($alias, $path);
918
        }
919
    }
920
}
921