1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11:
12:
13: namespace KeepItSimple\FileSystem;
14:
15: use Countable;
16: use Iterator;
17: use IteratorAggregate;
18: use ArrayIterator;
19: use AppendIterator;
20: use FilesystemIterator;
21: use RecursiveArrayIterator;
22: use RecursiveDirectoryIterator;
23: use RecursiveIteratorIterator;
24: use CallbackFilterIterator;
25: use RecursiveCallbackFilterIterator;
26: use SplFileInfo;
27: use InvalidArgumentException;
28:
29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40:
41: class Finder implements IteratorAggregate, Countable
42: {
43:
44: const ONLY_FILES = 1;
45:
46: const ONLY_DIRECTORIES = 2;
47:
48:
49: private $mode;
50:
51:
52: private $dirs = [];
53:
54:
55: private $filters = [];
56:
57:
58: private $sorts = [];
59:
60:
61: private $excluded_dirs = [];
62:
63:
64: private $ignore_vcs = true;
65:
66:
67: private $ignore_unreadable_dirs = true;
68:
69:
70: private $flags;
71:
72:
73: private $vcs_list = ['.svn', '.cvs', '.idea', '.DS_Store', '.git', '.hg'];
74:
75:
76: private $depth = -1;
77:
78: 79: 80: 81:
82: public function __construct()
83: {
84: $this->flags = FilesystemIterator::CURRENT_AS_FILEINFO|FilesystemIterator::SKIP_DOTS;
85: }
86:
87: 88: 89: 90: 91: 92:
93: public static function create()
94: {
95: return new static();
96: }
97:
98: 99: 100: 101: 102:
103: public function files()
104: {
105: $this->mode = self::ONLY_FILES;
106:
107: return $this;
108: }
109:
110: 111: 112: 113: 114:
115: public function directories()
116: {
117: $this->mode = self::ONLY_DIRECTORIES;
118:
119: return $this;
120: }
121:
122: 123: 124: 125: 126: 127:
128: public function in($directories)
129: {
130: $new_directories = array_map(function ($directory) {
131: if (is_dir($directory)) {
132: return $directory;
133: }
134:
135: return glob($directory, (defined('GLOB_BRACE') ? GLOB_BRACE : 0) | GLOB_ONLYDIR);
136: }, (array)$directories);
137:
138: $this->dirs = array_unique(array_merge(
139: $this->dirs,
140: $this->flattenParameters($new_directories)
141: ));
142:
143: return $this;
144: }
145:
146: 147: 148: 149: 150: 151:
152: public function exclude($directories)
153: {
154: $this->excluded_dirs = array_merge(
155: $this->excluded_dirs,
156: (array)$directories
157: );
158:
159: return $this;
160: }
161:
162: 163: 164: 165: 166: 167:
168: public function ignoreUnreadableDirs($yes = true)
169: {
170: $this->ignore_unreadable_dirs = (bool)$yes;
171:
172: return $this;
173: }
174:
175: 176: 177: 178: 179: 180: 181: 182:
183: public function ignoreDots($yes = true)
184: {
185: if ($yes) {
186: $this->flags |= FilesystemIterator::SKIP_DOTS;
187: } else {
188: $this->flags &= ~FilesystemIterator::SKIP_DOTS;
189: }
190:
191: return $this;
192: }
193:
194: 195: 196: 197: 198: 199:
200: public function ignoreVCS($yes = true)
201: {
202: $this->ignore_vcs = (bool)$yes;
203:
204: return $this;
205: }
206:
207: 208: 209: 210: 211: 212: 213:
214: public function followLinks($yes = true)
215: {
216: if ($yes) {
217: $this->flags |= FilesystemIterator::FOLLOW_SYMLINKS;
218: } else {
219: $this->flags &= ~FilesystemIterator::FOLLOW_SYMLINKS;
220: }
221:
222: return $this;
223: }
224:
225: 226: 227: 228: 229: 230: 231: 232: 233:
234: public function sort(callable $callback)
235: {
236: $this->sorts[] = $callback;
237:
238: return $this;
239: }
240:
241: 242: 243: 244: 245: 246:
247: public function sortByName()
248: {
249: return $this->sort(function (SplFileInfo $a, SplFileInfo $b) {
250: return strcmp($a->getFilename(), $b->getFilename());
251: });
252: }
253:
254: 255: 256: 257: 258: 259:
260: public function sortByType()
261: {
262: return $this->sort(function (SplFileInfo $a, SplFileInfo $b) {
263: return strcmp($a->getType(), $b->getType());
264: });
265: }
266:
267: 268: 269: 270: 271: 272:
273: public function sortBySize()
274: {
275: return $this->sort(function (SplFileInfo $a, SplFileInfo $b) {
276: return strcmp($a->getSize(), $b->getSize());
277: });
278: }
279:
280: 281: 282: 283: 284: 285:
286: public function sortByExtension()
287: {
288: return $this->sort(function (SplFileInfo $a, SplFileInfo $b) {
289: return strcmp($a->getExtension(), $b->getExtension());
290: });
291: }
292:
293: 294: 295: 296: 297: 298:
299: public function sortByPath()
300: {
301: return $this->sort(function (SplFileInfo $a, SplFileInfo $b) {
302: return strcmp($a->getPath(), $b->getPath());
303: });
304: }
305:
306: 307: 308: 309: 310: 311:
312: public function sortByPermission()
313: {
314: return $this->sort(function (SplFileInfo $a, SplFileInfo $b) {
315: return strcmp($a->getPerms(), $b->getPerms());
316: });
317: }
318:
319: 320: 321: 322: 323: 324:
325: public function sortByAccessedTime()
326: {
327: return $this->sort(function (SplFileInfo $a, SplFileInfo $b) {
328: return strcmp($a->getATime(), $b->getATime());
329: });
330: }
331:
332: 333: 334: 335: 336: 337:
338: public function sortByModifiedTime()
339: {
340: return $this->sort(function (SplFileInfo $a, SplFileInfo $b) {
341: return strcmp($a->getMTime(), $b->getMTime());
342: });
343: }
344:
345: 346: 347: 348: 349: 350:
351: public function sortByChangedTime()
352: {
353: return $this->sort(function (SplFileInfo $a, SplFileInfo $b) {
354: return strcmp($a->getCTime(), $b->getCTime());
355: });
356: }
357:
358: 359: 360: 361: 362: 363: 364:
365: public function filter(callable $callback)
366: {
367: $this->filters[] = $callback;
368:
369: return $this;
370: }
371:
372: 373: 374: 375: 376: 377: 378: 379: 380:
381: public function withName($name)
382: {
383: return $this->filter(function (SplFileInfo $current) use ($name) {
384: if ($name == $current->getBasename()) {
385: return true;
386: }
387:
388: $path_name = str_replace('\\', '/', $current->getPathname());
389: $glob = array_map(function ($string) {
390: return str_replace('\\', '/', $string);
391: }, glob($current->getPath().'/'.$name));
392:
393: if (in_array($path_name, $glob)) {
394: return true;
395: }
396:
397: set_error_handler(function () {
398: return true;
399: });
400:
401: $preg_pattern = '/'.trim($name, '/#').'/';
402:
403: if (preg_match($preg_pattern, $current->getBasename())) {
404: return true;
405: }
406:
407: set_error_handler(null);
408:
409: return false;
410: });
411: }
412:
413: 414: 415: 416: 417: 418: 419: 420: 421:
422: public function withoutName($name)
423: {
424: return $this->filter(function (SplFileInfo $current) use ($name) {
425: if ($name == $current->getBasename()) {
426: return false;
427: }
428:
429: $path_name = str_replace('\\', '/', $current->getPathname());
430: $glob = array_map(function ($string) {
431: return str_replace('\\', '/', $string);
432: }, glob($current->getPath().'/'.$name));
433:
434: if (in_array($path_name, $glob)) {
435: return false;
436: }
437:
438: set_error_handler(function () {
439: return true;
440: });
441:
442: $preg_pattern = '/'.trim($name, '/#').'/';
443:
444: if (preg_match($preg_pattern, $current->getBasename())) {
445: return false;
446: }
447:
448: set_error_handler(null);
449:
450: return true;
451: });
452: }
453:
454: 455: 456: 457: 458: 459: 460: 461: 462: 463:
464: public function contains($pattern)
465: {
466: return $this->filter(function (SplFileInfo $current) use ($pattern) {
467: if ($current->isDir() || $current->getSize() == 0) {
468: return true;
469: }
470:
471: $content = $current->openFile()->fread($current->getSize());
472:
473: set_error_handler(function () {
474: return true;
475: });
476:
477: $preg_pattern = '/'.trim($pattern, '/#').'/';
478:
479: if (strpos($content, $pattern) !== false || preg_match($preg_pattern, $content)) {
480: return true;
481: }
482:
483: set_error_handler(null);
484:
485: return false;
486: });
487: }
488:
489: 490: 491: 492: 493: 494: 495: 496: 497: 498:
499: public function doesNotContain($pattern)
500: {
501: return $this->filter(function (SplFileInfo $current) use ($pattern) {
502: if ($current->isDir() || $current->getSize() == 0) {
503: return true;
504: }
505:
506: $content = $current->openFile()->fread($current->getSize());
507:
508: set_error_handler(function () {
509: return true;
510: });
511:
512: $preg_pattern = '/'.trim($pattern, '/#').'/';
513:
514: if (strpos($content, $pattern) !== false || preg_match($preg_pattern, $content)) {
515: return false;
516: }
517:
518: set_error_handler(null);
519:
520: return true;
521: });
522: }
523:
524: 525: 526: 527: 528: 529: 530:
531: public function withPath($path)
532: {
533: return $this->filter(function (SplFileInfo $current) use ($path) {
534: set_error_handler(function () {
535: return true;
536: });
537:
538: $path = str_replace('\\', '/', $path);
539: $realpath = str_replace('\\', '/', $current->getRealPath());
540: $preg_pattern = '/'.trim($path, '/#').'/';
541:
542: if (strpos($realpath, $path) !== false || preg_match($preg_pattern, $realpath)) {
543: return true;
544: }
545:
546: set_error_handler(null);
547:
548: return false;
549: });
550: }
551:
552: 553: 554: 555: 556: 557: 558:
559: public function withoutPath($path)
560: {
561: return $this->filter(function (SplFileInfo $current) use ($path) {
562: set_error_handler(function () {
563: return true;
564: });
565:
566: $path = str_replace('\\', '/', $path);
567: $realpath = str_replace('\\', '/', $current->getRealPath());
568: $pos = strpos($realpath, $path);
569: $preg_pattern = '/'.trim($path, '/#').'/';
570:
571: if ($pos === 0 || $pos > 0 || preg_match($preg_pattern, $realpath)) {
572: return false;
573: }
574:
575: set_error_handler(null);
576:
577: return true;
578: });
579: }
580:
581: 582: 583: 584: 585: 586: 587: 588:
589: public function date($date)
590: {
591: return $this->filter(function (SplFileInfo $current) use ($date) {
592: return $current->getCTime() >= strtotime($date);
593: });
594: }
595:
596: 597: 598: 599: 600: 601: 602:
603: public function size($size)
604: {
605: return $this->filter(function (SplFileInfo $current) use ($size) {
606: return $current->getSize() >= $size;
607: });
608: }
609:
610: 611: 612: 613: 614: 615:
616: public function depth($depth)
617: {
618: $this->depth = max(-1, (int)$depth);
619:
620: return $this;
621: }
622:
623: 624: 625: 626: 627: 628: 629:
630: public function merge($iterator)
631: {
632: if (is_array($iterator)) {
633: $this->in($iterator);
634: } elseif ($iterator instanceof Finder) {
635: $this->dirs = array_unique(array_merge($this->dirs, $iterator->dirs));
636: } elseif ($iterator instanceof Iterator) {
637: $this->in(iterator_to_array($iterator));
638: } else {
639: throw new InvalidArgumentException(sprintf(
640: 'The argument given to %s is not an instance of Finder/Iterator or an array.',
641: __METHOD__
642: ));
643: }
644:
645: return $this;
646: }
647:
648: 649: 650: 651: 652: 653: 654:
655: public function getIterator()
656: {
657: $iterator = new AppendIterator();
658:
659: if ($this->ignore_vcs) {
660: $this->excluded_dirs = array_unique(array_merge($this->excluded_dirs, $this->vcs_list));
661: }
662:
663: foreach ($this->dirs as $dir) {
664: $directory = new RecursiveCallbackFilterIterator(
665: new RecursiveDirectoryIterator($dir, $this->flags),
666: function (SplFileInfo $current) {
667: if (in_array($current->getFilename(), $this->excluded_dirs)) {
668: return false;
669: }
670:
671: return true;
672: }
673: );
674:
675: $directory = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST);
676: $directory->setMaxDepth($this->depth);
677:
678: if ($this->ignore_unreadable_dirs) {
679: $directory = new CallbackFilterIterator($directory, function (SplFileInfo $current) {
680: return $current->isFile() || $current->isReadable();
681: });
682: }
683:
684: if ($this->mode === self::ONLY_DIRECTORIES) {
685: $directory = new CallbackFilterIterator($directory, function (SplFileInfo $current) {
686: return $current->isDir();
687: });
688: } elseif ($this->mode === self::ONLY_FILES) {
689: $directory = new CallbackFilterIterator($directory, function (SplFileInfo $current) {
690: return $current->isFile();
691: });
692: }
693:
694: foreach ($this->filters as $filter) {
695: $directory = new CallbackFilterIterator($directory, $filter);
696: }
697:
698: $iterator->append($directory);
699: }
700:
701: $files = iterator_to_array($iterator);
702: foreach ($this->sorts as $sort) {
703: uasort($files, $sort);
704: }
705:
706: return new ArrayIterator($files);
707: }
708:
709: 710: 711: 712: 713: 714: 715:
716: public function count()
717: {
718: return (int)iterator_count($this->getIterator());
719: }
720:
721: 722: 723: 724:
725: private function flattenParameters()
726: {
727: return iterator_to_array(
728: new RecursiveIteratorIterator(new RecursiveArrayIterator(func_get_args())),
729: false
730: );
731: }
732: }
733: