AssetPublisher::withFileMode()   A
last analyzed

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