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

FileHelper::matchPathname()   C

Complexity

Conditions 12
Paths 72

Size

Total Lines 44
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 12.7571

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 12
eloc 22
c 1
b 0
f 0
nc 72
nop 5
dl 0
loc 44
ccs 19
cts 23
cp 0.8261
crap 12.7571
rs 6.9666

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\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