Passed
Push — master ( 15149f...6f649b )
by Alexander
02:11
created

FileHelper::removeLinkOrEmptyDirectory()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2.0625

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 9
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 15
ccs 6
cts 8
cp 0.75
crap 2.0625
rs 9.9666
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Files;
6
7
use FilesystemIterator;
8
use InvalidArgumentException;
9
use LogicException;
10
use RecursiveDirectoryIterator;
11
use RecursiveIteratorIterator;
12
use RuntimeException;
13
use Yiisoft\Files\PathMatcher\PathMatcherInterface;
14
15
use function array_key_exists;
16
use function filemtime;
17
use function get_debug_type;
18
use function is_file;
19
use function is_string;
20
21
/**
22
 * Provides useful methods to manage files and directories.
23
 */
24
final class FileHelper
25
{
26
    /**
27
     * Opens a file or URL.
28
     *
29
     * This method is similar to the PHP {@see fopen()} function, except that it suppresses the {@see E_WARNING}
30
     * level error and throws the {@see RuntimeException} exception if it can't open the file.
31
     *
32
     * @param string $filename The file or URL.
33
     * @param string $mode The type of access.
34
     * @param bool $useIncludePath Whether to search for a file in the include path.
35
     * @param resource|null $context The stream context or `null`.
36
     *
37
     * @throws RuntimeException If the file could not be opened.
38
     *
39
     * @return resource The file pointer resource.
40
     *
41
     * @psalm-suppress PossiblyNullArgument
42
     */
43 3
    public static function openFile(string $filename, string $mode, bool $useIncludePath = false, $context = null)
44
    {
45
        /** @psalm-suppress InvalidArgument, MixedArgumentTypeCoercion */
46 3
        set_error_handler(static function (int $errorNumber, string $errorString) use ($filename): bool {
47 1
            throw new RuntimeException(
48 1
                sprintf('Failed to open a file "%s". ', $filename) . $errorString,
49
                $errorNumber
50
            );
51
        });
52
53 3
        $filePointer = fopen($filename, $mode, $useIncludePath, $context);
54
55 2
        restore_error_handler();
56
57 2
        if ($filePointer === false) {
58
            throw new RuntimeException(sprintf('Failed to open a file "%s". ', $filename));
59
        }
60
61 2
        return $filePointer;
62
    }
63
64
    /**
65
     * Ensures directory exists and has specific permissions.
66
     *
67
     * This method is similar to the PHP {@see mkdir()} function with some differences:
68
     *
69
     * - It does not fail if directory already exists.
70
     * - It uses {@see chmod()} to set the permission of the created directory in order to avoid the impact
71
     *   of the `umask` setting.
72
     * - It throws exceptions instead of returning false and emitting {@see E_WARNING}.
73
     *
74
     * @param string $path Path of the directory to be created.
75
     * @param int $mode The permission to be set for the created directory.
76
     */
77 67
    public static function ensureDirectory(string $path, int $mode = 0775): void
78
    {
79 67
        $path = self::normalizePath($path);
80
81 67
        if (!is_dir($path)) {
82 67
            set_error_handler(static function (int $errorNumber, string $errorString) use ($path): bool {
83
                // Handle race condition.
84
                // See https://github.com/kalessil/phpinspectionsea/blob/master/docs/probable-bugs.md#mkdir-race-condition
85 1
                if (!is_dir($path)) {
86 1
                    throw new RuntimeException(
87 1
                        sprintf('Failed to create directory "%s". ', $path) . $errorString,
88
                        $errorNumber
89
                    );
90
                }
91
                return true;
92
            });
93
94 67
            mkdir($path, $mode, true);
95
96 67
            restore_error_handler();
97
        }
98
99 67
        if (!chmod($path, $mode)) {
100
            throw new RuntimeException(sprintf('Unable to set mode "%s" for "%s".', $mode, $path));
101
        }
102
    }
103
104
    /**
105
     * Normalizes a file/directory path.
106
     *
107
     * The normalization does the following work:
108
     *
109
     * - Convert all directory separators into `/` (e.g. "\a/b\c" becomes "/a/b/c")
110
     * - Remove trailing directory separators (e.g. "/a/b/c/" becomes "/a/b/c")
111
     * - Turn multiple consecutive slashes into a single one (e.g. "/a///b/c" becomes "/a/b/c")
112
     * - Remove ".." and "." based on their meanings (e.g. "/a/./b/../c" becomes "/a/c")
113
     *
114
     * @param string $path The file/directory path to be normalized.
115
     *
116
     * @return string The normalized file/directory path.
117
     */
118 67
    public static function normalizePath(string $path): string
119
    {
120 67
        $isWindowsShare = str_starts_with($path, '\\\\');
121
122 67
        if ($isWindowsShare) {
123 1
            $path = substr($path, 2);
124
        }
125
126 67
        $path = rtrim(strtr($path, '/\\', '//'), '/');
127
128 67
        if (!str_contains('/' . $path, '/.') && !str_contains($path, '//')) {
129 67
            return $isWindowsShare ? "\\\\$path" : $path;
130
        }
131
132 1
        $parts = [];
133
134 1
        foreach (explode('/', $path) as $part) {
135 1
            if ($part === '..' && !empty($parts) && end($parts) !== '..') {
136 1
                array_pop($parts);
137 1
            } elseif ($part !== '.' && ($part !== '' || empty($parts))) {
138 1
                $parts[] = $part;
139
            }
140
        }
141
142 1
        $path = implode('/', $parts);
143
144 1
        if ($isWindowsShare) {
145 1
            $path = '\\\\' . $path;
146
        }
147
148 1
        return $path === '' ? '.' : $path;
149
    }
150
151
    /**
152
     * Removes a directory (and all its content) recursively. Does nothing if directory does not exist.
153
     *
154
     * @param string $directory The directory to be deleted recursively.
155
     * @param array $options Options for directory remove. Valid options are:
156
     *
157
     * - traverseSymlinks: boolean, whether symlinks to the directories should be traversed too.
158
     *   Defaults to `false`, meaning the content of the symlinked directory would not be deleted.
159
     *   Only symlink would be removed in that default case.
160
     *
161
     * @psalm-param array{
162
     *     traverseSymlinks?:bool,
163
     * } $options
164
     *
165
     * @throw RuntimeException When unable to remove directory.
166
     */
167 67
    public static function removeDirectory(string $directory, array $options = []): void
168
    {
169 67
        if (!file_exists($directory)) {
170 1
            return;
171
        }
172
173 67
        self::clearDirectory(
174
            $directory,
175 67
            ['traverseSymlinks' => $options['traverseSymlinks'] ?? false]
176
        );
177
178 67
        self::removeLinkOrEmptyDirectory($directory);
179
    }
180
181
    /**
182
     * Clears all directory content.
183
     *
184
     * @param string $directory The directory to be cleared.
185
     * @param array $options Options for directory clear. Valid options are:
186
     *
187
     * - traverseSymlinks: boolean, whether symlinks to the directories should be traversed too.
188
     *   Defaults to `false`, meaning the content of the symlinked directory would not be deleted.
189
     *   Only symlink would be removed in that default case.
190
     * - filter: a filter to apply while deleting files. It should be an instance of {@see PathMatcherInterface}.
191
     *
192
     * @throws RuntimeException if unable to open directory.
193
     *
194
     * @psalm-param array{
195
     *     traverseSymlinks?:bool,
196
     *     filter?:PathMatcherInterface
197
     * } $options
198
     */
199 67
    public static function clearDirectory(string $directory, array $options = []): void
200
    {
201 67
        $filter = self::getFilter($options);
202 67
        $handle = self::openDirectory($directory);
203 67
        if (!empty($options['traverseSymlinks']) || !is_link($directory)) {
204 67
            while (($file = readdir($handle)) !== false) {
205 67
                if ($file === '.' || $file === '..') {
206 67
                    continue;
207
                }
208
209 48
                $path = $directory . '/' . $file;
210
211 48
                if ($filter === null || $filter->match($path)) {
212 48
                    if (is_dir($path)) {
213 47
                        self::clearDirectory($path, $options);
214 47
                        if (is_link($path) || self::isEmptyDirectory($path)) {
215 47
                            self::removeLinkOrEmptyDirectory($path);
216
                        }
217
                    } else {
218 38
                        self::unlink($path);
219
                    }
220
                }
221
            }
222 67
            closedir($handle);
223
        }
224
    }
225
226
    /**
227
     * Removes a file or symlink in a cross-platform way.
228
     *
229
     * @param string $path Path to unlink.
230
     */
231 42
    public static function unlink(string $path): void
232
    {
233
        /** @psalm-suppress InvalidArgument, MixedArgumentTypeCoercion */
234 42
        set_error_handler(static function (int $errorNumber, string $errorString) use ($path): bool {
235 1
            throw new RuntimeException(
236 1
                sprintf('Failed to unlink "%s". ', $path) . $errorString,
237
                $errorNumber
238
            );
239
        });
240
241 42
        $isWindows = DIRECTORY_SEPARATOR === '\\';
242
243 42
        if ($isWindows) {
244
            if (is_link($path)) {
245
                try {
246
                    unlink($path);
247
                } catch (RuntimeException) {
248
                    rmdir($path);
249
                }
250
            } else {
251
                if (file_exists($path) && !is_writable($path)) {
252
                    chmod($path, 0777);
253
                }
254
                unlink($path);
255
            }
256
        } else {
257 42
            unlink($path);
258
        }
259 41
        restore_error_handler();
260
    }
261
262
    /**
263
     * Tells whether the path is an empty directory.
264
     *
265
     * @param string $path Path to check for being an empty directory.
266
     *
267
     * @return bool
268
     */
269 47
    public static function isEmptyDirectory(string $path): bool
270
    {
271 47
        if (!is_dir($path)) {
272 1
            return false;
273
        }
274
275 47
        return !(new FilesystemIterator($path))->valid();
276
    }
277
278
    /**
279
     * Copies a whole directory as another one.
280
     *
281
     * The files and subdirectories will also be copied over.
282
     *
283
     * @param string $source The source directory.
284
     * @param string $destination The destination directory.
285
     * @param array $options Options for directory copy. Valid options are:
286
     *
287
     * - dirMode: integer, the permission to be set for newly copied directories. Defaults to 0775.
288
     * - fileMode: integer, the permission to be set for newly copied files. Defaults to the current environment
289
     *   setting.
290
     * - filter: a filter to apply while copying files. It should be an instance of {@see PathMatcherInterface}.
291
     * - recursive: boolean, whether the files under the subdirectories should also be copied. Defaults to true.
292
     * - beforeCopy: callback, a PHP callback that is called before copying each subdirectory or file. If the callback
293
     *   returns false, the copy operation for the subdirectory or file will be cancelled. The signature of the
294
     *   callback should be: `function ($from, $to)`, where `$from` is the subdirectory or file to be copied from,
295
     *   while `$to` is the copy target.
296
     * - afterCopy: callback, a PHP callback that is called after each subdirectory or file is successfully copied.
297
     *   The signature of the callback should be: `function ($from, $to)`, where `$from` is the subdirectory or file
298
     *   copied from, while `$to` is the copy target.
299
     * - copyEmptyDirectories: boolean, whether to copy empty directories. Set this to false to avoid creating
300
     *   directories that do not contain files. This affects directories that do not contain files initially as well as
301
     *   directories that do not contain files at the target destination because files have been filtered via `only` or
302
     *   `except`. Defaults to true.
303
     *
304
     * @throws RuntimeException if unable to open directory
305
     *
306
     * @psalm-param array{
307
     *   dirMode?: int,
308
     *   fileMode?: int,
309
     *   filter?: PathMatcherInterface,
310
     *   recursive?: bool,
311
     *   beforeCopy?: callable,
312
     *   afterCopy?: callable,
313
     *   copyEmptyDirectories?: bool,
314
     * } $options
315
     */
316 18
    public static function copyDirectory(string $source, string $destination, array $options = []): void
317
    {
318 18
        $filter = self::getFilter($options);
319 18
        $afterCopy = $options['afterCopy'] ?? null;
320 18
        $beforeCopy = $options['beforeCopy'] ?? null;
321 18
        $recursive = !array_key_exists('recursive', $options) || $options['recursive'];
322
323 18
        if (!isset($options['dirMode'])) {
324 17
            $options['dirMode'] = 0755;
325
        }
326
327 18
        $source = self::normalizePath($source);
328 18
        $destination = self::normalizePath($destination);
329 18
        $copyEmptyDirectories = !array_key_exists('copyEmptyDirectories', $options) || $options['copyEmptyDirectories'];
330
331 18
        self::assertNotSelfDirectory($source, $destination);
332
333 16
        if (self::processCallback($beforeCopy, $source, $destination) === false) {
334 1
            return;
335
        }
336
337 14
        if ($copyEmptyDirectories && !is_dir($destination)) {
338 10
            self::ensureDirectory($destination, $options['dirMode']);
339
        }
340
341 14
        $handle = self::openDirectory($source);
342
343 13
        if (!array_key_exists('basePath', $options)) {
344 13
            $options['basePath'] = realpath($source);
345
        }
346
347 13
        while (($file = readdir($handle)) !== false) {
348 13
            if ($file === '.' || $file === '..') {
349 13
                continue;
350
            }
351
352 11
            $from = $source . '/' . $file;
353 11
            $to = $destination . '/' . $file;
354
355 11
            if ($filter === null || $filter->match($from)) {
356 11
                if (is_file($from)) {
357 11
                    self::copyFile($from, $to, $options);
358 6
                } elseif ($recursive) {
359 5
                    self::copyDirectory($from, $to, $options);
360
                }
361
            }
362
        }
363
364 13
        closedir($handle);
365
366 13
        self::processCallback($afterCopy, $source, $destination);
367
    }
368
369
    /**
370
     * Copies files with some options.
371
     *
372
     * - dirMode: integer or null, the permission to be set for newly copied directories. Defaults to null.
373
     *   When null - directory will be not created
374
     * - fileMode: integer, the permission to be set for newly copied files. Defaults to the current environment
375
     *   setting.
376
     * - beforeCopy: callback, a PHP callback that is called before copying file. If the callback
377
     *   returns false, the copy operation for file will be cancelled. The signature of the
378
     *   callback should be: `function ($from, $to)`, where `$from` is the file to be copied from,
379
     *   while `$to` is the copy target.
380
     * - afterCopy: callback, a PHP callback that is called after file if successfully copied.
381
     *   The signature of the callback should be: `function ($from, $to)`, where `$from` is the file
382
     *   copied from, while `$to` is the copy target.
383
     *
384
     * @param string $source The source file
385
     * @param string $destination The destination filename
386
     * @param array $options
387
     *
388
     * @psalm-param array{
389
     *   dirMode?: int,
390
     *   fileMode?: int,
391
     *   beforeCopy?: callable,
392
     *   afterCopy?: callable,
393
     * } $options
394
     */
395 14
    public static function copyFile(string $source, string $destination, array $options = []): void
396
    {
397 14
        if (!is_file($source)) {
398 1
            throw new InvalidArgumentException('Argument $source must be an existing file.');
399
        }
400
401 13
        $dirname = dirname($destination);
402 13
        $dirMode = $options['dirMode'] ?? 0755;
403 13
        $fileMode = $options['fileMode'] ?? null;
404 13
        $afterCopy = $options['afterCopy'] ?? null;
405 13
        $beforeCopy = $options['beforeCopy'] ?? null;
406
407 13
        if (self::processCallback($beforeCopy, $source, $destination) === false) {
408 1
            return;
409
        }
410
411 13
        if (!is_dir($dirname)) {
412 4
            self::ensureDirectory($dirname, $dirMode);
413
        }
414
415 13
        if (!copy($source, $destination)) {
416
            throw new RuntimeException('Failed to copy the file.');
417
        }
418
419 13
        if ($fileMode !== null && !chmod($destination, $fileMode)) {
420
            throw new RuntimeException(sprintf('Unable to set mode "%s" for "%s".', $fileMode, $destination));
421
        }
422
423 13
        self::processCallback($afterCopy, $source, $destination);
424
    }
425
426
    /**
427
     * @param callable|null $callback
428
     * @param array $arguments
429
     *
430
     * @throws InvalidArgumentException
431
     *
432
     * @return mixed
433
     */
434 17
    private static function processCallback(?callable $callback, mixed ...$arguments): mixed
435
    {
436 17
        return $callback ? $callback(...$arguments) : null;
437
    }
438
439 67
    private static function getFilter(array $options): ?PathMatcherInterface
440
    {
441 67
        if (!array_key_exists('filter', $options)) {
442 67
            return null;
443
        }
444
445 9
        if (!$options['filter'] instanceof PathMatcherInterface) {
446 2
            $type = get_debug_type($options['filter']);
447 2
            throw new InvalidArgumentException(
448 2
                sprintf('Filter should be an instance of PathMatcherInterface, %s given.', $type)
449
            );
450
        }
451
452 7
        return $options['filter'];
453
    }
454
455
    /**
456
     * Assert that destination is not within the source directory.
457
     *
458
     * @param string $source Path to source.
459
     * @param string $destination Path to destination.
460
     *
461
     * @throws InvalidArgumentException
462
     */
463 18
    private static function assertNotSelfDirectory(string $source, string $destination): void
464
    {
465 18
        if ($source === $destination || str_starts_with($destination, $source . '/')) {
466 2
            throw new InvalidArgumentException('Trying to copy a directory to itself or a subdirectory.');
467
        }
468
    }
469
470
    /**
471
     * Open directory handle.
472
     *
473
     * @param string $directory Path to directory.
474
     *
475
     * @throws RuntimeException if unable to open directory.
476
     * @throws InvalidArgumentException if argument is not a directory.
477
     *
478
     * @return resource
479
     */
480 67
    private static function openDirectory(string $directory)
481
    {
482 67
        if (!file_exists($directory)) {
483 3
            throw new InvalidArgumentException("\"$directory\" does not exist.");
484
        }
485
486 67
        if (!is_dir($directory)) {
487 1
            throw new InvalidArgumentException("\"$directory\" is not a directory.");
488
        }
489
490
        /** @psalm-suppress InvalidArgument, MixedArgumentTypeCoercion */
491 67
        set_error_handler(static function (int $errorNumber, string $errorString) use ($directory): bool {
492
            throw new RuntimeException(
493
                sprintf('Unable to open directory "%s". ', $directory) . $errorString,
494
                $errorNumber
495
            );
496
        });
497
498 67
        $handle = opendir($directory);
499
500 67
        if ($handle === false) {
501
            throw new RuntimeException(sprintf('Unable to open directory "%s". ', $directory));
502
        }
503
504 67
        restore_error_handler();
505
506 67
        return $handle;
507
    }
508
509
    /**
510
     * Returns the last modification time for the given paths.
511
     *
512
     * If the path is a directory, any nested files/directories will be checked as well.
513
     *
514
     * @param RecursiveDirectoryIterator[]|string[] $paths The directories to be checked.
515
     *
516
     * @throws LogicException If path is not set.
517
     *
518
     * @return int|null Unix timestamp representing the last modification time.
519
     */
520 3
    public static function lastModifiedTime(string|RecursiveDirectoryIterator ...$paths): ?int
521
    {
522 3
        if (empty($paths)) {
523 1
            throw new LogicException('Path is required.');
524
        }
525
526 2
        $time = null;
527
528 2
        foreach ($paths as $path) {
529 2
            if (is_string($path)) {
530 2
                $timestamp = self::modifiedTime($path);
531
532 2
                if ($timestamp > $time) {
533 2
                    $time = $timestamp;
534
                }
535
536 2
                if (is_file($path)) {
537 1
                    continue;
538
                }
539
540 2
                $path = new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS);
541
            }
542
543
            /** @var iterable<string, string> $iterator */
544 2
            $iterator = new RecursiveIteratorIterator(
545
                $path,
546
                RecursiveIteratorIterator::SELF_FIRST
547
            );
548
549 2
            foreach ($iterator as $path => $_info) {
0 ignored issues
show
Comprehensibility Bug introduced by
$path is overwriting a variable from outer foreach loop.
Loading history...
550 2
                $timestamp = self::modifiedTime($path);
551
552 2
                if ($timestamp > $time) {
553 1
                    $time = $timestamp;
554
                }
555
            }
556
        }
557
558 2
        return $time;
559
    }
