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