Files   F
last analyzed

Complexity

Total Complexity 63

Size/Duplication

Total Lines 353
Duplicated Lines 0 %

Test Coverage

Coverage 98.66%

Importance

Changes 0
Metric Value
wmc 63
eloc 128
dl 0
loc 353
ccs 147
cts 149
cp 0.9866
rs 3.36
c 0
b 0
f 0

25 Methods

Rating   Name   Duplication   Size   Complexity  
A append() 0 7 1
A copy() 0 7 2
A exists() 0 3 1
A move() 0 7 2
A getPermissions() 0 7 2
A delete() 0 12 2
A read() 0 7 2
A isDirectory() 0 3 1
A getFiles() 0 14 3
B ensureDirectory() 0 36 7
A touch() 0 7 2
A tempFilename() 0 16 3
A extension() 0 3 1
A isFile() 0 3 1
A __destruct() 0 4 2
A time() 0 7 2
A setPermissions() 0 8 3
A __construct() 0 3 1
A size() 0 7 2
A md5() 0 7 2
A deleteDirectory() 0 24 5
A write() 0 34 6
A normalizePath() 0 12 5
A filesIterator() 0 6 1
A relativePath() 0 28 4

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Spiral\Files;
6
7
use Spiral\Files\Exception\FileNotFoundException;
8
use Spiral\Files\Exception\FilesException;
9
use Spiral\Files\Exception\WriteErrorException;
10
11
/**
12
 * Default abstraction for file management operations.
13
 */