560
561 2
    private static function modifiedTime(string $path): ?int
562
    {
563 2
        if (false !== $timestamp = filemtime($path)) {
564 2
            return $timestamp;
565
        }
566
567
        return null;
568
    }
569
570
    /**
571
     * Returns the directories found under the specified directory and subdirectories.
572
     *
573
     * @param string $directory The directory under which the files will be looked for.
574
     * @param array $options Options for directory searching. Valid options are:
575
     *
576
     * - filter: a filter to apply while looked directories. It should be an instance of {@see PathMatcherInterface}.
577
     * - recursive: boolean, whether the subdirectories should also be looked for. Defaults to `true`.
578
     *
579
     * @psalm-param array{
580
     *   filter?: PathMatcherInterface,
581
     *   recursive?: bool,
582
     * } $options
583
     *
584
     * @throws InvalidArgumentException If the directory is invalid.
585
     *
586
     * @return string[] Directories found under the directory specified, in no particular order.
587
     * Ordering depends on the file system used.
588
     */
589 6
    public static function findDirectories(string $directory, array $options = []): array
590
    {
591 6
        $filter = self::getFilter($options);
592 5
        $recursive = !array_key_exists('recursive', $options) || $options['recursive'];
593 5
        $directory = self::normalizePath($directory);
594
595 5
        $result = [];
596
597 5
        $handle = self::openDirectory($directory);
598 4
        while (false !== $file = readdir($handle)) {
599 4
            if ($file === '.' || $file === '..') {
600 4
                continue;
601
            }
602
603 4
            $path = $directory . '/' . $file;
604 4
            if (is_file($path)) {
605 3
                continue;
606
            }
607
608 4
            if ($filter === null || $filter->match($path)) {
609 4
                $result[] = $path;
610
            }
611
612 4
            if ($recursive) {
613 3
                $result = array_merge($result, self::findDirectories($path, $options));
614
            }
615
        }
616 4
        closedir($handle);
617
618 4
        return $result;
619
    }
620
621
    /**
622
     * Returns the files found under the specified directory and subdirectories.
623
     *
624
     * @param string $directory The directory under which the files will be looked for.
625
     * @param array $options Options for file searching. Valid options are:
626
     *
627
     * - filter: a filter to apply while looked files. It should be an instance of {@see PathMatcherInterface}.
628
     * - recursive: boolean, whether the files under the subdirectories should also be looked for. Defaults to `true`.
629
     *
630
     * @psalm-param array{
631
     *   filter?: PathMatcherInterface,
632
     *   recursive?: bool,
633
     * } $options
634
     *
635
     * @throws InvalidArgumentException If the directory is invalid.
636
     *
637
     * @return string[] Files found under the directory specified, in no particular order.
638
     * Ordering depends on the files system used.
639
     */
640 6
    public static function findFiles(string $directory, array $options = []): array
641
    {
642 6
        $filter = self::getFilter($options);
643 5
        $recursive = !array_key_exists('recursive', $options) || $options['recursive'];
644
645 5
        $directory = self::normalizePath($directory);
646
647 5
        $result = [];
648
649 5
        $handle = self::openDirectory($directory);
650 4
        while (false !== $file = readdir($handle)) {
651 4
            if ($file === '.' || $file === '..') {
652 4
                continue;
653
            }
654
655 4
            $path = $directory . '/' . $file;
656
657 4
            if (is_file($path)) {
658 4
                if ($filter === null || $filter->match($path)) {
659 4
                    $result[] = $path;
660
                }
661 4
                continue;
662
            }
663
664 4
            if ($recursive) {
665 3
                $result = array_merge($result, self::findFiles($path, $options));
666
            }
667
        }
668 4
        closedir($handle);
669
670 4
        return $result;
671
    }
672
673
    /**
674
     * Removes a link or an empty directory.
675
     *
676
     * @param string $directory The empty directory or the link to be deleted.
677
     *
678
     * @throw RuntimeException When unable to remove directory or link.
679
     */
680 67
    private static function removeLinkOrEmptyDirectory(string $directory): void
681
    {
682 67
        if (is_link($directory)) {
683 2
            self::unlink($directory);
684
        } else {
685 67
            set_error_handler(static function (int $errorNumber, string $errorString) use ($directory): bool {
686
                throw new RuntimeException(
687
                    sprintf('Failed to remove directory "%s". ', $directory) . $errorString,
688
                    $errorNumber
689
                );
690
            });
691
692 67
            rmdir($directory);
693
694 67
            restore_error_handler();
695
        }
696
    }
697
}
698