Passed
Pull Request — master (#22)
by Sergei
25:38 queued 10:38
created

FileHelper   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 324
Duplicated Lines 0 %

Test Coverage

Coverage 92.13%

Importance

Changes 7
Bugs 1 Features 0
Metric Value
eloc 96
dl 0
loc 324
ccs 82
cts 89
cp 0.9213
rs 5.04
c 7
b 1
f 0
wmc 57

11 Methods

Rating   Name   Duplication   Size   Complexity  
A setBasePath() 0 8 2
C normalizePath() 0 31 14
A openDirectory() 0 9 2
B copyDirectory() 0 38 11
B clearDirectory() 0 16 7
A unlink() 0 13 4
A chmod() 0 9 2
A createDirectory() 0 19 5
A assertNotSelfDirectory() 0 4 3
A setDestination() 0 10 4
A removeDirectory() 0 12 3

How to fix   Complexity   

Complex Class

Complex classes like FileHelper 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 FileHelper, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Files;
6
7
use Exception;
8
use InvalidArgumentException;
9
use RuntimeException;
10
11
/**
12
 * FileHelper provides useful methods to manage files and directories
13
 */
14
class FileHelper
15
{
16
    /**
17
     * Creates a new directory.
18
     *
19
     * This method is similar to the PHP `mkdir()` function except that it uses `chmod()` to set the permission of the
20
     * created directory in order to avoid the impact of the `umask` setting.
21
     *
22
     * @param string $path path of the directory to be created.
23
     * @param int $mode the permission to be set for the created directory.
24
     *
25
     * @return bool whether the directory is created successfully.
26
     */
27
    public static function createDirectory(string $path, int $mode = 0775): bool
28
    {
29
        $path = static::normalizePath($path);
30
31
        try {
32
            if (!mkdir($path, $mode, true) && !is_dir($path)) {
33
                return false;
34
            }
35
        } catch (Exception $e) {
36
            if (!is_dir($path)) {
37
                throw new RuntimeException(
38
                    "Failed to create directory \"$path\": " . $e->getMessage(),
39
                    $e->getCode(),
40
                    $e
41
                );
42
            }
43
        }
44
45
        return static::chmod($path, $mode);
46
    }
47
48
    /**
49
     * Set permissions directory.
50
     *
51
     * @param string $path
52
     * @param integer $mode
53
     *
54 31
     * @return boolean|null
55
     *
56 31
     * @throws RuntimeException
57
     */
58
    private static function chmod(string $path, int $mode): ?bool
59 31
    {
60 31
        try {
61
            return chmod($path, $mode);
62 2
        } catch (Exception $e) {
63 2
            throw new RuntimeException(
64 1
                "Failed to change permissions for directory \"$path\": " . $e->getMessage(),
65 1
                $e->getCode(),
66 1
                $e
67
            );
68
        }
69
    }
70
71
    /**
72 31
     * Normalizes a file/directory path.
73
     *
74
     * The normalization does the following work:
75
     *
76
     * - Convert all directory separators into `/` (e.g. "\a/b\c" becomes "/a/b/c")
77
     * - Remove trailing directory separators (e.g. "/a/b/c/" becomes "/a/b/c")
78
     * - Turn multiple consecutive slashes into a single one (e.g. "/a///b/c" becomes "/a/b/c")
79
     * - Remove ".." and "." based on their meanings (e.g. "/a/./b/../c" becomes "/a/c")
80
     *
81
     * @param string $path the file/directory path to be normalized
82
     *
83
     * @return string the normalized file/directory path
84
     */
85 31
    public static function normalizePath(string $path): string
86
    {
87
        $isWindowsShare = strpos($path, '\\\\') === 0;
88 31
89
        if ($isWindowsShare) {
90
            $path = substr($path, 2);
91
        }
92
93
        $path = rtrim(strtr($path, '/\\', '//'), '/');
94
95
        if (strpos('/' . $path, '/.') === false && strpos($path, '//') === false) {
96
            return $isWindowsShare ? "\\\\$path" : $path;
97
        }
98
99
        $parts = [];
100
101
        foreach (explode('/', $path) as $part) {
102
            if ($part === '..' && !empty($parts) && end($parts) !== '..') {
103
                array_pop($parts);
104
            } elseif ($part !== '.' && ($part !== '' || empty($parts))) {
105
                $parts[] = $part;
106
            }
107
        }
108
109
        $path = implode('/', $parts);
110
111
        if ($isWindowsShare) {
112 31
            $path = '\\\\' . $path;
113
        }
114 31
115
        return $path === '' ? '.' : $path;
116 31
    }
117 1
118
    /**
119
     * Removes a directory (and all its content) recursively.
120 31
     *
121
     * @param string $directory the directory to be deleted recursively.
122 31
     * @param array $options options for directory remove ({@see clearDirectory()}).
123 31
     *
124
     * @return void
125
     */
126 1
    public static function removeDirectory(string $directory, array $options = []): void
127
    {
128 1
        try {
129 1
            static::clearDirectory($directory, $options);
130 1
        } catch (InvalidArgumentException $e) {
131 1
            return;
132 1
        }
133
134
        if (is_link($directory)) {
135
            self::unlink($directory);
136 1
        } else {
137
            rmdir($directory);
138 1
        }
139 1
    }
140
141
    /**
142 1
     * Clear all directory content.
143
     *
144
     * @param string $directory the directory to be cleared.
145
     * @param array $options options for directory clear . Valid options are:
146
     *
147
     * - traverseSymlinks: boolean, whether symlinks to the directories should be traversed too.
148
     *   Defaults to `false`, meaning the content of the symlinked directory would not be deleted.
149
     *   Only symlink would be removed in that default case.
150
     *
151
     * @return void
152
     *
153 31
     * @throws InvalidArgumentException if unable to open directory
154
     */
155
    public static function clearDirectory(string $directory, array $options = []): void
156 31
    {
157 1
        $handle = static::openDirectory($directory);
158 1
        if (!empty($options['traverseSymlinks']) || !is_link($directory)) {
159
            while (($file = readdir($handle)) !== false) {
160
                if ($file === '.' || $file === '..') {
161 31
                    continue;
162 2
                }
163
                $path = $directory . '/' . $file;
164 31
                if (is_dir($path)) {
165
                    self::removeDirectory($path, $options);
166 31
                } else {
167
                    self::unlink($path);
168
                }
169
            }
170
            closedir($handle);
171
        }
172
    }
173
174
    /**
175
     * Removes a file or symlink in a cross-platform way.
176
     *
177
     * @param string $path
178
     *
179
     * @return bool
180
     */
181
    public static function unlink(string $path): bool
182 31
    {
183
        $isWindows = DIRECTORY_SEPARATOR === '\\';
184 31
185 31
        if (!$isWindows) {
186 31
            return unlink($path);
187 31
        }
188 31
189
        if (is_link($path) && is_dir($path)) {
190 19
            return rmdir($path);
191 19
        }
192 19
193
        return unlink($path);
194 12
    }
195
196
    /**
197 31
     * Copies a whole directory as another one.
198
     *
199 31
     * The files and sub-directories will also be copied over.
200
     *
201
     * @param string $source the source directory.
202
     * @param string $destination the destination directory.
203
     * @param array $options options for directory copy. Valid options are:
204
     *
205
     * - dirMode: integer, the permission to be set for newly copied directories. Defaults to 0775.
206
     * - fileMode:  integer, the permission to be set for newly copied files. Defaults to the current environment
207
     *   setting.
208 12
     * - filter: PathMatcher
209
     * - recursive: boolean, whether the files under the subdirectories should also be copied. Defaults to true.
210 12
     * - beforeCopy: callback, a PHP callback that is called before copying each sub-directory or file. If the callback
211
     *   returns false, the copy operation for the sub-directory or file will be cancelled. The signature of the
212 12
     *   callback should be: `function ($from, $to)`, where `$from` is the sub-directory or file to be copied from,
213 12
     *   while `$to` is the copy target.
214
     * - afterCopy: callback, a PHP callback that is called after each sub-directory or file is successfully copied.
215
     *   The signature of the callback should be: `function ($from, $to)`, where `$from` is the sub-directory or file
216
     *   copied from, while `$to` is the copy target.
217
     * - copyEmptyDirectories: boolean, whether to copy empty directories. Set this to false to avoid creating
218
     *   directories that do not contain files. This affects directories that do not contain files initially as well as
219
     *   directories that do not contain files at the target destination because files have been filtered via `only` or
220
     *   `except`. Defaults to true.
221
     *
222
     * @return void
223
     * @throws Exception
224
     *
225
     * @throws InvalidArgumentException if unable to open directory
226
     */
227
    public static function copyDirectory(string $source, string $destination, array $options = []): void
228
    {
229
        $source = static::normalizePath($source);
230
        $destination = static::normalizePath($destination);
231
232
        static::assertNotSelfDirectory($source, $destination);
233
234
        $destinationExists = static::setDestination($destination, $options);
235
236
        $handle = static::openDirectory($source);
237
238
        $options = static::setBasePath($source, $options);
239
240
        while (($file = readdir($handle)) !== false) {
241
            if ($file === '.' || $file === '..') {
242
                continue;
243
            }
244
245
            $from = $source . '/' . $file;
246
            $to = $destination . '/' . $file;
247
248
            if (is_file($from)) {
249
                if (!isset($options['filter']) || $options['filter']->match($from)) {
250
                    if (!$destinationExists) {
251
                        static::createDirectory($destination, $options['dirMode'] ?? 0775);
252
                        $destinationExists = true;
253
                    }
254
                    copy($from, $to);
255
                    if (isset($options['fileMode'])) {
256
                        static::chmod($to, $options['fileMode']);
257
                    }
258
                }
259
            } elseif (!isset($options['recursive']) || $options['recursive']) {
260
                static::copyDirectory($from, $to, $options);
261
            }
262
        }
263
264
        closedir($handle);
265
    }
266
267
    /**
268
     * Check copy it self directory.
269
     *
270
     * @param string $source
271
     * @param string $destination
272
     *
273 12
     * @throws InvalidArgumentException
274
     */
275 12
    private static function assertNotSelfDirectory(string $source, string $destination): void
276 12
    {
277
        if ($source === $destination || strpos($destination, $source . '/') === 0) {
278 12
            throw new InvalidArgumentException('Trying to copy a directory to itself or a subdirectory.');
279
        }
280 10
    }
281
282 10
    /**
283
     * Open directory handle.
284 9
     *
285
     * @param string $directory
286 9
     *
287 9
     * @return resource
288 9
     *
289
     * @throws InvalidArgumentException
290
     */
291 7
    private static function openDirectory(string $directory)
292 7
    {
293
        $handle = @opendir($directory);
294 7
295 7
        if ($handle === false) {
296 7
            throw new InvalidArgumentException("Unable to open directory: $directory");
297 2
        }
298 2
299
        return $handle;
300 7
    }
301 7
302 7
    /**
303
     * Set base path directory.
304 6
     *
305 5
     * @param string $source
306
     * @param array $options
307
     *
308
     * @return array
309
     */
310 9
    private static function setBasePath(string $source, array $options): array
311 9
    {
312
        if (!isset($options['basePath'])) {
313
            // this should be done only once
314
            $options['basePath'] = realpath($source);
315
        }
316
317
        return $options;
318
    }
319
320
    /**
321 12
     * Set destination directory.
322
     *
323 12
     * @param string $destination
324 2
     * @param array $options
325
     *
326 10
     * @return bool
327
     */
328
    private static function setDestination(string $destination, array $options): bool
329
    {
330
        $destinationExists = is_dir($destination);
331
332
        if (!$destinationExists && (!isset($options['copyEmptyDirectories']) || $options['copyEmptyDirectories'])) {
333
            static::createDirectory($destination, $options['dirMode'] ?? 0775);
334
            $destinationExists = true;
335
        }
336 31
337
        return $destinationExists;
338 31
    }
339
}
340