Passed
Push — master ( a1416a...fd5900 )
by Alexander
01:40
created

AssetPublisher   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 298
Duplicated Lines 0 %

Test Coverage

Coverage 92.78%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 94
c 3
b 0
f 0
dl 0
loc 298
rs 8.8798
ccs 90
cts 97
cp 0.9278
wmc 44

10 Methods

Rating   Name   Duplication   Size   Complexity  
A checkBasePath() 0 13 4
B getAssetUrl() 0 24 7
A getPublishedPath() 0 7 2
A publish() 0 17 3
B publishDirectory() 0 32 9
A checkBaseUrl() 0 13 4
A loadBundle() 0 17 3
B registerAssetFiles() 0 19 7
A getPublishedUrl() 0 7 2
A hash() 0 9 3

How to fix   Complexity   

Complex Class

Complex classes like AssetPublisher often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AssetPublisher, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Assets;
6
7
use Yiisoft\Assets\Exception\InvalidConfigException;
8
use Yiisoft\Files\FileHelper;
9
10
/**
11
 * AssetPublisher is responsible for executing the publication of the assets from {@see sourcePath} to {@see basePath}.
12
 */
13
final class AssetPublisher
14
{
15
    /**
16
     * @var string|null the root directory storing the published asset files.
17
     */
18
    private ?string $basePath;
19
20
    /**
21
     * @var string|null the root directory storing the published asset files.
22
     */
23
    private ?string $baseUrl;
24
25
    /**
26
     * @var array published assets
27
     */
28
    private array $published = [];
29
30
    /**
31
     * Returns the actual URL for the specified asset.
32
     *
33
     * The actual URL is obtained by prepending either {@see AssetBundle::$baseUrl} or {@see AssetManager::$baseUrl} to
34
     * the given asset path.
35
     *
36
     * @param AssetBundle $bundle the asset bundle which the asset file belongs to.
37
     * @param string $pathAsset the asset path. This should be one of the assets listed in {@see AssetBundle::$js} or
38
     * {@see AssetBundle::$css}.
39
     *
40
     * @return string the actual URL for the specified asset.
41
     * @throws InvalidConfigException
42
     */
43 20
    public function getAssetUrl(AssetManager $am, AssetBundle $bundle, string $pathAsset): string
44
    {
45 20
        $basePath = $am->getAliases()->get($bundle->basePath);
46 20
        $baseUrl = $am->getAliases()->get($bundle->baseUrl);
47
48 20
        $asset = AssetUtil::resolveAsset($bundle, $pathAsset, $am->getAssetMap());
49
50 20
        if (!empty($asset)) {
51 1
            $pathAsset = $asset;
52
        }
53
54 20
        if (!AssetUtil::isRelative($pathAsset) || strncmp($pathAsset, '/', 1) === 0) {
55 2
            return $pathAsset;
56
        }
57
58 18
        if (!is_file("$basePath/$pathAsset")) {
59 1
            throw new InvalidConfigException("Asset files not found: '$basePath/$pathAsset.'");
60
        }
61
62 17
        if ($am->getAppendTimestamp()  && ($timestamp = @filemtime("$basePath/$pathAsset")) > 0) {
63 1
            return "$baseUrl/$pathAsset?v=$timestamp";
64
        }
65
66 16
        return "$baseUrl/$pathAsset";
67
    }
68
69
    /**
70
     * Loads asset bundle class by name.
71
     *
72
     * @param string $name bundle name.
73
     * @param array $config bundle object configuration.
74
     *
75
     * @return AssetBundle
76
     *
77
     * @throws InvalidConfigException
78
     */
79 27
    public function loadBundle(AssetManager $am, string $name, array $config = []): AssetBundle
80
    {
81
        /** @var AssetBundle $bundle */
82 27
        $bundle = new $name();
83
84 27
        foreach ($config as $property => $value) {
85 13
            $bundle->$property = $value;
86
        }
87
88 27
        $this->checkBasePath($am, $bundle->basePath);
89 26
        $this->checkBaseUrl($am, $bundle->baseUrl);
90
91 25
        if (!empty($bundle->sourcePath)) {
92 6
            [$bundle->basePath, $bundle->baseUrl] = ($this->publish($am, $bundle));
93
        }
94
95 25
        return $bundle;
96
    }
97
    /**
98
     * Publishes a file or a directory.
99
     *
100
     * This method will copy the specified file or directory to {@see basePath} so that it can be accessed via the Web
101
     * server.
102
     *
103
     * If the asset is a file, its file modification time will be checked to avoid unnecessary file copying.
104
     *
105
     * If the asset is a directory, all files and subdirectories under it will be published recursively. Note, in case
106
     * $forceCopy is false the method only checks the existence of the target directory to avoid repetitive copying
107
     * (which is very expensive).
108
     *
109
     * By default, when publishing a directory, subdirectories and files whose name starts with a dot "." will NOT be
110
     * published.
111
     *
112
     * Note: On rare scenario, a race condition can develop that will lead to a  one-time-manifestation of a
113
     * non-critical problem in the creation of the directory that holds the published assets. This problem can be
114
     * avoided altogether by 'requesting' in advance all the resources that are supposed to trigger a 'publish()' call,
115
     * and doing that in the application deployment phase, before system goes live. See more in the following
116
     * discussion: http://code.google.com/p/yii/issues/detail?id=2579
117
     *
118
     * @param AssetBundle $bundle the asset (file or directory) to be read.
119
     *
120
     * - only: array, list of patterns that the file paths should match if they want to be copied.
121
     *
122
     * @return array the path (directory or file path) and the URL that the asset is published as.
123
     * @throws InvalidConfigException if the asset to be published does not exist.
124
     *
125
     */
126 8
    public function publish(AssetManager $am, AssetBundle $bundle): array
127
    {
128 8
        $this->checkBasePath($am, $bundle->basePath);
129 8
        $this->checkBaseUrl($am, $bundle->baseUrl);
130
131 8
        if (isset($this->published[$bundle->sourcePath])) {
132 1
            return $this->published[$bundle->sourcePath];
133
        }
134
135 8
        if (!file_exists($am->getAliases()->get($bundle->sourcePath))) {
136 1
            throw new InvalidConfigException("The sourcePath to be published does not exist: $bundle->sourcePath");
137
        }
138
139 7
        return $this->published[$bundle->sourcePath] = $this->publishDirectory(
140 7
            $am,
141 7
            $bundle->sourcePath,
0 ignored issues
show
Bug introduced by
It seems like $bundle->sourcePath can also be of type null; however, parameter $src of Yiisoft\Assets\AssetPublisher::publishDirectory() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

141
            /** @scrutinizer ignore-type */ $bundle->sourcePath,
Loading history...
142 7
            $bundle->publishOptions
143
        );
144
    }
145
146
    /**
147
     * Returns the published path of a file path.
148
     *
149
     * This method does not perform any publishing. It merely tells you if the file or directory is published, where it
150
     * will go.
151
     *
152
     * @param string $sourcePath directory or file path being published.
153
     *
154
     * @return string|null string the published file path. Null if the file or directory does not exist
155
     */
156 2
    public function getPublishedPath(string $sourcePath): ?string
157
    {
158 2
        if (isset($this->published[$sourcePath])) {
159 1
            return $this->published[$sourcePath][0];
160
        }
161
162 1
        return null;
163
    }
164
165
    /**
166
     * Returns the URL of a published file path.
167
     *
168
     * This method does not perform any publishing. It merely tells you if the file path is published, what the URL will
169
     * be to access it.
170
     *
171
     * @param string $sourcePath directory or file path being published
172
     *
173
     * @return string|null string the published URL for the file or directory. Null if the file or directory does not
174
     * exist.
175
     */
176 2
    public function getPublishedUrl(string $sourcePath): ?string
177
    {
178 2
        if (isset($this->published[$sourcePath])) {
179 1
            return $this->published[$sourcePath][1];
180
        }
181
182 1
        return null;
183
    }
184
185
    /**
186
     * Registers the CSS and JS files with the given view.
187
     *
188
     * @param AssetBundle $bundle the asset files are to be registered in the view.
189
     *
190
     * @return void
191
     */
192 20
    public function registerAssetFiles(AssetManager $am, AssetBundle $bundle): void
193
    {
194 20
        foreach ($bundle->js as $js) {
195 20
            if (\is_array($js)) {
196 2
                $file = array_shift($js);
197 2
                $options = array_merge($bundle->jsOptions, $js);
198 2
                $am->registerJsFile($this->getAssetUrl($am, $bundle, $file), $options);
199 19
            } elseif ($js !== null) {
200 19
                $am->registerJsFile($this->getAssetUrl($am, $bundle, $js), $bundle->jsOptions);
201
            }
202
        }
203
204 19
        foreach ($bundle->css as $css) {
205 14
            if (\is_array($css)) {
206 1
                $file = array_shift($css);
207 1
                $options = array_merge($bundle->cssOptions, $css);
208 1
                $am->registerCssFile($this->getAssetUrl($am, $bundle, $file), $options);
209 14
            } elseif ($css !== null) {
210 14
                $am->registerCssFile($this->getAssetUrl($am, $bundle, $css), $bundle->cssOptions);
211
            }
212
        }
213 19
    }
214
215 29
    private function checkBasePath(AssetManager $am, ?string $basePath): void
216
    {
217 29
        if (empty($basePath) && empty($am->getBasePath())) {
218 1
            throw new InvalidConfigException(
219
                'basePath must be set in AssetManager->setBasePath($path) or ' .
220 1
                'AssetBundle property public ?string $basePath = $path'
221
            );
222
        }
223
224 28
        if (empty($basePath)) {
225 1
            $this->basePath = $am->getBasePath();
226
        } else {
227 27
            $this->basePath = $am->getAliases()->get($basePath);
0 ignored issues
show
Documentation Bug introduced by
It seems like $am->getAliases()->get($basePath) can also be of type boolean. However, the property $basePath is declared as type null|string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
228
        }
229 28
    }
230
231 28
    private function checkBaseUrl(AssetManager $am, ?string $baseUrl): void
232
    {
233 28
        if (empty($baseUrl) && empty($am->getBaseUrl())) {
234 1
            throw new InvalidConfigException(
235
                'baseUrl must be set in AssetManager->setBaseUrl($path) or ' .
236 1
                'AssetBundle property public ?string $baseUrl = $path'
237
            );
238
        }
239
240 27
        if (empty($baseUrl)) {
241
            $this->baseUrl = $am->getBaseUrl();
242
        } else {
243 27
            $this->baseUrl = $am->getAliases()->get($baseUrl);
0 ignored issues
show
Documentation Bug introduced by
It seems like $am->getAliases()->get($baseUrl) can also be of type boolean. However, the property $baseUrl is declared as type null|string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
244
        }
245 27
    }
246
247
    /**
248
     * Generate a CRC32 hash for the directory path. Collisions are higher than MD5 but generates a much smaller hash
249
     * string.
250
     *
251
     * @param string $path string to be hashed.
252
     *
253
     * @return string hashed string.
254
     */
255 7
    private function hash(AssetManager $am, string $path): string
256
    {
257 7
        if (\is_callable($am->getHashCallback())) {
258 1
            return \call_user_func($am->getHashCallback(), $path);
259
        }
260
261 6
        $path = (is_file($path) ? \dirname($path) : $path) . @filemtime($path);
262
263 6
        return sprintf('%x', crc32($path . '|' . $am->getLinkAssets()));
264
    }
265
266
    /**
267
     * Publishes a directory.
268
     *
269
     * @param string $src the asset directory to be published
270
     * @param array $options the options to be applied when publishing a directory. The following options are
271
     * supported:
272
     *
273
     * - only: patterns that the file paths should match if they want to be copied.
274
     *
275
     * @return array the path directory and the URL that the asset is published as.
276
     *
277
     * @throws \Exception if the asset to be published does not exist.
278
     */
279 7
    private function publishDirectory(AssetManager $am, string $src, array $options): array
280
    {
281 7
        $src = $am->getAliases()->get($src);
282 7
        $dir = $this->hash($am, $src);
0 ignored issues
show
Bug introduced by
It seems like $src can also be of type boolean; however, parameter $path of Yiisoft\Assets\AssetPublisher::hash() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

282
        $dir = $this->hash($am, /** @scrutinizer ignore-type */ $src);
Loading history...
283 7
        $dstDir = $this->basePath . '/' . $dir;
284
285 7
        if ($am->getLinkAssets()) {
286
            if (!is_dir($dstDir)) {
287
                FileHelper::createDirectory(\dirname($dstDir), $am->getDirMode());
288
                try { // fix #6226 symlinking multi threaded
289
                    symlink($src, $dstDir);
290
                } catch (\Exception $e) {
291
                    if (!is_dir($dstDir)) {
292
                        throw $e;
293
                    }
294
                }
295
            }
296 7
        } elseif (!empty($options['forceCopy']) ||
297 7
            ($am->getForceCopy() && !isset($options['forceCopy'])) || !is_dir($dstDir)) {
298 7
            $opts = array_merge(
299 7
                $options,
300
                [
301 7
                    'dirMode' => $am->getDirMode(),
302 7
                    'fileMode' => $am->getFileMode(),
303
                    'copyEmptyDirectories' => false,
304
                ]
305
            );
306
307 7
            FileHelper::copyDirectory($src, $dstDir, $opts);
0 ignored issues
show
Bug introduced by
It seems like $src can also be of type boolean; however, parameter $source of Yiisoft\Files\FileHelper::copyDirectory() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

307
            FileHelper::copyDirectory(/** @scrutinizer ignore-type */ $src, $dstDir, $opts);
Loading history...
308
        }
309
310 7
        return [$dstDir, $this->baseUrl . '/' . $dir];
311
    }
312
}
313