Overview

Namespaces

  • KeepItSimple
    • FileSystem

Classes

  • KeepItSimple\FileSystem\Finder
  • Overview
  • Namespace
  • Class
  1: <?php
  2: /**
  3:  * This file is part of the KeepItSimple package.
  4:  * For the full copyright and license information, please view the LICENSE
  5:  * file that was distributed with this source code.
  6:  *
  7:  * @package   KeepItSimple\FileSystem
  8:  * @author    Alexandre Debusschere (debuss-a)
  9:  * @copyright Copyright (c) Alexandre Debusschere <alexandre@debuss-a.com>
 10:  * @licence   MIT
 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:  * Finder finds files and directories via a set of rules.
 31:  * It is a thin wrapper around several specialized iterator classes.
 32:  * All rules may be invoked several times.
 33:  * All methods return the current Finder object to allow easy chaining:
 34:  * <code>$finder = Finder::create()->files()->name('*.php')->in(__DIR__);</code>
 35:  *
 36:  * @package KeepItSimple\FileSystem\Finder
 37:  * @author  Alexandre Debusschere (debuss-a)
 38:  * @implements IteratorAggregate
 39:  * @implements Countable
 40:  */
 41: class Finder implements IteratorAggregate, Countable
 42: {
 43: 
 44:     const ONLY_FILES = 1;
 45: 
 46:     const ONLY_DIRECTORIES = 2;
 47: 
 48:     /** @ignore */
 49:     private $mode;
 50: 
 51:     /** @ignore */
 52:     private $dirs = [];
 53: 
 54:     /** @ignore */
 55:     private $filters = [];
 56: 
 57:     /** @ignore */
 58:     private $sorts = [];
 59: 
 60:     /** @ignore */
 61:     private $excluded_dirs = [];
 62: 
 63:     /** @ignore */
 64:     private $ignore_vcs = true;
 65: 
 66:     /** @ignore */
 67:     private $ignore_unreadable_dirs = true;
 68: 
 69:     /** @ignore */
 70:     private $flags;
 71: 
 72:     /** @ignore */
 73:     private $vcs_list = ['.svn', '.cvs', '.idea', '.DS_Store', '.git', '.hg'];
 74: 
 75:     /** @ignore */
 76:     private $depth = -1;
 77: 
 78:     /**
 79:      * Finder constructor.
 80:      * No parameters needed.
 81:      */
 82:     public function __construct()
 83:     {
 84:         $this->flags = FilesystemIterator::CURRENT_AS_FILEINFO|FilesystemIterator::SKIP_DOTS;
 85:     }
 86: 
 87:     /**
 88:      * Create a Finder instance and returns it.
 89:      * Equivalent to : <code>$finder = (new Finder())->in(__DIR__);</code>
 90:      *
 91:      * @return Finder
 92:      */
 93:     public static function create()
 94:     {
 95:         return new static();
 96:     }
 97: 
 98:     /**
 99:      * Restricts the matching to files only.
100:      *
101:      * @return Finder
102:      */
103:     public function files()
104:     {
105:         $this->mode = self::ONLY_FILES;
106: 
107:         return $this;
108:     }
109: 
110:     /**
111:      * Restricts the matching to directories only.
112:      *
113:      * @return Finder
114:      */
115:     public function directories()
116:     {
117:         $this->mode = self::ONLY_DIRECTORIES;
118: 
119:         return $this;
120:     }
121: 
122:     /**
123:      * Searches files and/or directories in the given path(s).
124:      *
125:      * @param string|array $directories
126:      * @return Finder
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:      * Searches files and/or directories except in the given path(s).
148:      *
149:      * @param $directories
150:      * @return Finder
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:      * Tells Finder to ignore (or not) unreadable directories.
164:      *
165:      * @param bool $yes
166:      * @return Finder
167:      */
168:     public function ignoreUnreadableDirs($yes = true)
169:     {
170:         $this->ignore_unreadable_dirs = (bool)$yes;
171: 
172:         return $this;
173:     }
174: 
175:     /**
176:      * Tells Finder to ignore (or not) dot directories.
177:      * Will remove current directory and parent directory ("." and "..") as well as file starting with ".".
178:      *
179:      * @param bool $yes
180:      * @return Finder
181:      * @uses FilesystemIterator::SKIP_DOTS
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:      * Tells Finder to ignore (or not) VCS files.
196:      *
197:      * @param bool $yes
198:      * @return Finder
199:      */
200:     public function ignoreVCS($yes = true)
201:     {
202:         $this->ignore_vcs = (bool)$yes;
203: 
204:         return $this;
205:     }
206: 
207:     /**
208:      * Tells Finder to follow (or not) symbolic links.
209:      *
210:      * @param bool $yes
211:      * @return Finder
212:      * @uses FilesystemIterator::FOLLOW_SYMLINKS
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:      * Sorts files and directories from a user defined function.
227:      * The anonymous function receives two \SplFileInfo instances to compare.
228:      * This can be slow as all the matching files and directories must be retrieved for comparison.
229:      *
230:      * @param callable $callback
231:      * @return Finder
232:      * @uses ArrayIterator::uasort()
233:      */
234:     public function sort(callable $callback)
235:     {
236:         $this->sorts[] = $callback;
237: 
238:         return $this;
239:     }
240: 
241:     /**
242:      * Sorts files and directories by name.
243:      * This can be slow as all the matching files and directories must be retrieved for comparison.
244:      *
245:      * @return Finder
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:      * Sorts files and directories by type.
256:      * This can be slow as all the matching files and directories must be retrieved for comparison.
257:      *
258:      * @return Finder
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:      * Sorts files and directories by size.
269:      * This can be slow as all the matching files and directories must be retrieved for comparison.
270:      *
271:      * @return Finder
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:      * Sorts files and directories by file extension.
282:      * This can be slow as all the matching files and directories must be retrieved for comparison.
283:      *
284:      * @return Finder
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:      * Sorts files and directories by path (without file name).
295:      * This can be slow as all the matching files and directories must be retrieved for comparison.
296:      *
297:      * @return Finder
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:      * Sorts files and directories by permissions.
308:      * This can be slow as all the matching files and directories must be retrieved for comparison.
309:      *
310:      * @return Finder
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:      * Sorts files and directories by accessed time.
321:      * This can be slow as all the matching files and directories must be retrieved for comparison.
322:      *
323:      * @return Finder
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:      * Sorts files and directories by modified time.
334:      * This can be slow as all the matching files and directories must be retrieved for comparison.
335:      *
336:      * @return Finder
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:      * Sorts files and directories by changed time.
347:      * This can be slow as all the matching files and directories must be retrieved for comparison.
348:      *
349:      * @return Finder
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:      * Filters the iterator with a user defined function.
360:      * The anonymous function receives a SplFileInfo and must return false to remove files.
361:      *
362:      * @param callable $callback
363:      * @return Finder
364:      */
365:     public function filter(callable $callback)
366:     {
367:         $this->filters[] = $callback;
368: 
369:         return $this;
370:     }
371: 
372:     /**
373:      * Adds rules that files name must match.
374:      * You can use patterns regex, globs or simple strings.
375:      * Example : <code>$finder->withName('*.php')</code> will produce same result as
376:      * <code>$finder->name('/.php$/')</code>
377:      *
378:      * @param string $name
379:      * @return Finder
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:      * Adds rules that files name must NOT match.
415:      * You can use patterns regex, globs or simple strings.
416:      * Example : <code>$finder->withoutName('*.php')</code> will produce same result as
417:      * <code>$finder->name('/.php$/')</code>
418:      *
419:      * @param $name
420:      * @return Finder
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:      * Adds rules that files content must match.
456:      * You can use patterns regex or simple strings.
457:      * Example : <code>$finder->contains('Hello World')</code>
458:      * will produce same result as
459:      * <code>$finder->name('/Hello World/i')</code>
460:      *
461:      * @param string $pattern
462:      * @return Finder
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:      * Adds rules that files content must NOT match.
491:      * You can use patterns regex or simple strings.
492:      * Example : <code>$finder->doesNotContain('Hello World')</code>
493:      * will produce same result as
494:      * <code>$finder->name('/Hello World/i')</code>
495:      *
496:      * @param $pattern
497:      * @return Finder
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:      * Restrict files and directories by path.
526:      * You can use patterns regex or simple strings.
527:      *
528:      * @param string $path
529:      * @return Finder
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:      * Exclude files and directories by path.
554:      * You can use patterns regex or simple strings.
555:      *
556:      * @param string $path
557:      * @return Finder
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:      * Adds tests for file dates (last modified).
583:      * Remove the file if <code>$current->getCTime() >= strtotime($date)</code>.
584:      * The date must be something that strtotime() is able to parse.
585:      *
586:      * @param string $date
587:      * @return Finder
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:      * Adds tests for file sizes in bytes.
598:      * Remove the file if <code>$current->getSize() >= $size</code>.
599:      *
600:      * @param string $size
601:      * @return Finder
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:      * Set the maximum allowed depth.
612:      *
613:      * @param int $depth
614:      * @return $this
615:      */
616:     public function depth($depth)
617:     {
618:         $this->depth = max(-1, (int)$depth);
619: 
620:         return $this;
621:     }
622: 
623:     /**
624:      * Merge an other Finder or Iterator or simple array instance with the current Finder instance.
625:      *
626:      * @param Finder|Iterator|array $iterator
627:      * @return Finder
628:      * @throws InvalidArgumentException
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:      * Retrieve an external iterator.
650:      *
651:      * @link http://php.net/manual/en/iteratoraggregate.getiterator.php
652:      * @return Iterator An instance of an object implementing <b>Iterator</b>
653:      * @since 5.0.0
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:      * Count elements of an object.
711:      *
712:      * @link http://php.net/manual/en/countable.count.php
713:      * @return int The custom count as an integer.
714:      * @since 5.1.0
715:      */
716:     public function count()
717:     {
718:         return (int)iterator_count($this->getIterator());
719:     }
720: 
721:     /**
722:      * @param ...mixed
723:      * @return array Flatten parameters into a single array.
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: 
API documentation generated by ApiGen