Passed
Pull Request — master (#110)
by Sergei
04:35 queued 02:08
created

AssetPublisher::publishBundleDirectory()   B

Complexity

Conditions 11
Paths 6

Size

Total Lines 50
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 11.0699

Importance

Changes 3
Bugs 3 Features 0
Metric Value
cc 11
eloc 24
c 3
b 3
f 0
nc 6
nop 1
dl 0
loc 50
ccs 22
cts 24
cp 0.9167
crap 11.0699
rs 7.3166

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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