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