14
final class Files implements FilesInterface
15
{
16
    /**
17
     * Default file mode for this manager.
18
     */
19
    public const DEFAULT_FILE_MODE = self::READONLY;
20
21
    /**
22
     * Files to be removed when component destructed.
23
     */
24
    private array $destructFiles = [];
25
26
    /**
27
     * FileManager constructor.
28
     */
29 525
    public function __construct()
30
    {
31 525
        \register_shutdown_function([$this, '__destruct']);
32
    }
33
34
    /**
35
     * Destruct every temporary file.
36
     */
37 1
    public function __destruct()
38
    {
39 1
        foreach ($this->destructFiles as $filename) {
40 1
            $this->delete($filename);
41
        }
42
    }
43
44
    /**
45
     * @param bool $recursivePermissions Propagate permissions on created directories.
46
     */
47 460
    public function ensureDirectory(
48
        string $directory,
49
        int $mode = null,
50
        bool $recursivePermissions = true
51
    ): bool {
52 460
        if (empty($mode)) {
53 8
            $mode = self::DEFAULT_FILE_MODE;
54
        }
55
56
        //Directories always executable
57 460
        $mode |= 0o111;
58 460
        if (\is_dir($directory)) {
59
            //Exists :(
60 456
            return $this->setPermissions($directory, $mode);
61
        }
62
63 349
        if (!$recursivePermissions) {
64 1
            return \mkdir($directory, $mode, true);
65
        }
66
67 348
        $directoryChain = [\basename($directory)];
68
69 348
        $baseDirectory = $directory;
70 348
        while (!\is_dir($baseDirectory = \dirname($baseDirectory))) {
71 329
            $directoryChain[] = \basename($baseDirectory);
72
        }
73
74 348
        foreach (\array_reverse($directoryChain) as $dir) {
75 348
            if (!mkdir($baseDirectory = \sprintf('%s/%s', $baseDirectory, $dir))) {
76
                return false;
77
            }
78
79 348
            \chmod($baseDirectory, $mode);
80
        }
81
82 348
        return true;
83
    }
84
85 43
    public function read(string $filename): string
86
    {
87 43
        if (!$this->exists($filename)) {
88 1
            throw new FileNotFoundException($filename);
89
        }
90
91 42
        return \file_get_contents($filename);
92
    }
93
94
    /**
95
     * @param bool $append To append data at the end of existed file.
96
     */
97 432
    public function write(
98
        string $filename,
99
        string $data,
100
        int $mode = null,
101
        bool $ensureDirectory = false,
102
        bool $append = false
103
    ): bool {
104 432
        $mode ??= self::DEFAULT_FILE_MODE;
105
106
        try {
107 432
            if ($ensureDirectory) {
108 413
                $this->ensureDirectory(\dirname($filename), $mode);
109
            }
110
111 432
            if ($this->exists($filename)) {
112
                //Forcing mode for existed file
113 109
                $this->setPermissions($filename, $mode);
114
            }
115
116 432
            $result = \file_put_contents(
117 432
                $filename,
118 432
                $data,
119 432
                $append ? FILE_APPEND | LOCK_EX : LOCK_EX
120 432
            );
121
122 431
            if ($result !== false) {
123
                //Forcing mode after file creation
124 431
                $this->setPermissions($filename, $mode);
125
            }
126 1
        } catch (\Exception $e) {
127 1
            throw new WriteErrorException($e->getMessage(), (int) $e->getCode(), $e);
128
        }
129
130 431
        return $result !== false;
131
    }
132
133 3
    public function append(
134
        string $filename,
135
        string $data,
136
        int $mode = null,
137
        bool $ensureDirectory = false
138
    ): bool {
139 3
        return $this->write($filename, $data, $mode, $ensureDirectory, true);
140
    }
141
142 380
    public function delete(string $filename): bool
143
    {
144 380
        if ($this->exists($filename)) {
145 379
            $result = \unlink($filename);
146
147
            //Wiping out changes in local file cache
148 379
            \clearstatcache(false, $filename);
149
150 379
            return $result;
151
        }
152
153 1
        return false;
154
    }
155
156
    /**
157
     * @see http://stackoverflow.com/questions/3349753/delete-directory-with-files-in-it
158
     *
159
     * @throws FilesException
160
     */
161 372
    public function deleteDirectory(string $directory, bool $contentOnly = false): bool
162
    {
163 372
        if (!$this->isDirectory($directory)) {
164 1
            throw new FilesException(\sprintf('Undefined or invalid directory %s', $directory));
165
        }
166
167 372
        $files = new \RecursiveIteratorIterator(
168 372
            new \RecursiveDirectoryIterator($directory, \RecursiveDirectoryIterator::SKIP_DOTS),
169 372
            \RecursiveIteratorIterator::CHILD_FIRST
170 372
        );
171
172 372
        foreach ($files as $file) {
173 349
            if ($file->isDir()) {
174 332
                \rmdir($file->getRealPath());
175
            } else {
176 339
                $this->delete($file->getRealPath());
177
            }
178
        }
179
180 372
        if (!$contentOnly) {
181 324
            return \rmdir($directory);
182
        }
183
184 49
        return true;
185
    }
186
187 2
    public function move(string $filename, string $destination): bool
188
    {
189 2
        if (!$this->exists($filename)) {
190 1
            throw new FileNotFoundException($filename);
191
        }
192
193 1
        return \rename($filename, $destination);
194
    }
195
196 9
    public function copy(string $filename, string $destination): bool
197
    {
198 9
        if (!$this->exists($filename)) {
199 1
            throw new FileNotFoundException($filename);
200
        }
201
202 8
        return \copy($filename, $destination);
203
    }
204
205 5
    public function touch(string $filename, int $mode = null): bool
206
    {
207 5
        if (!\touch($filename)) {
208
            return false;
209
        }
210
211 5
        return $this->setPermissions($filename, $mode ?? self::DEFAULT_FILE_MODE);
212
    }
213
214 502
    public function exists(string $filename): bool
215
    {
216 502
        return \file_exists($filename);
217
    }
218
219 2
    public function size(string $filename): int
220
    {
221 2
        if (!$this->exists($filename)) {
222 1
            throw new FileNotFoundException($filename);
223
        }
224
225 1
        return \filesize($filename);
226
    }
227
228 3
    public function extension(string $filename): string
229
    {
230 3
        return \strtolower(\pathinfo($filename, PATHINFO_EXTENSION));
0 ignored issues
show
Bug introduced by
It seems like pathinfo($filename, Spir...les\PATHINFO_EXTENSION) can also be of type array; however, parameter $string of strtolower() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

230
        return \strtolower(/** @scrutinizer ignore-type */ \pathinfo($filename, PATHINFO_EXTENSION));
Loading history...
231
    }
232
233 9
    public function md5(string $filename): string
234
    {
235 9
        if (!$this->exists($filename)) {
236 1
            throw new FileNotFoundException($filename);
237
        }
238
239 8
        return \md5_file($filename);
240
    }
241
242 2
    public function time(string $filename): int
243
    {
244 2
        if (!$this->exists($filename)) {
245 1
            throw new FileNotFoundException($filename);
246
        }
247
248 1
        return \filemtime($filename);
249
    }
250
251 376
    public function isDirectory(string $filename): bool
252
    {
253 376
        return \is_dir($filename);
254
    }
255
256 16
    public function isFile(string $filename): bool
257
    {
258 16
        return \is_file($filename);
259
    }
260
261 458
    public function getPermissions(string $filename): int
262
    {
263 458
        if (!$this->exists($filename)) {
264 1
            throw new FileNotFoundException($filename);
265
        }
266
267 458
        return \fileperms($filename) & 0777;
268
    }
269
270 458
    public function setPermissions(string $filename, int $mode): bool
271
    {
272 458
        if (\is_dir($filename)) {
273
            //Directories must always be executable (i.e. 664 for dir => 775)
274 456
            $mode |= 0111;
275
        }
276
277 458
        return $this->getPermissions($filename) === $mode || \chmod($filename, $mode);
278
    }
279
280 22
    public function getFiles(string $location, string $pattern = null): array
281
    {
282 22
        $result = [];
283 22
        foreach ($this->filesIterator($location, $pattern) as $filename) {
284 21
            if ($this->isDirectory($filename->getPathname())) {
285 17
                $result = \array_merge($result, $this->getFiles($filename . DIRECTORY_SEPARATOR));
286
287 17
                continue;
288
            }
289
290 21
            $result[] = $this->normalizePath((string)$filename);
291
        }
292
293 22
        return $result;
294
    }
295
296 4
    public function tempFilename(string $extension = '', string $location = null): string
297
    {
298 4
        if (empty($location)) {
299 3
            $location = \sys_get_temp_dir();
300
        }
301
302 4
        $filename = \tempnam($location, 'spiral');
303
304 4
        if (!empty($extension)) {
305 3
            $old = $filename;
306 3
            $filename = \sprintf('%s.%s', $filename, $extension);
307 3
            \rename($old, $filename);
308 3
            $this->destructFiles[] = $filename;
309
        }
310
311 4
        return $filename;
312
    }
313
314 72
    public function normalizePath(string $path, bool $asDirectory = false): string
315
    {
316 72
        $isUnc = \str_starts_with($path, '\\\\') || \str_starts_with($path, '//');
317 72
        if ($isUnc) {
318 1
            $leadingSlashes = \substr($path, 0, 2);
319 1
            $path = \substr($path, 2);
320
        }
321
322 72
        $path = \str_replace(['//', '\\'], '/', $path);
323
324
        //Potentially open links and ../ type directories?
325 72
        return ($isUnc ? $leadingSlashes : '') . \rtrim($path, '/') . ($asDirectory ? '/' : '');
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $leadingSlashes does not seem to be defined for all execution paths leading up to this point.
Loading history...
326
    }
327
328
    /**
329
     * @link http://stackoverflow.com/questions/2637945/getting-relative-path-from-absolute-path-in-php
330
     */
331 16
    public function relativePath(string $path, string $from): string
332
    {
333 16
        $path = $this->normalizePath($path);
334 16
        $from = $this->normalizePath($from);
335
336 16
        $from = \explode('/', $from);
337 16
        $path = \explode('/', $path);
338 16
        $relative = $path;
339
340 16
        foreach ($from as $depth => $dir) {
341
            //Find first non-matching dir
342 16
            if ($dir === $path[$depth]) {
343
                //Ignore this directory
344 16
                \array_shift($relative);
345
            } else {
346
                //Get number of remaining dirs to $from
347 1
                $remaining = \count($from) - $depth;
348 1
                if ($remaining > 1) {
349
                    //Add traversals up to first matching directory
350 1
                    $padLength = (\count($relative) + $remaining - 1) * -1;
351 1
                    $relative = \array_pad($relative, $padLength, '..');
352 1
                    break;
353
                }
354 1
                $relative[0] = './' . $relative[0];
355
            }
356
        }
357
358 16
        return \implode('/', $relative);
359
    }
360
361 22
    private function filesIterator(string $location, string $pattern = null): \GlobIterator
362
    {
363 22
        $pattern ??= '*';
364 22
        $regexp = \rtrim($location, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . \ltrim($pattern, DIRECTORY_SEPARATOR);
365
366 22
        return new \GlobIterator($regexp);
367
    }
368
}
369