Passed
Push — master ( 506152...7c24f3 )
by Alexander
05:14 queued 01:53
created

AssetPublisher::withLinkAssets()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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