Passed
Push — master ( 781931...9601bc )
by Alexander
02:28
created

AssetPublisher::hash()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 3
eloc 4
nc 3
nop 1
dl 0
loc 9
ccs 5
cts 5
cp 1
crap 3
rs 10
c 2
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Assets;
6
7
use Exception;
8
use Yiisoft\Aliases\Aliases;
9
use Yiisoft\Assets\Exception\InvalidConfigException;
10
use Yiisoft\Files\FileHelper;
11
12
use function array_merge;
13
use function crc32;
14
use function dirname;
15
use function file_exists;
16
use function is_callable;
17
use function is_dir;
18
use function is_file;
19
use function sprintf;
20
use function symlink;
21
22
/**
23
 * AssetPublisher is responsible for executing the publication of the assets
24
 * from {@see AssetBundle::$sourcePath} to {@see AssetBundle::$basePath}.
25
 */
26
final class AssetPublisher implements AssetPublisherInterface
27
{
28
    private Aliases $aliases;
29
30
    /**
31
     * @var int The permission to be set for newly generated asset directories. This value will be used by PHP chmod()
32
     * function. No umask will be applied. Defaults to 0775, meaning the directory is read-writable by owner
33
     * and group, but read-only for other users.
34
     */
35
    private int $dirMode = 0775;
36
37
    /**
38
     * @var int The permission to be set for newly published asset files. This value will be used by PHP chmod()
39
     * function. No umask will be applied. If not set, the permission will be determined by the current
40
     * environment.
41
     */
42
    private int $fileMode = 0755;
43
44
    /**
45
     * @var bool Whether the directory being published should be copied even if it is found in the target directory.
46
     * This option is used only when publishing a directory. You may want to set this to be `true` during the
47
     * development stage to make sure the published directory is always up-to-date. Do not set this to true
48
     * on production servers as it will significantly degrade the performance.
49
     */
50
    private bool $forceCopy = false;
51
52
    /**
53
     * @var callable|null A callback that will be called to produce hash for asset directory generation. The signature
54
     * of the callback should be as follows:
55
     *
56
     * ```
57
     * function ($path)
58
     * ```
59
     *
60
     * Where `$path` is the asset path. Note that the `$path` can be either directory where the asset files reside or a
61
     * single file. For a CSS file that uses relative path in `url()`, the hash implementation should use the directory
62
     * path of the file instead of the file path to include the relative asset files in the copying.
63
     *
64
     * If this is not set, the asset manager will use the default CRC32 and filemtime in the `hash` method.
65
     *
66
     * Example of an implementation using MD4 hash:
67
     *
68
     * ```php
69
     * function ($path) {
70
     *     return hash('md4', $path);
71
     * }
72
     * ```
73
     */
74
    private $hashCallback = null;
75
76
    /**
77
     * @var bool Whether to use symbolic link to publish asset files. Defaults to false, meaning asset files are copied
78
     * to {@see AssetBundle::$basePath}. Using symbolic links has the benefit that the published assets will always
79
     * be consistent with the source assets and there is no copy operation required.
80
     * This is especially useful during development.
81
     *
82
     * However, there are special requirements for hosting environments in order to use symbolic links. In particular,
83
     * symbolic links are supported only on Linux/Unix, and Windows Vista/2008 or greater.
84
     *
85
     * Moreover, some Web servers need to be properly configured so that the linked assets are accessible to Web users.
86
     * For example, for Apache Web server, the following configuration directive should be added for the Web folder:
87
     *
88
     * ```apache
89
     * Options FollowSymLinks
90
     * ```
91
     */
92
    private bool $linkAssets = false;
93
94
    /**
95
     * @var array Contain published {@see AssetsBundle}.
96
     */
97
    private array $published = [];
98
99 78
    public function __construct(Aliases $aliases)
100
    {
101 78
        $this->aliases = $aliases;
102 78
    }
103
104 14
    public function publish(AssetBundle $bundle): array
105
    {
106 14
        if (empty($bundle->sourcePath)) {
107 1
            throw new InvalidConfigException(
108 1
                'The sourcePath must be defined in AssetBundle property public ?string $sourcePath = $path.',
109
            );
110
        }
111
112 13
        if (isset($this->published[$bundle->sourcePath])) {
113 1
            return $this->published[$bundle->sourcePath];
114
        }
115
116 13
        if (empty($bundle->basePath)) {
117 2
            throw new InvalidConfigException(
118 2
                'The basePath must be defined in AssetBundle property public ?string $basePath = $path.',
119
            );
120
        }
121
122 12
        if ($bundle->baseUrl === null) {
123 1
            throw new InvalidConfigException(
124 1
                'The baseUrl must be defined in AssetBundle property public ?string $baseUrl = $path.',
125
            );
126
        }
127
128 11
        if (!file_exists($this->aliases->get($bundle->sourcePath))) {
129 1
            throw new InvalidConfigException("The sourcePath to be published does not exist: {$bundle->sourcePath}");
130
        }
131
132 10
        return $this->published[$bundle->sourcePath] = $this->publishBundleDirectory($bundle);
133
    }
134
135
    /**
136
     * Return config linkAssets.
137
     *
138
     * @return bool
139
     */
140 3
    public function getLinkAssets(): bool
141
    {
142 3
        return $this->linkAssets;
143
    }
144
145
    /**
146
     * Returns the published path of a file path.
147
     *
148
     * This method does not perform any publishing. It merely tells you if the file or directory is published, where it
149
     * will go.
150
     *
151
     * @param string $sourcePath The directory or file path being published.
152
     *
153
     * @return string|null The string the published file path. Null if the file or directory does not exist
154
     */
155 2
    public function getPublishedPath(string $sourcePath): ?string
156
    {
157 2
        if (isset($this->published[$sourcePath])) {
158 1
            return $this->published[$sourcePath][0];
159
        }
160
161 1
        return null;
162
    }
163
164
    /**
165
     * Returns the URL of a published file path.
166
     *
167
     * This method does not perform any publishing. It merely tells you if the file path is published,
168
     * what the URL will be to access it.
169
     *
170
     * @param string $sourcePath The directory or file path being published.
171
     *
172
     * @return string|null The string the published URL for the file or directory.
173
     * Null if the file or directory does not exist.
174
     */
175 2
    public function getPublishedUrl(string $sourcePath): ?string
176
    {
177 2
        if (isset($this->published[$sourcePath])) {
178 1
            return $this->published[$sourcePath][1];
179
        }
180
181 1
        return null;
182
    }
183
184
    /**
185
     * The permission to be set for newly generated asset directories.
186
     *
187
     * @param int $value
188
     *
189
     * {@see $dirMode}
190
     */
191 1
    public function setDirMode(int $value): void
192
    {
193 1
        $this->dirMode = $value;
194 1
    }
195
196
    /**
197
     * The permission to be set for newly published asset files.
198
     *
199
     * @param int $value
200
     *
201
     * {@see $fileMode}
202
     */
203 1
    public function setFileMode(int $value): void
204
    {
205 1
        $this->fileMode = $value;
206 1
    }
207
208
    /**
209
     * Whether the directory being published should be copied even if it is found in the target directory.
210
     *
211
     * @param bool $value
212
     *
213
     * {@see $forceCopy}
214
     */
215 78
    public function setForceCopy(bool $value): void
216
    {
217 78
        $this->forceCopy = $value;
218 78
    }
219
220
    /**
221
     * A callback that will be called to produce hash for asset directory generation.
222
     *
223
     * @param callable $value
224
     *
225
     * {@see $hashCallback}
226
     */
227 2
    public function setHashCallback(callable $value): void
228
    {
229 2
        $this->hashCallback = $value;
230 2
    }
231
232
    /**
233
     * Whether to use symbolic link to publish asset files.
234
     *
235
     * @param bool $value
236
     *
237
     * {@see $linkAssets}
238
     */
239 78
    public function setLinkAssets(bool $value): void
240
    {
241 78
        $this->linkAssets = $value;
242 78
    }
243
244
    /**
245
     * Generate a CRC32 hash for the directory path. Collisions are higher than MD5 but generates a much smaller hash
246
     * string.
247
     *
248
     * @param string $path The string to be hashed.
249
     *
250
     * @return string The hashed string.
251
     */
252 10
    private function hash(string $path): string
253
    {
254 10
        if (is_callable($this->hashCallback)) {
255 2
            return ($this->hashCallback)($path);
256
        }
257
258 8
        $path = (is_file($path) ? dirname($path) : $path) . FileHelper::lastModifiedTime($path);
259
260 8
        return sprintf('%x', crc32($path . '|' . $this->linkAssets));
261
    }
262
263
    /**
264
     * Publishes a bundle directory.
265
     *
266
     * @param AssetBundle $bundle The asset bundle instance.
267
     *
268
     * @throws Exception If the asset to be published does not exist.
269
     *
270
     * @return array The path directory and the URL that the asset is published as.
271
     */
272 10
    private function publishBundleDirectory(AssetBundle $bundle): array
273
    {
274 10
        $src = $this->aliases->get((string) $bundle->sourcePath);
275 10
        $dir = $this->hash($src);
276 10
        $dstDir = "{$this->aliases->get((string) $bundle->basePath)}/{$dir}";
277
278 10
        if ($this->linkAssets) {
279 1
            if (!is_dir($dstDir)) {
280 1
                FileHelper::ensureDirectory(dirname($dstDir), $this->dirMode);
281
                try { // fix #6226 symlinking multi threaded
282 1
                    symlink($src, $dstDir);
283
                } catch (Exception $e) {
284
                    if (!is_dir($dstDir)) {
285 1
                        throw $e;
286
                    }
287
                }
288
            }
289
        } elseif (
290 9
            !empty($bundle->publishOptions['forceCopy'])
291 9
            || ($this->forceCopy && !isset($bundle->publishOptions['forceCopy']))
292 9
            || !is_dir($dstDir)
293
        ) {
294 9
            FileHelper::copyDirectory($src, $dstDir, array_merge($bundle->publishOptions, [
295 9
                'dirMode' => $this->dirMode,
296 9
                'fileMode' => $this->fileMode,
297
                'copyEmptyDirectories' => false,
298
            ]));
299
        }
300
301 10
        return [$dstDir, "{$this->aliases->get((string) $bundle->baseUrl)}/{$dir}"];
302
    }
303
}
304