Passed
Push — master ( 9601bc...40dd50 )
by Evgeniy
02:37
created

AssetPublisher   A

Complexity

Total Complexity 29

Size/Duplication

Total Lines 282
Duplicated Lines 0 %

Test Coverage

Coverage 97.1%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 61
c 3
b 0
f 0
dl 0
loc 282
ccs 67
cts 69
cp 0.971
rs 10
wmc 29

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A setFileMode() 0 3 1
A setDirMode() 0 3 1
A getLinkAssets() 0 3 1
B publishBundleDirectory() 0 30 9
A setForceCopy() 0 3 1
A setLinkAssets() 0 3 1
A hash() 0 9 3
A setHashCallback() 0 3 1
A getPublishedPath() 0 9 2
A publish() 0 31 6
A getPublishedUrl() 0 9 2
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 81
    public function __construct(Aliases $aliases)
100
    {
101 81
        $this->aliases = $aliases;
102 81
    }
103
104 16
    public function publish(AssetBundle $bundle): array
105
    {
106 16
        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 15
        $sourcePath = $this->aliases->get($bundle->sourcePath);
113
114 15
        if (isset($this->published[$sourcePath])) {
115 3
            return $this->published[$sourcePath];
116
        }
117
118 15
        if (empty($bundle->basePath)) {
119 2
            throw new InvalidConfigException(
120 2
                'The basePath must be defined in AssetBundle property public ?string $basePath = $path.',
121
            );
122
        }
123
124 14
        if ($bundle->baseUrl === null) {
125 1
            throw new InvalidConfigException(
126 1
                'The baseUrl must be defined in AssetBundle property public ?string $baseUrl = $path.',
127
            );
128
        }
129
130 13
        if (!file_exists($sourcePath)) {
131 1
            throw new InvalidConfigException("The sourcePath to be published does not exist: {$sourcePath}");
132
        }
133
134 12
        return $this->published[$sourcePath] = $this->publishBundleDirectory($bundle);
135
    }
136
137
    /**
138
     * Return config linkAssets.
139
     *
140
     * @return bool
141
     */
142 3
    public function getLinkAssets(): bool
143
    {
144 3
        return $this->linkAssets;
145
    }
146
147
    /**
148
     * Returns the published path of a file path.
149
     *
150
     * This method does not perform any publishing. It merely tells you if the file or directory is published, where it
151
     * will go.
152
     *
153
     * @param string $sourcePath The directory or file path being published.
154
     *
155
     * @return string|null The string the published file path. Null if the file or directory does not exist
156
     */
157 2
    public function getPublishedPath(string $sourcePath): ?string
158
    {
159 2
        $sourcePath = $this->aliases->get($sourcePath);
160
161 2
        if (isset($this->published[$sourcePath])) {
162 1
            return $this->published[$sourcePath][0];
163
        }
164
165 1
        return null;
166
    }
167
168
    /**
169
     * Returns the URL of a published file path.
170
     *
171
     * This method does not perform any publishing. It merely tells you if the file path is published,
172
     * what the URL will be to access it.
173
     *
174
     * @param string $sourcePath The directory or file path being published.
175
     *
176
     * @return string|null The string the published URL for the file or directory.
177
     * Null if the file or directory does not exist.
178
     */
179 2
    public function getPublishedUrl(string $sourcePath): ?string
180
    {
181 2
        $sourcePath = $this->aliases->get($sourcePath);
182
183 2
        if (isset($this->published[$sourcePath])) {
184 1
            return $this->published[$sourcePath][1];
185
        }
186
187 1
        return null;
188
    }
189
190
    /**
191
     * The permission to be set for newly generated asset directories.
192
     *
193
     * @param int $value
194
     *
195
     * {@see $dirMode}
196
     */
197 1
    public function setDirMode(int $value): void
198
    {
199 1
        $this->dirMode = $value;
200 1
    }
201
202
    /**
203
     * The permission to be set for newly published asset files.
204
     *
205
     * @param int $value
206
     *
207
     * {@see $fileMode}
208
     */
209 1
    public function setFileMode(int $value): void
210
    {
211 1
        $this->fileMode = $value;
212 1
    }
213
214
    /**
215
     * Whether the directory being published should be copied even if it is found in the target directory.
216
     *
217
     * @param bool $value
218
     *
219
     * {@see $forceCopy}
220
     */
221 81
    public function setForceCopy(bool $value): void
222
    {
223 81
        $this->forceCopy = $value;
224 81
    }
225
226
    /**
227
     * A callback that will be called to produce hash for asset directory generation.
228
     *
229
     * @param callable $value
230
     *
231
     * {@see $hashCallback}
232
     */
233 2
    public function setHashCallback(callable $value): void
234
    {
235 2
        $this->hashCallback = $value;
236 2
    }
237
238
    /**
239
     * Whether to use symbolic link to publish asset files.
240
     *
241
     * @param bool $value
242
     *
243
     * {@see $linkAssets}
244
     */
245 81
    public function setLinkAssets(bool $value): void
246
    {
247 81
        $this->linkAssets = $value;
248 81
    }
249
250
    /**
251
     * Generate a CRC32 hash for the directory path. Collisions are higher than MD5 but generates a much smaller hash
252
     * string.
253
     *
254
     * @param string $path The string to be hashed.
255
     *
256
     * @return string The hashed string.
257
     */
258 12
    private function hash(string $path): string
259
    {
260 12
        if (is_callable($this->hashCallback)) {
261 2
            return ($this->hashCallback)($path);
262
        }
263
264 10
        $path = (is_file($path) ? dirname($path) : $path) . FileHelper::lastModifiedTime($path);
265
266 10
        return sprintf('%x', crc32($path . '|' . $this->linkAssets));
267
    }
268
269
    /**
270
     * Publishes a bundle directory.
271
     *
272
     * @param AssetBundle $bundle The asset bundle instance.
273
     *
274
     * @throws Exception If the asset to be published does not exist.
275
     *
276
     * @return array The path directory and the URL that the asset is published as.
277
     */
278 12
    private function publishBundleDirectory(AssetBundle $bundle): array
279
    {
280 12
        $src = $this->aliases->get((string) $bundle->sourcePath);
281 12
        $dir = $this->hash($src);
282 12
        $dstDir = "{$this->aliases->get((string) $bundle->basePath)}/{$dir}";
283
284 12
        if ($this->linkAssets) {
285 1
            if (!is_dir($dstDir)) {
286 1
                FileHelper::ensureDirectory(dirname($dstDir), $this->dirMode);
287
                try { // fix #6226 symlinking multi threaded
288 1
                    symlink($src, $dstDir);
289
                } catch (Exception $e) {
290
                    if (!is_dir($dstDir)) {
291 1
                        throw $e;
292
                    }
293
                }
294
            }
295
        } elseif (
296 11
            !empty($bundle->publishOptions['forceCopy'])
297 11
            || ($this->forceCopy && !isset($bundle->publishOptions['forceCopy']))
298 11
            || !is_dir($dstDir)
299
        ) {
300 11
            FileHelper::copyDirectory($src, $dstDir, array_merge($bundle->publishOptions, [
301 11
                'dirMode' => $this->dirMode,
302 11
                'fileMode' => $this->fileMode,
303
                'copyEmptyDirectories' => false,
304
            ]));
305
        }
306
307 12
        return [$dstDir, "{$this->aliases->get((string) $bundle->baseUrl)}/{$dir}"];
308
    }
309
}
310