Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
98.67% |
148 / 150 |
|
96.55% |
28 / 29 |
CRAP | |
0.00% |
0 / 1 |
Dir | |
98.67% |
148 / 150 |
|
96.55% |
28 / 29 |
101 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
10 | |||
count | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getIterator | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setAbsolute | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
setRelative | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
setRecursive | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setFilesOnly | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
isAbsolute | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isRelative | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isRecursive | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isFilesOnly | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getPath | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFiles | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getTree | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
copyTo | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
6 | |||
fileExists | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
deleteFile | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
emptyDir | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
8 | |||
__get | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
__isset | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
__set | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
__unset | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
offsetExists | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
offsetGet | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
offsetSet | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
offsetUnset | |
83.33% |
10 / 12 |
|
0.00% |
0 / 1 |
7.23 | |||
traverse | |
100.00% |
26 / 26 |
|
100.00% |
1 / 1 |
18 | |||
traverseRecursively | |
100.00% |
27 / 27 |
|
100.00% |
1 / 1 |
19 | |||
buildTree | |
100.00% |
10 / 10 |
|
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 | */ |
14 | namespace Pop\Dir; |
15 | |
16 | use ArrayIterator; |
17 | use DirectoryIterator; |
18 | use RecursiveIteratorIterator; |
19 | use 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 | */ |
31 | class 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 | } |