1919use Iterator ;
2020use IteratorAggregate ;
2121use LogicException ;
22- use RecursiveArrayIterator ;
2322use RecursiveDirectoryIterator ;
2423use RecursiveIterator ;
2524use RecursiveIteratorIterator ;
2827use Toolkit \Stdlib \Str ;
2928use Traversable ;
3029use UnexpectedValueException ;
31- use function array_flip ;
3230use function array_merge ;
3331use function closedir ;
3432use function count ;
6159 */
6260final class FileFinder implements IteratorAggregate, Countable
6361{
64- public const MODE_ALL = 0 ;
65- public const ONLY_FILE = 1 ;
66- public const ONLY_DIR = 2 ;
62+ public const MODE_ALL = 0 ;
63+ public const ONLY_FILE = 1 ;
64+ public const ONLY_DIR = 2 ;
6765
6866 public const IGNORE_VCS_FILES = 1 ;
6967 public const IGNORE_DOT_FILES = 2 ;
68+ public const IGNORE_DOT_DIRS = 4 ;
69+
70+ public const MODE2DESC = [
71+ self ::MODE_ALL => 'ALL ' ,
72+ self ::ONLY_DIR => 'DIR ' ,
73+ self ::ONLY_FILE => 'FILE ' ,
74+ ];
7075
7176 /** @var array */
7277 private static array $ vcsPatterns = ['.svn ' , '_svn ' , 'CVS ' , '_darcs ' , '.arch-params ' , '.monotone ' , '.bzr ' , '.git ' , '.hg ' ];
@@ -77,15 +82,24 @@ final class FileFinder implements IteratorAggregate, Countable
7782 /** @var int */
7883 private int $ ignore ;
7984
85+ /** @var bool */
86+ private bool $ initialized = false ;
87+
88+ /** @var bool recursive sub-dirs */
89+ private bool $ recursive = true ;
90+
8091 /** @var bool */
8192 private bool $ ignoreVcsAdded = false ;
8293
8394 /** @var bool */
8495 private bool $ skipUnreadableDirs = true ;
8596
86- /** @var array */
97+ /** @var array The find dirs */
8798 private array $ dirs = [];
8899
100+ /** @var array<string> exclude pattern for directory names and each sub-dirs */
101+ private array $ excludes = [];
102+
89103 /**
90104 * add include file,dir name match.
91105 *
@@ -96,7 +110,7 @@ final class FileFinder implements IteratorAggregate, Countable
96110 private array $ names = [];
97111
98112 /**
99- * add exclude file,dir name patterns
113+ * add exclude file,dir name patterns, but sub-dir will not be exclude.
100114 *
101115 * eg: '.php' '*.php' 'test.php'
102116 *
@@ -110,17 +124,14 @@ final class FileFinder implements IteratorAggregate, Countable
110124 /** @var array<string> exclude paths pattern */
111125 private array $ notPaths = [];
112126
113- /** @var array<string> exclude directory names */
114- private array $ excludes = [];
115-
116127 /**
117128 * path filters. each filter like: `Closure(SplFileInfo):bool`, return FALSE to exclude.
118129 *
119130 * @var array
120131 */
121132 private array $ filters = [];
122133
123- /** @var array */
134+ /** @var Traversable[] */
124135 private array $ iterators = [];
125136
126137 /** @var bool */
@@ -363,7 +374,7 @@ public function addNotPaths(array|string $patterns): self
363374 }
364375
365376 /**
366- * exclude directory names
377+ * exclude pattern for directory names and each sub-dirs
367378 *
368379 * @param array|string $dirNames
369380 *
@@ -391,7 +402,6 @@ public function ignoreVCS(bool $ignoreVCS): self
391402 } else {
392403 $ this ->ignore &= ~self ::IGNORE_VCS_FILES ;
393404 }
394-
395405 return $ this ;
396406 }
397407
@@ -410,6 +420,21 @@ public function ignoreDotFiles(bool $ignoreDotFiles = true): self
410420 return $ this ;
411421 }
412422
423+ /**
424+ * @param bool $ignoreDotDirs
425+ *
426+ * @return FileFinder
427+ */
428+ public function ignoreDotDirs (bool $ ignoreDotDirs = true ): self
429+ {
430+ if ($ ignoreDotDirs ) {
431+ $ this ->ignore |= self ::IGNORE_DOT_DIRS ;
432+ } else {
433+ $ this ->ignore &= ~self ::IGNORE_DOT_DIRS ;
434+ }
435+ return $ this ;
436+ }
437+
413438 /**
414439 * @param bool $skipUnreadableDirs
415440 *
@@ -451,6 +476,26 @@ public function followLinks(mixed $followLinks = true): self
451476 return $ this ;
452477 }
453478
479+ /**
480+ * @return $this
481+ */
482+ public function notRecursive (): self
483+ {
484+ $ this ->recursive = false ;
485+ return $ this ;
486+ }
487+
488+ /**
489+ * @param bool $recursive
490+ *
491+ * @return $this
492+ */
493+ public function recursiveDir (bool $ recursive ): self
494+ {
495+ $ this ->recursive = $ recursive ;
496+ return $ this ;
497+ }
498+
454499 /**
455500 * @param Closure(SplFileInfo): bool $closure
456501 *
@@ -501,7 +546,7 @@ public function append(mixed $iterator): self
501546 $ this ->iterators [] = $ iterator ->getIterator ();
502547 } elseif ($ iterator instanceof Iterator) {
503548 $ this ->iterators [] = $ iterator ;
504- // } elseif (\is_array($iterator) || $iterator instanceof Traversable) {
549+ // } elseif (\is_array($iterator) || $iterator instanceof Traversable) {
505550 } elseif (is_iterable ($ iterator )) {
506551 $ it = new ArrayIterator ();
507552 foreach ($ iterator as $ file ) {
@@ -520,18 +565,40 @@ public function append(mixed $iterator): self
520565 */
521566 public function getInfo (): array
522567 {
568+ $ this ->initialize ();
523569 $ info = get_object_vars ($ this );
524570
525- $ mode2desc = [
526- self ::MODE_ALL => 'ALL ' ,
527- self ::ONLY_DIR => 'DIR ' ,
528- self ::ONLY_FILE => 'FILE ' ,
529- ];
530- $ info ['mode ' ] = $ mode2desc [$ this ->mode ];
531-
571+ // change mode value
572+ $ info ['mode ' ] = self ::MODE2DESC [$ this ->mode ];
532573 return $ info ;
533574 }
534575
576+ protected function initialize (): void
577+ {
578+ if ($ this ->initialized ) {
579+ return ;
580+ }
581+
582+ if (0 === count ($ this ->dirs ) && 0 === count ($ this ->iterators )) {
583+ throw new LogicException ('You must call one of in() or append() methods before iterating over a Finder. ' );
584+ }
585+
586+ if (!$ this ->ignoreVcsAdded && self ::IGNORE_VCS_FILES === (self ::IGNORE_VCS_FILES & $ this ->ignore )) {
587+ $ this ->excludes = array_merge ($ this ->excludes , self ::$ vcsPatterns );
588+ $ this ->ignoreVcsAdded = true ;
589+ }
590+
591+ if (self ::IGNORE_DOT_DIRS === (self ::IGNORE_DOT_DIRS & $ this ->ignore )) {
592+ $ this ->excludes [] = '.* ' ;
593+ }
594+
595+ if (self ::IGNORE_DOT_FILES === (self ::IGNORE_DOT_FILES & $ this ->ignore )) {
596+ $ this ->notNames [] = '.* ' ;
597+ }
598+
599+ $ this ->initialized = true ;
600+ }
601+
535602 /**
536603 * @return int
537604 */
@@ -558,27 +625,25 @@ public function each(callable $fn): void
558625 }
559626 }
560627
628+ /**
629+ * Retrieve an external iterator
630+ *
631+ * @return Traversable<SplFileInfo> An Traversable
632+ */
633+ public function all (): Traversable
634+ {
635+ return $ this ->getIterator ();
636+ }
637+
561638 /**
562639 * Retrieve an external iterator
563640 *
564641 * @link http://php.net/manual/en/iteratoraggregate.getiterator.php
565- * @return Traversable An Traversable
566- * @psalm-return SplFileInfo[]
642+ * @return Traversable<SplFileInfo> An Traversable
567643 */
568644 public function getIterator (): Traversable
569645 {
570- if (0 === count ($ this ->dirs ) && 0 === count ($ this ->iterators )) {
571- throw new LogicException ('You must call one of in() or append() methods before iterating over a Finder. ' );
572- }
573-
574- if (!$ this ->ignoreVcsAdded && self ::IGNORE_VCS_FILES === (self ::IGNORE_VCS_FILES & $ this ->ignore )) {
575- $ this ->excludes = array_merge ($ this ->excludes , self ::$ vcsPatterns );
576- $ this ->ignoreVcsAdded = true ;
577- }
578-
579- if (self ::IGNORE_DOT_FILES === (self ::IGNORE_DOT_FILES & $ this ->ignore )) {
580- $ this ->notNames [] = '.* ' ;
581- }
646+ $ this ->initialize ();
582647
583648 if (1 === count ($ this ->dirs ) && 0 === count ($ this ->iterators )) {
584649 return $ this ->findInDirectory ($ this ->dirs [0 ]);
@@ -608,21 +673,23 @@ private function findInDirectory(string $dir): Iterator
608673 $ flags |= RecursiveDirectoryIterator::FOLLOW_SYMLINKS ;
609674 }
610675
611- $ iterator = new class ($ dir , $ flags , $ this ->skipUnreadableDirs ) extends RecursiveDirectoryIterator {
676+ $ iterator = new class ($ dir , $ flags , $ this ->recursive , $ this -> skipUnreadableDirs ) extends RecursiveDirectoryIterator {
612677 private string $ rootPath ;
613678 private string $ subPath = '' ;
679+ private bool $ recursive ;
614680 private bool |null $ rewindable = null ;
615681
616682 private string $ directorySep = '/ ' ;
617683 private bool $ skipUnreadableDirs ;
618684
619- public function __construct (string $ path , int $ flags , bool $ skipUnreadableDirs = true )
685+ public function __construct (string $ path , int $ flags , bool $ recursive , bool $ skipUnreadableDirs = true )
620686 {
621687 if ($ flags & (self ::CURRENT_AS_PATHNAME | self ::CURRENT_AS_SELF )) {
622688 throw new RuntimeException ('This iterator only support returning current as fileInfo. ' );
623689 }
624690
625691 $ this ->rootPath = $ path ;
692+ $ this ->recursive = $ recursive ;
626693 $ this ->skipUnreadableDirs = $ skipUnreadableDirs ;
627694 parent ::__construct ($ path , $ flags );
628695
@@ -654,6 +721,14 @@ public function current(): SplFileInfo
654721 return $ fileInfo ;
655722 }
656723
724+ public function hasChildren (bool $ allowLinks = false ): bool
725+ {
726+ if (!$ this ->recursive ) {
727+ return false ;
728+ }
729+ return parent ::hasChildren ($ allowLinks );
730+ }
731+
657732 public function getChildren (): RecursiveDirectoryIterator
658733 {
659734 try {
@@ -668,6 +743,7 @@ public function getChildren(): RecursiveDirectoryIterator
668743 } catch (UnexpectedValueException $ e ) {
669744 if ($ this ->skipUnreadableDirs ) {
670745 return new RecursiveArrayIterator ([]);
746+ // return new RecursiveDirectoryIterator([]);
671747 }
672748
673749 throw new RuntimeException ($ e ->getMessage (), $ e ->getCode (), $ e );
@@ -705,23 +781,32 @@ public function isRewindable(): ?bool
705781 // exclude directories
706782 if ($ this ->excludes ) {
707783 $ iterator = new class ($ iterator , $ this ->excludes ) extends FilterIterator implements RecursiveIterator {
708- /** @var array<string, int > */
784+ /** @var array<string> */
709785 private array $ excludes ;
710786
711787 private RecursiveIterator $ iterator ;
712788
713789 public function __construct (RecursiveIterator $ iterator , array $ excludes )
714790 {
715- $ this ->excludes = array_flip ( $ excludes) ;
791+ $ this ->excludes = $ excludes ;
716792 $ this ->iterator = $ iterator ;
717793
718794 parent ::__construct ($ iterator );
719795 }
720796
721797 public function accept (): bool
722798 {
723- $ name = $ this ->current ()->getFilename ();
724- return !($ this ->current ()->isDir () && isset ($ this ->excludes [$ name ]));
799+ if ($ this ->current ()->isDir ()) {
800+ $ name = $ this ->current ()->getFilename ();
801+
802+ foreach ($ this ->excludes as $ not ) {
803+ if ($ not === $ name || fnmatch ($ not , $ name )) {
804+ return false ;
805+ }
806+ }
807+ }
808+
809+ return true ;
725810 }
726811
727812 public function hasChildren (): bool
@@ -738,7 +823,6 @@ public function getChildren(): ?RecursiveIterator
738823 $ children = new self ($ child , []);
739824 // sync
740825 $ children ->excludes = $ this ->excludes ;
741-
742826 return $ children ;
743827 }
744828 };
@@ -769,7 +853,6 @@ public function accept(): bool
769853 if (FileFinder::ONLY_FILE === $ this ->mode && $ info ->isDir ()) {
770854 return false ;
771855 }
772-
773856 return true ;
774857 }
775858 };
@@ -791,6 +874,7 @@ public function accept(): bool
791874 {
792875 $ filename = $ this ->current ()->getFilename ();
793876 foreach ($ this ->notNames as $ not ) {
877+ // vdump($not, $this->current()->getPathname(), $filename);
794878 if ($ not === $ filename || fnmatch ($ not , $ filename )) {
795879 return false ;
796880 }
0 commit comments