Completed
Pull Request — master (#50)
by Wilmer
01:38
created

AssetManager::setAssetMap()   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 yii\helpers\Url;
8
use Yiisoft\Aliases\Aliases;
9
use Yiisoft\Asset\AssetBundle;
10
use Yiisoft\Asset\AssetConverterInterface;
11
use Yiisoft\Files\FileHelper;
12
use YiiSoft\Html\Html;
13
14
/**
15
 * AssetManager manages asset bundle configuration and loading.
16
 *
17
 * AssetManager is configured in config/web.php. You can access that instance via $container->get(AssetManager::class).
18
 *
19
 * You can modify its configuration by adding an array to your application config under `components` as shown in the
20
 * following example:
21
 *
22
 * ```php
23
24
 * ```
25
 *
26
 * @property AssetConverterInterface $converter The asset converter. Note that the type of this property differs in
27
 *                                   getter and setter. See {@see getConverter()} and {@see setConverter()} for
28
 *                                   details.
29
 */
30
class AssetManager
31
{
32
    /**
33
     * @var callable a PHP callback that is called after a sub-directory or file is successfully copied. This option is
34
     *               used only when publishing a directory. The signature of the callback is the same as for
35
     *               {@see {beforeCopy}.
36
     *               This is passed as a parameter `afterCopy` to {@see \Yiisoft\Files\FileHelper::copyDirectory()}.
37
     */
38
    private $afterCopy;
39
40
    /**
41
     * Aliases component.
42
     *
43
     * @var Aliases $aliase
44
     */
45
    private $aliases;
46
47
    /**
48
     * directory path node_modules.
49
     *
50
     * @var array $alternatives
51
     */
52
    private $alternatives = [
53
        '@npm' => '@root/node_modules',
54
    ];
55
56
    /**
57
     * @var bool whether to append a timestamp to the URL of every published asset. When this is true, the URL of a
58
     *           published asset may look like `/path/to/asset?v=timestamp`, where `timestamp` is the last modification
59
     *           time of the published asset file. You normally would want to set this property to true when you have
60
     *           enabled HTTP caching for assets, because it allows you to bust caching when the assets are updated.
61
     */
62
    private $appendTimestamp = false;
63
64
    /**
65
     * @var array mapping from source asset files (keys) to target asset files (values).
66
     *
67
     * This property is provided to support fixing incorrect asset file paths in some asset bundles. When an asset
68
     * bundle is registered with a view, each relative asset file in its {@see AssetBundle::css|css} and
69
     * {@see AssetBundle::js|js} arrays will be examined against this map. If any of the keys is found to be the last
70
     * part of an asset file (which is prefixed with {@see {AssetBundle::sourcePath} if available), the corresponding
71
     * value will replace the asset and be registered with the view. For example, an asset file `my/path/to/jquery.js`
72
     * matches a key `jquery.js`.
73
     *
74
     * Note that the target asset files should be absolute URLs, domain relative URLs (starting from '/') or paths
75
     * relative to {@see baseUrl} and {@see basePath}.
76
     *
77
     * In the following example, any assets ending with `jquery.min.js` will be replaced with `jquery/dist/jquery.js`
78
     * which is relative to {@see baseUrl} and {@see basePath}.
79
     *
80
     * ```php
81
     * [
82
     *     'jquery.min.js' => 'jquery/dist/jquery.js',
83
     * ]
84
     * ```
85
     *
86
     * You may also use aliases while specifying map value, for example:
87
     *
88
     * ```php
89
     * [
90
     *     'jquery.min.js' => '@web/js/jquery/jquery.js',
91
     * ]
92
     * ```
93
     */
94
    private $assetMap = [];
95
96
    /**
97
     * @var string the root directory storing the published asset files.
98
     */
99
    private $basePath = '@public/assets';
100
101
    /**
102
     * @var string the base URL through which the published asset files can be accessed.
103
     */
104
    private $baseUrl = '@web/assets';
105
106
    /**
107
     * @var callable a PHP callback that is called before copying each sub-directory or file. This option is used only
108
     *               when publishing a directory. If the callback returns false, the copy operation for the
109
     *               sub-directory or file will be cancelled.
110
     *
111
     * The signature of the callback should be: `function ($from, $to)`, where `$from` is the sub-directory or file to
112
     * be copied from, while `$to` is the copy target.
113
     *
114
     * This is passed as a parameter `beforeCopy` to {@see Yiisoft\Files\FileHelper::copyDirectory()}.
115
     */
116
    private $beforeCopy;
117
118
    /**
119
     * @var array|bool list of asset bundle configurations. This property is provided to customize asset bundles.
120
     *                 When a bundle is being loaded by {@see getBundle()}, if it has a corresponding configuration
121
     *                 specified here, the configuration will be applied to the bundle.
122
     *
123
     * The array keys are the asset bundle names, which typically are asset bundle class names without leading
124
     * backslash. The array values are the corresponding configurations. If a value is false, it means the corresponding
125
     * asset bundle is disabled and {@see getBundle()} should return null.
126
     *
127
     * If this property is false, it means the whole asset bundle feature is disabled and {@see {getBundle()} will
128
     * always return null.
129
     *
130
     * The following example shows how to disable the bootstrap css file used by Bootstrap widgets (because you want to
131
     * use your own styles):
132
     *
133
     * ```php
134
     * [
135
     *     \Yiisoft\Bootstrap4\BootstrapAsset::class => [
136
     *         'css' => [],
137
     *     ],
138
     * ]
139
     * ```
140
     */
141
    private $bundles = [];
142
143
    /**
144
     * AssetConverter component.
145
     *
146
     * @var AssetConverterInterface $converter
147
     */
148
    private $converter;
149
150
    /**
151
     * @var int the permission to be set for newly generated asset directories. This value will be used by PHP chmod()
152
     *          function. No umask will be applied. Defaults to 0775, meaning the directory is read-writable by owner
153
     *          and group, but read-only for other users.
154
     */
155
    private $dirMode = 0775;
156
157
    /**
158
     * @var AssetBundle $dummyBundles
159
     */
160
    private $dummyBundles;
161
162
    /**
163
     * @var int the permission to be set for newly published asset files. This value will be used by PHP chmod()
164
     *          function. No umask will be applied. If not set, the permission will be determined by the current
165
     *          environment.
166
     */
167
    private $fileMode;
168
169
    /**
170
     * @var bool whether the directory being published should be copied even if it is found in the target directory.
171
     *           This option is used only when publishing a directory. You may want to set this to be `true` during the
172
     *           development stage to make sure the published directory is always up-to-date. Do not set this to true
173
     *           on production servers as it will significantly degrade the performance.
174
     */
175
    private $forceCopy = false;
176
177
    /**
178
     * @var callable a callback that will be called to produce hash for asset directory generation. The signature of the
179
     *               callback should be as follows:
180
     *
181
     * ```
182
     * function ($path)
183
     * ```
184
     *
185
     * where `$path` is the asset path. Note that the `$path` can be either directory where the asset files reside or a
186
     * single file. For a CSS file that uses relative path in `url()`, the hash implementation should use the directory
187
     * path of the file instead of the file path to include the relative asset files in the copying.
188
     *
189
     * If this is not set, the asset manager will use the default CRC32 and filemtime in the `hash` method.
190
     *
191
     * Example of an implementation using MD4 hash:
192
     *
193
     * ```php
194
     * function ($path) {
195
     *     return hash('md4', $path);
196
     * }
197
     * ```
198
     */
199
    private $hashCallback;
200
201
    /**
202
     * @var bool whether to use symbolic link to publish asset files. Defaults to false, meaning asset files are copied
203
     *           to {@see basePath}. Using symbolic links has the benefit that the published assets will always be
204
     *           consistent with the source assets and there is no copy operation required. This is especially useful
205
     *           during development.
206
     *
207
     * However, there are special requirements for hosting environments in order to use symbolic links. In particular,
208
     * symbolic links are supported only on Linux/Unix, and Windows Vista/2008 or greater.
209
     *
210
     * Moreover, some Web servers need to be properly configured so that the linked assets are accessible to Web users.
211
     * For example, for Apache Web server, the following configuration directive should be added for the Web folder:
212
     *
213
     * ```apache
214
     * Options FollowSymLinks
215
     * ```
216
     */
217
    private $linkAssets = false;
218
219
    /**
220
     * @var LoggerInterface $logger
221
     */
222
    private $logger;
223
224
    /**
225
     * @var array published assets
226
     */
227
    private $published = [];
228
229
    /**
230
     * @var string $realBasePath
231
     */
232
    private $realBasePath;
233
234
    /**
235
     * AssetManager constructor.
236
     *
237
     * @param Aliases $aliases
238
     */
239 51
    public function __construct(Aliases $aliases, LoggerInterface $logger)
240
    {
241 51
        $this->aliases = $aliases;
242 51
        $this->logger = $logger;
243 51
        $this->setDefaultPaths();
244 51
    }
245
246
    /**
247
     * Returns the actual URL for the specified asset.
248
     * The actual URL is obtained by prepending either {@see AssetBundle::$baseUrl} or {@see AssetManager::$baseUrl} to
249
     * the given asset path.
250
     *
251
     * @param AssetBundle $bundle the asset bundle which the asset file belongs to
252
     * @param string      $asset the asset path. This should be one of the assets listed in {@see AssetBundle::$js} or
253
     *                    {@see AssetBundle::$css}.
254
     *
255
     * @return string the actual URL for the specified asset.
256
     */
257 8
    public function getAssetUrl(AssetBundle $bundle, string $asset): string
258
    {
259 8
        if (($actualAsset = $this->resolveAsset($bundle, $asset)) !== false) {
260
            if (strncmp((string) $actualAsset, '@web/', 5) === 0) {
261
                $asset = substr((string) $actualAsset, 5);
262
                $basePath = $this->aliases->get('@public');
263
                $baseUrl = $this->aliases->get('@web');
264
            } else {
265
                $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

265
                $asset = $this->aliases->get(/** @scrutinizer ignore-type */ $actualAsset);
Loading history...
266
                $basePath = $this->getRealBasePath();
267
                $baseUrl = $this->baseUrl;
268
            }
269
        } else {
270 8
            $basePath = $this->aliases->get($bundle->basePath);
271 8
            $baseUrl = $this->aliases->get($bundle->baseUrl);
272
        }
273
274 8
        if (!Url::isRelative($asset) || strncmp($asset, '/', 1) === 0) {
275
            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...
276
        }
277
278 8
        if ($this->appendTimestamp && ($timestamp = @filemtime("$basePath/$asset")) > 0) {
279
            return "$baseUrl/$asset?v=$timestamp";
280
        }
281
282 8
        return "$baseUrl/$asset";
283
    }
284
285
    /**
286
     * Returns the actual file path for the specified asset.
287
     *
288
     * @param AssetBundle $bundle the asset bundle which the asset file belongs to
289
     * @param string      $asset  the asset path. This should be one of the assets listed in {@see AssetBundle::$js} or
290
     *                    {@see AssetBundle::$css}.
291
     *
292
     * @return false|string the actual file path, or `false` if the asset is specified as an absolute URL
293
     */
294
    public function getAssetPath(AssetBundle $bundle, string $asset)
295
    {
296
        if (($actualAsset = $this->resolveAsset($bundle, $asset)) !== false) {
297
            return Url::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

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

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

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