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