• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nette / utils / 20431395313

22 Dec 2025 12:06PM UTC coverage: 93.164% (+71.8%) from 21.324%
20431395313

push

github

dg
Html::addText() accepts int|null for back compatibility [Closes #332][Closes #333]

1 of 1 new or added line in 1 file covered. (100.0%)

140 existing lines in 15 files now uncovered.

2058 of 2209 relevant lines covered (93.16%)

0.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.32
/src/Utils/FileSystem.php
1
<?php
2

3
/**
4
 * This file is part of the Nette Framework (https://nette.org)
5
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6
 */
7

8
declare(strict_types=1);
9

10
namespace Nette\Utils;
11

12
use Nette;
13
use function array_pop, chmod, decoct, dirname, end, fclose, file_exists, file_get_contents, file_put_contents, fopen, implode, is_dir, is_file, is_link, mkdir, preg_match, preg_split, realpath, rename, rmdir, rtrim, sprintf, str_replace, stream_copy_to_stream, stream_is_local, strtr;
14
use const DIRECTORY_SEPARATOR;
15

16

17
/**
18
 * File system tool.
19
 */
20
final class FileSystem
21
{
22
        /**
23
         * Creates a directory if it does not exist, including parent directories.
24
         * @throws Nette\IOException  on error occurred
25
         */
26
        public static function createDir(string $dir, int $mode = 0o777): void
1✔
27
        {
28
                if (!is_dir($dir) && !@mkdir($dir, $mode, recursive: true) && !is_dir($dir)) { // @ - dir may already exist
1✔
29
                        throw new Nette\IOException(sprintf(
1✔
30
                                "Unable to create directory '%s' with mode %s. %s",
1✔
31
                                self::normalizePath($dir),
1✔
32
                                decoct($mode),
1✔
33
                                Helpers::getLastError(),
1✔
34
                        ));
35
                }
36
        }
1✔
37

38

39
        /**
40
         * Copies a file or an entire directory. Overwrites existing files and directories by default.
41
         * @throws Nette\IOException  on error occurred
42
         * @throws Nette\InvalidStateException  if $overwrite is set to false and destination already exists
43
         */
44
        public static function copy(string $origin, string $target, bool $overwrite = true): void
1✔
45
        {
46
                if (stream_is_local($origin) && !file_exists($origin)) {
1✔
47
                        throw new Nette\IOException(sprintf("File or directory '%s' not found.", self::normalizePath($origin)));
1✔
48

49
                } elseif (!$overwrite && file_exists($target)) {
1✔
50
                        throw new Nette\InvalidStateException(sprintf("File or directory '%s' already exists.", self::normalizePath($target)));
1✔
51

52
                } elseif (is_dir($origin)) {
1✔
53
                        static::createDir($target);
1✔
54
                        foreach (new \FilesystemIterator($target) as $item) {
1✔
55
                                static::delete($item->getPathname());
1✔
56
                        }
57

58
                        foreach ($iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($origin, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST) as $item) {
1✔
59
                                if ($item->isDir()) {
1✔
UNCOV
60
                                        static::createDir($target . '/' . $iterator->getSubPathName());
×
61
                                } else {
62
                                        static::copy($item->getPathname(), $target . '/' . $iterator->getSubPathName());
1✔
63
                                }
64
                        }
65
                } else {
66
                        static::createDir(dirname($target));
1✔
67
                        if (@stream_copy_to_stream(static::open($origin, 'rb'), static::open($target, 'wb')) === false) { // @ is escalated to exception
1✔
UNCOV
68
                                throw new Nette\IOException(sprintf(
×
UNCOV
69
                                        "Unable to copy file '%s' to '%s'. %s",
×
UNCOV
70
                                        self::normalizePath($origin),
×
UNCOV
71
                                        self::normalizePath($target),
×
UNCOV
72
                                        Helpers::getLastError(),
×
73
                                ));
74
                        }
75
                }
76
        }
1✔
77

78

79
        /**
80
         * Opens file and returns resource.
81
         * @return resource
82
         * @throws Nette\IOException  on error occurred
83
         */
84
        public static function open(string $path, string $mode)
1✔
85
        {
86
                $f = @fopen($path, $mode); // @ is escalated to exception
1✔
87
                if (!$f) {
1✔
88
                        throw new Nette\IOException(sprintf(
1✔
89
                                "Unable to open file '%s'. %s",
1✔
90
                                self::normalizePath($path),
1✔
91
                                Helpers::getLastError(),
1✔
92
                        ));
93
                }
94
                return $f;
1✔
95
        }
96

97

98
        /**
99
         * Deletes a file or an entire directory if exists. If the directory is not empty, it deletes its contents first.
100
         * @throws Nette\IOException  on error occurred
101
         */
102
        public static function delete(string $path): void
1✔
103
        {
104
                if (is_file($path) || is_link($path)) {
1✔
105
                        $func = DIRECTORY_SEPARATOR === '\\' && is_dir($path) ? 'rmdir' : 'unlink';
1✔
106
                        if (!@$func($path)) { // @ is escalated to exception
1✔
UNCOV
107
                                throw new Nette\IOException(sprintf(
×
UNCOV
108
                                        "Unable to delete '%s'. %s",
×
UNCOV
109
                                        self::normalizePath($path),
×
UNCOV
110
                                        Helpers::getLastError(),
×
111
                                ));
112
                        }
113
                } elseif (is_dir($path)) {
1✔
114
                        foreach (new \FilesystemIterator($path) as $item) {
1✔
115
                                static::delete($item->getPathname());
1✔
116
                        }
117

118
                        if (!@rmdir($path)) { // @ is escalated to exception
1✔
UNCOV
119
                                throw new Nette\IOException(sprintf(
×
UNCOV
120
                                        "Unable to delete directory '%s'. %s",
×
UNCOV
121
                                        self::normalizePath($path),
×
UNCOV
122
                                        Helpers::getLastError(),
×
123
                                ));
124
                        }
125
                }
126
        }
1✔
127

128

129
        /**
130
         * Renames or moves a file or a directory. Overwrites existing files and directories by default.
131
         * @throws Nette\IOException  on error occurred
132
         * @throws Nette\InvalidStateException  if $overwrite is set to false and destination already exists
133
         */
134
        public static function rename(string $origin, string $target, bool $overwrite = true): void
1✔
135
        {
136
                if (!$overwrite && file_exists($target)) {
1✔
137
                        throw new Nette\InvalidStateException(sprintf("File or directory '%s' already exists.", self::normalizePath($target)));
1✔
138

139
                } elseif (!file_exists($origin)) {
1✔
140
                        throw new Nette\IOException(sprintf("File or directory '%s' not found.", self::normalizePath($origin)));
1✔
141

142
                } else {
143
                        static::createDir(dirname($target));
1✔
144
                        if (realpath($origin) !== realpath($target)) {
1✔
145
                                static::delete($target);
1✔
146
                        }
147

148
                        if (!@rename($origin, $target)) { // @ is escalated to exception
1✔
UNCOV
149
                                throw new Nette\IOException(sprintf(
×
UNCOV
150
                                        "Unable to rename file or directory '%s' to '%s'. %s",
×
UNCOV
151
                                        self::normalizePath($origin),
×
UNCOV
152
                                        self::normalizePath($target),
×
UNCOV
153
                                        Helpers::getLastError(),
×
154
                                ));
155
                        }
156
                }
157
        }
1✔
158

159

160
        /**
161
         * Reads the content of a file.
162
         * @throws Nette\IOException  on error occurred
163
         */
164
        public static function read(string $file): string
1✔
165
        {
166
                $content = @file_get_contents($file); // @ is escalated to exception
1✔
167
                if ($content === false) {
1✔
168
                        throw new Nette\IOException(sprintf(
1✔
169
                                "Unable to read file '%s'. %s",
1✔
170
                                self::normalizePath($file),
1✔
171
                                Helpers::getLastError(),
1✔
172
                        ));
173
                }
174

175
                return $content;
1✔
176
        }
177

178

179
        /**
180
         * Reads the file content line by line. Because it reads continuously as we iterate over the lines,
181
         * it is possible to read files larger than the available memory.
182
         * @return \Generator<int, string>
183
         * @throws Nette\IOException  on error occurred
184
         */
185
        public static function readLines(string $file, bool $stripNewLines = true): \Generator
1✔
186
        {
187
                return (function ($f) use ($file, $stripNewLines) {
1✔
188
                        $counter = 0;
1✔
189
                        do {
190
                                $line = Callback::invokeSafe('fgets', [$f], fn($error) => throw new Nette\IOException(sprintf(
1✔
191
                                        "Unable to read file '%s'. %s",
192
                                        self::normalizePath($file),
193
                                        $error,
194
                                )));
1✔
195
                                if ($line === false) {
1✔
196
                                        fclose($f);
1✔
197
                                        break;
1✔
198
                                }
199
                                if ($stripNewLines) {
1✔
200
                                        $line = rtrim($line, "\r\n");
1✔
201
                                }
202

203
                                yield $counter++ => $line;
1✔
204

205
                        } while (true);
1✔
206
                })(static::open($file, 'r'));
1✔
207
        }
208

209

210
        /**
211
         * Writes the string to a file.
212
         * @throws Nette\IOException  on error occurred
213
         */
214
        public static function write(string $file, string $content, ?int $mode = 0o666): void
1✔
215
        {
216
                static::createDir(dirname($file));
1✔
217
                if (@file_put_contents($file, $content) === false) { // @ is escalated to exception
1✔
UNCOV
218
                        throw new Nette\IOException(sprintf(
×
UNCOV
219
                                "Unable to write file '%s'. %s",
×
UNCOV
220
                                self::normalizePath($file),
×
UNCOV
221
                                Helpers::getLastError(),
×
222
                        ));
223
                }
224

225
                if ($mode !== null && !@chmod($file, $mode)) { // @ is escalated to exception
1✔
UNCOV
226
                        throw new Nette\IOException(sprintf(
×
UNCOV
227
                                "Unable to chmod file '%s' to mode %s. %s",
×
UNCOV
228
                                self::normalizePath($file),
×
UNCOV
229
                                decoct($mode),
×
UNCOV
230
                                Helpers::getLastError(),
×
231
                        ));
232
                }
233
        }
1✔
234

235

236
        /**
237
         * Sets file permissions to `$fileMode` or directory permissions to `$dirMode`.
238
         * Recursively traverses and sets permissions on the entire contents of the directory as well.
239
         * @throws Nette\IOException  on error occurred
240
         */
241
        public static function makeWritable(string $path, int $dirMode = 0o777, int $fileMode = 0o666): void
1✔
242
        {
243
                if (is_file($path)) {
1✔
244
                        if (!@chmod($path, $fileMode)) { // @ is escalated to exception
1✔
UNCOV
245
                                throw new Nette\IOException(sprintf(
×
UNCOV
246
                                        "Unable to chmod file '%s' to mode %s. %s",
×
UNCOV
247
                                        self::normalizePath($path),
×
UNCOV
248
                                        decoct($fileMode),
×
UNCOV
249
                                        Helpers::getLastError(),
×
250
                                ));
251
                        }
252
                } elseif (is_dir($path)) {
1✔
253
                        foreach (new \FilesystemIterator($path) as $item) {
1✔
254
                                static::makeWritable($item->getPathname(), $dirMode, $fileMode);
1✔
255
                        }
256

257
                        if (!@chmod($path, $dirMode)) { // @ is escalated to exception
1✔
UNCOV
258
                                throw new Nette\IOException(sprintf(
×
UNCOV
259
                                        "Unable to chmod directory '%s' to mode %s. %s",
×
UNCOV
260
                                        self::normalizePath($path),
×
UNCOV
261
                                        decoct($dirMode),
×
UNCOV
262
                                        Helpers::getLastError(),
×
263
                                ));
264
                        }
265
                } else {
266
                        throw new Nette\IOException(sprintf("File or directory '%s' not found.", self::normalizePath($path)));
1✔
267
                }
268
        }
1✔
269

270

271
        /**
272
         * Determines if the path is absolute.
273
         */
274
        public static function isAbsolute(string $path): bool
1✔
275
        {
276
                return (bool) preg_match('#([a-z]:)?[/\\\]|[a-z][a-z0-9+.-]*://#Ai', $path);
1✔
277
        }
278

279

280
        /**
281
         * Normalizes `..` and `.` and directory separators in path.
282
         */
283
        public static function normalizePath(string $path): string
1✔
284
        {
285
                $parts = $path === '' ? [] : preg_split('~[/\\\]+~', $path);
1✔
286
                $res = [];
1✔
287
                foreach ($parts as $part) {
1✔
288
                        if ($part === '..' && $res && end($res) !== '..' && end($res) !== '') {
1✔
289
                                array_pop($res);
1✔
290
                        } elseif ($part !== '.') {
1✔
291
                                $res[] = $part;
1✔
292
                        }
293
                }
294

295
                return $res === ['']
1✔
296
                        ? DIRECTORY_SEPARATOR
1✔
297
                        : implode(DIRECTORY_SEPARATOR, $res);
1✔
298
        }
299

300

301
        /**
302
         * Joins all segments of the path and normalizes the result.
303
         */
304
        public static function joinPaths(string ...$paths): string
1✔
305
        {
306
                return self::normalizePath(implode('/', $paths));
1✔
307
        }
308

309

310
        /**
311
         * Resolves a path against a base path. If the path is absolute, returns it directly, if it's relative, joins it with the base path.
312
         */
313
        public static function resolvePath(string $basePath, string $path): string
1✔
314
        {
315
                return match (true) {
316
                        self::isAbsolute($path) => self::platformSlashes($path),
1✔
317
                        $path === '' => self::platformSlashes($basePath),
1✔
318
                        default => self::joinPaths($basePath, $path),
1✔
319
                };
320
        }
321

322

323
        /**
324
         * Converts backslashes to slashes.
325
         */
326
        public static function unixSlashes(string $path): string
1✔
327
        {
328
                return strtr($path, '\\', '/');
1✔
329
        }
330

331

332
        /**
333
         * Converts slashes to platform-specific directory separators.
334
         */
335
        public static function platformSlashes(string $path): string
1✔
336
        {
337
                return DIRECTORY_SEPARATOR === '/'
1✔
338
                        ? strtr($path, '\\', '/')
1✔
339
                        : str_replace(':\\\\', '://', strtr($path, '/', '\\')); // protocol://
1✔
340
        }
341
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc