Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.67% covered (success)
98.67%
148 / 150
96.55% covered (success)
96.55%
28 / 29
CRAP
0.00% covered (danger)
0.00%
0 / 1
Dir
98.67% covered (success)
98.67%
148 / 150
96.55% covered (success)
96.55%
28 / 29
101
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
10
 count
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getIterator
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setAbsolute
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 setRelative
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 setRecursive
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setFilesOnly
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 isAbsolute
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isRelative
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isRecursive
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isFilesOnly
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFiles
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTree
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 copyTo
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 fileExists
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 deleteFile
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 emptyDir
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
8
 __get
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __isset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __set
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __unset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offsetExists
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 offsetGet
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 offsetSet
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 offsetUnset
83.33% covered (success)
83.33%
10 / 12
0.00% covered (danger)
0.00%
0 / 1
7.23
 traverse
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
18
 traverseRecursively
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
19
 buildTree
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2/**
3 * Pop PHP Framework (http://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@nolainteractive.com>
7 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
8 * @license    http://www.popphp.org/license     New BSD License
9 */
10
11/**
12 * @namespace
13 */
14namespace Pop\Dir;
15
16use ArrayIterator;
17use DirectoryIterator;
18use RecursiveIteratorIterator;
19use RecursiveDirectoryIterator;
20
21/**
22 * Directory class
23 *
24 * @category   Pop
25 * @package    Pop\Dir
26 * @author     Nick Sagona, III <dev@nolainteractive.com>
27 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
28 * @license    http://www.popphp.org/license     New BSD License
29 * @version    4.0.0
30 */
31class Dir implements \ArrayAccess, \Countable, \IteratorAggregate
32{
33
34    /**
35     * The directory path
36     * @var ?string
37     */
38    protected ?string $path = null;
39
40    /**
41     * The files within the directory
42     * @var array
43     */
44    protected array $files = [];
45
46    /**
47     * The nested tree map of the directory and its files
48     * @var array
49     */
50    protected array $tree = [];
51
52    /**
53     * Flag to store the absolute path.
54     * @var bool
55     */
56    protected bool $absolute = false;
57
58    /**
59     * Flag to store the relative path.
60     * @var bool
61     */
62    protected bool $relative = false;
63
64    /**
65     * Flag to dig recursively.
66     * @var bool
67     */
68    protected bool $recursive = false;
69
70    /**
71     * Flag to include only files and no directories
72     * @var bool
73     */
74    protected bool $filesOnly = false;
75
76    /**
77     * Constructor
78     *
79     * Instantiate a directory object
80     *
81     * @param  string  $dir
82     * @param  array   $options
83     * @throws Exception
84     */
85    public function __construct(string $dir, array $options = [])
86    {
87        // Set the directory path.
88        if ((str_contains($dir, "\\")) && (DIRECTORY_SEPARATOR != "\\")) {
89            $this->path = str_replace("\\", '/', $dir);
90        } else {
91            $this->path = $dir;
92        }
93
94        // Check to see if the directory exists.
95        if (!file_exists($this->path)) {
96            throw new Exception("Error: The directory '" . $this->path . "' does not exist");
97        }
98
99        // Trim the trailing slash.
100        if (strrpos($this->path, DIRECTORY_SEPARATOR) == (strlen($this->path) - 1)) {
101            $this->path = substr($this->path, 0, -1);
102        }
103
104        if (isset($options['absolute'])) {
105            $this->setAbsolute($options['absolute']);
106        }
107        if (isset($options['relative'])) {
108            $this->setRelative($options['relative']);
109        }
110        if (isset($options['recursive'])) {
111            $this->setRecursive($options['recursive']);
112        }
113        if (isset($options['filesOnly'])) {
114            $this->setFilesOnly($options['filesOnly']);
115        }
116
117        $this->tree[realpath($this->path)] = $this->buildTree(new DirectoryIterator($this->path));
118
119        if ($this->recursive) {
120            $this->traverseRecursively();
121        } else {
122            $this->traverse();
123        }
124    }
125
126    /**
127     * Method to get the count of files in the directory
128     *
129     * @return int
130     */
131    public function count(): int
132    {
133        return count($this->files);
134    }
135
136    /**
137     * Method to iterate over the files
138     *
139     * @return ArrayIterator
140     */
141    public function getIterator(): ArrayIterator
142    {
143        return new ArrayIterator($this->files);
144    }
145
146    /**
147     * Set absolute
148     *
149     * @param  bool $absolute
150     * @return Dir
151     */
152    public function setAbsolute(bool $absolute): Dir
153    {
154        $this->absolute = $absolute;
155        if (($this->absolute) && ($this->isRelative())) {
156            $this->setRelative(false);
157        }
158        return $this;
159    }
160
161    /**
162     * Set relative
163     *
164     * @param  bool $relative
165     * @return Dir
166     */
167    public function setRelative(bool $relative): Dir
168    {
169        $this->relative = $relative;
170        if (($this->relative) && ($this->isAbsolute())) {
171            $this->setAbsolute(false);
172        }
173        return $this;
174    }
175
176    /**
177     * Set recursive
178     *
179     * @param  bool $recursive
180     * @return Dir
181     */
182    public function setRecursive(bool $recursive): Dir
183    {
184        $this->recursive = $recursive;
185        return $this;
186    }
187
188    /**
189     * Set files only
190     *
191     * @param  bool $filesOnly
192     * @return Dir
193     */
194    public function setFilesOnly(bool $filesOnly): Dir
195    {
196        $this->filesOnly = $filesOnly;
197        return $this;
198    }
199
200    /**
201     * Is absolute
202     *
203     * @return bool
204     */
205    public function isAbsolute(): bool
206    {
207        return $this->absolute;
208    }
209
210    /**
211     * Is relative
212     *
213     * @return bool
214     */
215    public function isRelative(): bool
216    {
217        return $this->relative;
218    }
219
220    /**
221     * Is recursive
222     *
223     * @return bool
224     */
225    public function isRecursive(): bool
226    {
227        return $this->recursive;
228    }
229
230    /**
231     * Is files only
232     *
233     * @return bool
234     */
235    public function isFilesOnly(): bool
236    {
237        return $this->filesOnly;
238    }
239
240    /**
241     * Get the path
242     *
243     * @return string|null
244     */
245    public function getPath(): string|null
246    {
247        return $this->path;
248    }
249
250    /**
251     * Get the files
252     *
253     * @return array
254     */
255    public function getFiles(): array
256    {
257        return $this->files;
258    }
259
260    /**
261     * Get the tree
262     *
263     * @return array
264     */
265    public function getTree(): array
266    {
267        return $this->tree;
268    }
269
270    /**
271     * Copy an entire directory recursively to another destination directory
272     *
273     * @param  string $destination
274     * @param  bool   $full
275     * @return void
276     */
277    public function copyTo(string $destination, bool $full = true): void
278    {
279        if ($full) {
280            if (str_contains($this->path, DIRECTORY_SEPARATOR)) {
281                $folder = substr($this->path, (strrpos($this->path, DIRECTORY_SEPARATOR) + 1));
282            }
283
284            if (!file_exists($destination . DIRECTORY_SEPARATOR . $folder)) {
285                mkdir($destination . DIRECTORY_SEPARATOR . $folder);
286            }
287            $destination = $destination . DIRECTORY_SEPARATOR . $folder;
288        }
289
290        foreach (
291            $iterator = new RecursiveIteratorIterator(
292                new RecursiveDirectoryIterator($this->path, RecursiveDirectoryIterator::SKIP_DOTS),
293                RecursiveIteratorIterator::SELF_FIRST) as $item
294        ) {
295            if ($item->isDir()) {
296                mkdir($destination . DIRECTORY_SEPARATOR . $iterator->getSubPathName());
297            } else {
298                copy($item, $destination . DIRECTORY_SEPARATOR . $iterator->getSubPathName());
299            }
300        }
301    }
302
303    /**
304     * File exists
305     *
306     * @param  string  $file
307     * @return bool
308     */
309    public function fileExists(string $file): bool
310    {
311        return $this->offsetExists($file);
312    }
313
314    /**
315     * Delete a file
316     *
317     * @param  string  $file
318     * @throws Exception
319     * @return void
320     */
321    public function deleteFile(string $file): void
322    {
323        $this->offsetUnset($file);
324    }
325
326    /**
327     * Empty an entire directory
328     *
329     * @param  bool    $remove
330     * @param  ?string $path
331     * @throws Exception
332     * @return void
333     */
334    public function emptyDir(bool $remove = false, ?string $path = null): void
335    {
336        if ($path === null) {
337            $path = $this->path;
338        }
339
340        // Get a directory handle.
341        if (!($dh = @opendir($path))) {
342            throw new Exception('Error: Unable to open the directory path "' . $path . '"');
343        }
344
345        // Recursively dig through the directory, deleting files where applicable.
346        while (false !== ($obj = readdir($dh))) {
347            if ($obj == '.' || $obj == '..') {
348                continue;
349            }
350            if (!@unlink($path . DIRECTORY_SEPARATOR . $obj)) {
351                $this->emptyDir(true, $path . DIRECTORY_SEPARATOR . $obj);
352            }
353        }
354
355        // Close the directory handle.
356        closedir($dh);
357
358        // If the delete flag was passed, remove the top level directory.
359        if ($remove) {
360            @rmdir($path);
361        }
362    }
363
364    /**
365     * Get a file
366     *
367     * @param  string $name
368     * @return mixed
369     */
370    public function __get(string $name): mixed
371    {
372        return $this->offsetGet($name);
373    }
374
375    /**
376     * Does file exist
377     *
378     * @param  string $name
379     * @return bool
380     */
381    public function __isset(string $name): bool
382    {
383        return $this->offsetExists($name);
384    }
385
386    /**
387     * Set method
388     *
389     * @param  string $name
390     * @param  mixed  $value
391     * @throws Exception
392     * @return void
393     */
394    public function __set(string $name, mixed $value): void
395    {
396        $this->offsetSet($name, $value);
397    }
398
399    /**
400     * Unset method
401     *
402     * @param  string $name
403     * @throws Exception
404     * @return void
405     */
406    public function __unset(string $name): void
407    {
408        $this->offsetUnset($name);
409    }
410
411    /**
412     * ArrayAccess offsetExists
413     *
414     * @param  mixed $offset
415     * @return bool
416     */
417    public function offsetExists(mixed $offset): bool
418    {
419        if (!is_numeric($offset) && in_array($offset, $this->files)) {
420            $offset = array_search($offset, $this->files);
421        }
422        return isset($this->files[$offset]);
423    }
424
425    /**
426     * ArrayAccess offsetGet
427     *
428     * @param  mixed $offset
429     * @return mixed
430     */
431    public function offsetGet(mixed $offset): mixed
432    {
433        return (isset($this->files[$offset])) ? $this->files[$offset] : null;
434    }
435
436    /**
437     * ArrayAccess offsetSet
438     *
439     * @param  mixed $offset
440     * @param  mixed $value
441     * @throws Exception
442     * @return void
443     */
444    public function offsetSet(mixed $offset, mixed $value): void
445    {
446        throw new Exception('Error: The directory object is read-only');
447    }
448
449    /**
450     * ArrayAccess offsetUnset
451     *
452     * @param  mixed $offset
453     * @throws Exception
454     * @return void
455     */
456    public function offsetUnset(mixed $offset): void
457    {
458        if (!is_numeric($offset) && in_array($offset, $this->files)) {
459            $offset = array_search($offset, $this->files);
460        }
461        if (isset($this->files[$offset])) {
462            if (is_dir($this->path . DIRECTORY_SEPARATOR . $this->files[$offset])) {
463                throw new Exception("Error: The file '" . $this->path . DIRECTORY_SEPARATOR . $this->files[$offset] . "' is a directory");
464            } else if (!file_exists($this->path . DIRECTORY_SEPARATOR . $this->files[$offset])) {
465                throw new Exception("Error: The file '" . $this->path . DIRECTORY_SEPARATOR . $this->files[$offset] . "' does not exist");
466            } else if (!is_writable($this->path . DIRECTORY_SEPARATOR . $this->files[$offset])) {
467                throw new Exception("Error: The file '" . $this->path . DIRECTORY_SEPARATOR . $this->files[$offset] . "' is read-only");
468            } else {
469                unlink($this->path . DIRECTORY_SEPARATOR . $this->files[$offset]);
470                unset($this->files[$offset]);
471            }
472        } else {
473            throw new Exception("Error: The file does not exist");
474        }
475    }
476
477    /**
478     * Traverse the directory
479     *
480     * @return void
481     */
482    protected function traverse(): void
483    {
484        foreach (new DirectoryIterator($this->path) as $fileInfo) {
485            if(!$fileInfo->isDot()) {
486                // If absolute path flag was passed, store the absolute path.
487                if ($this->absolute) {
488                    $f = null;
489                    if (!$this->filesOnly) {
490                        $f = ($fileInfo->isDir()) ?
491                            ($this->path . DIRECTORY_SEPARATOR . $fileInfo->getFilename() . DIRECTORY_SEPARATOR) :
492                            ($this->path . DIRECTORY_SEPARATOR . $fileInfo->getFilename());
493                    } else if (!$fileInfo->isDir()) {
494                        $f = $this->path . DIRECTORY_SEPARATOR . $fileInfo->getFilename();
495                    }
496                    if (($f !== false) && ($f !== null)) {
497                        $this->files[] = $f;
498                    }
499                // If relative path flag was passed, store the relative path.
500                } else if ($this->relative) {
501                    $f = null;
502                    if (!$this->filesOnly) {
503                        $f = ($fileInfo->isDir()) ?
504                            ($this->path . DIRECTORY_SEPARATOR . $fileInfo->getFilename() . DIRECTORY_SEPARATOR) :
505                            ($this->path . DIRECTORY_SEPARATOR . $fileInfo->getFilename());
506                    } else if (!$fileInfo->isDir()) {
507                        $f = $this->path . DIRECTORY_SEPARATOR . $fileInfo->getFilename();
508                    }
509                    if (($f !== false) && ($f !== null)) {
510                        $this->files[] = substr($f, (strlen(realpath($this->path)) + 1));
511                    }
512                // Else, store only the directory or file name.
513                } else {
514                    if (!$this->filesOnly) {
515                        $this->files[] = ($fileInfo->isDir()) ? ($fileInfo->getFilename()) : $fileInfo->getFilename();
516                    } else if (!$fileInfo->isDir()) {
517                        $this->files[] = $fileInfo->getFilename();
518                    }
519                }
520            }
521        }
522    }
523
524    /**
525     * Traverse the directory recursively
526     *
527     * @return void
528     */
529    protected function traverseRecursively(): void
530    {
531        $objects = new RecursiveIteratorIterator(
532            new RecursiveDirectoryIterator($this->path), RecursiveIteratorIterator::SELF_FIRST
533        );
534        foreach ($objects as $fileInfo) {
535            if (($fileInfo->getFilename() != '.') && ($fileInfo->getFilename() != '..')) {
536                // If absolute path flag was passed, store the absolute path.
537                if ($this->absolute) {
538                    $f = null;
539                    if (!$this->filesOnly) {
540                        $f = ($fileInfo->isDir()) ?
541                            (realpath($fileInfo->getPathname())) : realpath($fileInfo->getPathname());
542                    } else if (!$fileInfo->isDir()) {
543                        $f = realpath($fileInfo->getPathname());
544                    }
545                    if (($f !== false) && ($f !== null)) {
546                        $this->files[] = $f;
547                    }
548                // If relative path flag was passed, store the relative path.
549                } else if ($this->relative) {
550                    $f = null;
551                    if (!$this->filesOnly) {
552                        $f = ($fileInfo->isDir()) ?
553                            (realpath($fileInfo->getPathname())) : realpath($fileInfo->getPathname());
554                    } else if (!$fileInfo->isDir()) {
555                        $f = realpath($fileInfo->getPathname());
556                    }
557                    if (($f !== false) && ($f !== null)) {
558                        $this->files[] = substr($f, (strlen(realpath($this->path)) + 1));
559                    }
560                // Else, store only the directory or file name.
561                } else {
562                    if (!$this->filesOnly) {
563                        $this->files[] = ($fileInfo->isDir()) ? ($fileInfo->getFilename()) : $fileInfo->getFilename();
564                    } else if (!$fileInfo->isDir()) {
565                        $this->files[] = $fileInfo->getFilename();
566                    }
567                }
568            }
569        }
570    }
571
572    /**
573     * Build the directory tree
574     *
575     * @param  DirectoryIterator $it
576     * @return array
577     */
578    protected function buildTree(DirectoryIterator $it): array
579    {
580        $result = [];
581
582        foreach ($it as $key => $child) {
583            if ($child->isDot()) {
584                continue;
585            }
586
587            $name = $child->getBasename();
588
589            if ($child->isDir()) {
590                $subDir = new DirectoryIterator($child->getPathname());
591                $result[DIRECTORY_SEPARATOR . $name] = $this->buildTree($subDir);
592            } else {
593                $result[] = $name;
594            }
595        }
596
597        return $result;
598    }
599
600}