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

AssetPublisher::publish()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 9
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 16
rs 9.9666
ccs 10
cts 10
cp 1
crap 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Assets;
6
7
use Yiisoft\Assets\Exception\InvalidConfigException;
8
use Yiisoft\Files\FileHelper;
9
10
/**
11
 * AssetPublisher is responsible for executing the publication of the assets from {@see sourcePath} to {@see basePath}.
12
 */
13
final class AssetPublisher
14
{
15
    /**
16
     * @var AssetManager $assetManager
17
     */
18
    private AssetManager $assetManager;
19
20
    /**
21
     * @var string|null the root directory storing the published asset files.
22
     */
23
    private ?string $basePath;
24
25
    /**
26
     * @var string|null the root directory storing the published asset files.
27
     */
28
    private ?string $baseUrl;
29
30
    /**
31
     * @var array published assets
32
     */
33
    private array $published = [];
34
35 54
    public function __construct(AssetManager $assetManager)
36
    {
37 54
        $this->assetManager = $assetManager;
38 54
    }
39
40
    /**
41
     * Returns the actual URL for the specified asset.
42
     *
43
     * The actual URL is obtained by prepending either {@see AssetBundle::$baseUrl} or {@see AssetManager::$baseUrl} to
44
     * the given asset path.
45
     *
46
     * @param AssetBundle $bundle the asset bundle which the asset file belongs to.
47
     * @param string $pathAsset the asset path. This should be one of the assets listed in {@see AssetBundle::$js} or
48
     * {@see AssetBundle::$css}.
49
     *
50
     * @return string the actual URL for the specified asset.
51
     * @throws InvalidConfigException
52
     */
53 20
    public function getAssetUrl(AssetBundle $bundle, string $pathAsset): string
54
    {
55 20
        $basePath = $this->assetManager->getAliases()->get($bundle->basePath);
56 20
        $baseUrl = $this->assetManager->getAliases()->get($bundle->baseUrl);
57
58 20
        $asset = AssetUtil::resolveAsset($bundle, $pathAsset, $this->assetManager->getAssetMap());
59
60 20
        if (!empty($asset)) {
61 1
            $pathAsset = $asset;
62
        }
63
64 20
        if (!AssetUtil::isRelative($pathAsset) || strncmp($pathAsset, '/', 1) === 0) {
65 2
            return $pathAsset;
66
        }
67
68 18
        if (!is_file("$basePath/$pathAsset")) {
69 1
            throw new InvalidConfigException("Asset files not found: '$basePath/$pathAsset.'");
70
        }
71
72 17
        if ($this->assetManager->getAppendTimestamp()  && ($timestamp = @filemtime("$basePath/$pathAsset")) > 0) {
73 1
            return "$baseUrl/$pathAsset?v=$timestamp";
74
        }
75
76 16
        return "$baseUrl/$pathAsset";
77
    }
78
79
    /**
80
     * Loads asset bundle class by name.
81
     *
82
     * @param string $name bundle name.
83
     * @param array $config bundle object configuration.
84
     *
85
     * @return AssetBundle
86
     *
87
     * @throws InvalidConfigException
88
     */
89 27
    public function loadBundle(string $name, array $config = []): AssetBundle
90
    {
91
        /** @var AssetBundle $bundle */
92 27
        $bundle = new $name();
93
94 27
        foreach ($config as $property => $value) {
95 13
            $bundle->$property = $value;
96
        }
97
98 27
        $this->checkBasePath($bundle->basePath);
99 26
        $this->checkBaseUrl($bundle->baseUrl);
100
101 25
        if (!empty($bundle->sourcePath)) {
102 6
            [$bundle->basePath, $bundle->baseUrl] = ($this->publish($bundle));
103
        }
104
105 25
        return $bundle;
106
    }
107
    /**
108
     * Publishes a file or a directory.
109
     *
110
     * This method will copy the specified file or directory to {@see basePath} so that it can be accessed via the Web
111
     * server.
112
     *
113
     * If the asset is a file, its file modification time will be checked to avoid unnecessary file copying.
114
     *
115
     * If the asset is a directory, all files and subdirectories under it will be published recursively. Note, in case
116
     * $forceCopy is false the method only checks the existence of the target directory to avoid repetitive copying
117
     * (which is very expensive).
118
     *
119
     * By default, when publishing a directory, subdirectories and files whose name starts with a dot "." will NOT be
120
     * published.
121
     *
122
     * Note: On rare scenario, a race condition can develop that will lead to a  one-time-manifestation of a
123
     * non-critical problem in the creation of the directory that holds the published assets. This problem can be
124
     * avoided altogether by 'requesting' in advance all the resources that are supposed to trigger a 'publish()' call,
125
     * and doing that in the application deployment phase, before system goes live. See more in the following
126
     * discussion: http://code.google.com/p/yii/issues/detail?id=2579
127
     *
128
     * @param AssetBundle $bundle the asset (file or directory) to be read.
129
     *
130
     * - only: array, list of patterns that the file paths should match if they want to be copied.
131
     *
132
     * @return array the path (directory or file path) and the URL that the asset is published as.
133
     * @throws InvalidConfigException if the asset to be published does not exist.
134
     *
135
     */
136 8
    public function publish(AssetBundle $bundle): array
137
    {
138 8
        $this->checkBasePath($bundle->basePath);
139 8
        $this->checkBaseUrl($bundle->baseUrl);
140
141 8
        if (isset($this->published[$bundle->sourcePath])) {
142 1
            return $this->published[$bundle->sourcePath];
143
        }
144
145 8
        if (!file_exists($this->assetManager->getAliases()->get($bundle->sourcePath))) {
146 1
            throw new InvalidConfigException("The sourcePath to be published does not exist: $bundle->sourcePath");
147
        }
148
149 7
        return $this->published[$bundle->sourcePath] = $this->publishDirectory(
150 7
            $bundle->sourcePath,
0 ignored issues
show
Bug introduced by
It seems like $bundle->sourcePath can also be of type null; however, parameter $src of Yiisoft\Assets\AssetPublisher::publishDirectory() 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

150
            /** @scrutinizer ignore-type */ $bundle->sourcePath,
Loading history...
151 7
            $bundle->publishOptions
152
        );
153
    }
154
155
    /**
156
     * Returns the published path of a file path.
157
     *
158
     * This method does not perform any publishing. It merely tells you if the file or directory is published, where it
159
     * will go.
160
     *
161
     * @param string $sourcePath directory or file path being published.
162
     *
163
     * @return string|null string the published file path. Null if the file or directory does not exist
164
     */
165 2
    public function getPublishedPath(string $sourcePath): ?string
166
    {
167 2
        if (isset($this->published[$sourcePath])) {
168 1
            return $this->published[$sourcePath][0];
169
        }
170
171 1
        return null;
172
    }
173
174
    /**
175
     * Returns the URL of a published file path.
176
     *
177
     * This method does not perform any publishing. It merely tells you if the file path is published, what the URL will
178
     * be to access it.
179
     *
180
     * @param string $sourcePath directory or file path being published
181
     *
182
     * @return string|null string the published URL for the file or directory. Null if the file or directory does not
183
     * exist.
184
     */
185 2
    public function getPublishedUrl(string $sourcePath): ?string
186
    {
187 2
        if (isset($this->published[$sourcePath])) {
188 1
            return $this->published[$sourcePath][1];
189
        }
190
191 1
        return null;
192
    }
193
194
    /**
195
     * Registers the CSS and JS files with the given view.
196
     *
197
     * @param AssetBundle $bundle the asset files are to be registered in the view.
198
     *
199
     * @return void
200
     */
201 20
    public function registerAssetFiles(AssetBundle $bundle): void
202
    {
203 20
        foreach ($bundle->js as $js) {
204 20
            if (\is_array($js)) {
205 2
                $file = array_shift($js);
206 2
                $options = array_merge($bundle->jsOptions, $js);
207 2
                $this->assetManager->registerJsFile($this->getAssetUrl($bundle, $file), $options);
208 19
            } elseif ($js !== null) {
209 19
                $this->assetManager->registerJsFile($this->getAssetUrl($bundle, $js), $bundle->jsOptions);
210
            }
211
        }
212
213 19
        foreach ($bundle->css as $css) {
214 14
            if (\is_array($css)) {
215 1
                $file = array_shift($css);
216 1
                $options = array_merge($bundle->cssOptions, $css);
217 1
                $this->assetManager->registerCssFile($this->getAssetUrl($bundle, $file), $options);
218 14
            } elseif ($css !== null) {
219 14
                $this->assetManager->registerCssFile($this->getAssetUrl($bundle, $css), $bundle->cssOptions);
220
            }
221
        }
222 19
    }
223
224 29
    private function checkBasePath(?string $basePath): void
225
    {
226 29
        if (empty($basePath) && empty($this->assetManager->getBasePath())) {
227 1
            throw new InvalidConfigException(
228
                'basePath must be set in AssetManager->setBasePath($path) or ' .
229 1
                'AssetBundle property public ?string $basePath = $path'
230
            );
231
        }
232
233 28
        if (empty($basePath)) {
234 1
            $this->basePath = $this->assetManager->getBasePath();
235
        } else {
236 27
            $this->basePath = $this->assetManager->getAliases()->get($basePath);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->assetManager->getAliases()->get($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...
237
        }
238 28
    }
239
240 28
    private function checkBaseUrl(?string $baseUrl): void
241
    {
242 28
        if (empty($baseUrl) && empty($this->assetManager->getBaseUrl())) {
243 1
            throw new InvalidConfigException(
244
                'baseUrl must be set in AssetManager->setBaseUrl($path) or ' .
245 1
                'AssetBundle property public ?string $baseUrl = $path'
246
            );
247
        }
248
249 27
        if (empty($baseUrl)) {
250
            $this->baseUrl = $this->assetManager->getBaseUrl();
251
        } else {
252 27
            $this->baseUrl = $this->assetManager->getAliases()->get($baseUrl);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->assetManager->getAliases()->get($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...
253
        }
254 27
    }
255
256
    /**
257
     * Generate a CRC32 hash for the directory path. Collisions are higher than MD5 but generates a much smaller hash
258
     * string.
259
     *
260
     * @param string $path string to be hashed.
261
     *
262
     * @return string hashed string.
263
     */
264 7
    private function hash(string $path): string
265
    {
266 7
        if (\is_callable($this->assetManager->getHashCallback())) {
267 1
            return \call_user_func($this->assetManager->getHashCallback(), $path);
268
        }
269
270 6
        $path = (is_file($path) ? \dirname($path) : $path) . @filemtime($path);
271
272 6
        return sprintf('%x', crc32($path . '|' . $this->assetManager->getLinkAssets()));
273
    }
274
275
    /**
276
     * Publishes a directory.
277
     *
278
     * @param string $src the asset directory to be published
279
     * @param array $options the options to be applied when publishing a directory. The following options are
280
     * supported:
281
     *
282
     * - only: patterns that the file paths should match if they want to be copied.
283
     *
284
     * @return array the path directory and the URL that the asset is published as.
285
     *
286
     * @throws \Exception if the asset to be published does not exist.
287
     */
288 7
    private function publishDirectory(string $src, array $options): array
289
    {
290 7
        $src = $this->assetManager->getAliases()->get($src);
291 7
        $dir = $this->hash($src);
0 ignored issues
show
Bug introduced by
It seems like $src can also be of type boolean; however, parameter $path of Yiisoft\Assets\AssetPublisher::hash() 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

291
        $dir = $this->hash(/** @scrutinizer ignore-type */ $src);
Loading history...
292 7
        $dstDir = $this->basePath . '/' . $dir;
293
294 7
        if ($this->assetManager->getLinkAssets()) {
295
            if (!is_dir($dstDir)) {
296
                FileHelper::createDirectory(\dirname($dstDir), $this->assetManager->getDirMode());
297
                try { // fix #6226 symlinking multi threaded
298
                    symlink($src, $dstDir);
299
                } catch (\Exception $e) {
300
                    if (!is_dir($dstDir)) {
301
                        throw $e;
302
                    }
303
                }
304
            }
305 7
        } elseif (!empty($options['forceCopy']) ||
306 7
            ($this->assetManager->getForceCopy() && !isset($options['forceCopy'])) || !is_dir($dstDir)) {
307 7
            $opts = array_merge(
308 7
                $options,
309
                [
310 7
                    'dirMode' => $this->assetManager->getDirMode(),
311 7
                    'fileMode' => $this->assetManager->getFileMode(),
312
                    'copyEmptyDirectories' => false,
313
                ]
314
            );
315
316 7
            FileHelper::copyDirectory($src, $dstDir, $opts);
0 ignored issues
show
Bug introduced by
It seems like $src can also be of type boolean; however, parameter $source of Yiisoft\Files\FileHelper::copyDirectory() 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

316
            FileHelper::copyDirectory(/** @scrutinizer ignore-type */ $src, $dstDir, $opts);
Loading history...
317
        }
318
319 7
        return [$dstDir, $this->baseUrl . '/' . $dir];
320
    }
321
}
322