19
19
use Iterator ;
20
20
use IteratorAggregate ;
21
21
use LogicException ;
22
- use RecursiveArrayIterator ;
23
22
use RecursiveDirectoryIterator ;
24
23
use RecursiveIterator ;
25
24
use RecursiveIteratorIterator ;
28
27
use Toolkit \Stdlib \Str ;
29
28
use Traversable ;
30
29
use UnexpectedValueException ;
31
- use function array_flip ;
32
30
use function array_merge ;
33
31
use function closedir ;
34
32
use function count ;
61
59
*/
62
60
final class FileFinder implements IteratorAggregate, Countable
63
61
{
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 ;
67
65
68
66
public const IGNORE_VCS_FILES = 1 ;
69
67
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
+ ];
70
75
71
76
/** @var array */
72
77
private static array $ vcsPatterns = ['.svn ' , '_svn ' , 'CVS ' , '_darcs ' , '.arch-params ' , '.monotone ' , '.bzr ' , '.git ' , '.hg ' ];
@@ -77,15 +82,24 @@ final class FileFinder implements IteratorAggregate, Countable
77
82
/** @var int */
78
83
private int $ ignore ;
79
84
85
+ /** @var bool */
86
+ private bool $ initialized = false ;
87
+
88
+ /** @var bool recursive sub-dirs */
89
+ private bool $ recursive = true ;
90
+
80
91
/** @var bool */
81
92
private bool $ ignoreVcsAdded = false ;
82
93
83
94
/** @var bool */
84
95
private bool $ skipUnreadableDirs = true ;
85
96
86
- /** @var array */
97
+ /** @var array The find dirs */
87
98
private array $ dirs = [];
88
99
100
+ /** @var array<string> exclude pattern for directory names and each sub-dirs */
101
+ private array $ excludes = [];
102
+
89
103
/**
90
104
* add include file,dir name match.
91
105
*
@@ -96,7 +110,7 @@ final class FileFinder implements IteratorAggregate, Countable
96
110
private array $ names = [];
97
111
98
112
/**
99
- * add exclude file,dir name patterns
113
+ * add exclude file,dir name patterns, but sub-dir will not be exclude.
100
114
*
101
115
* eg: '.php' '*.php' 'test.php'
102
116
*
@@ -110,17 +124,14 @@ final class FileFinder implements IteratorAggregate, Countable
110
124
/** @var array<string> exclude paths pattern */
111
125
private array $ notPaths = [];
112
126
113
- /** @var array<string> exclude directory names */
114
- private array $ excludes = [];
115
-
116
127
/**
117
128
* path filters. each filter like: `Closure(SplFileInfo):bool`, return FALSE to exclude.
118
129
*
119
130
* @var array
120
131
*/
121
132
private array $ filters = [];
122
133
123
- /** @var array */
134
+ /** @var Traversable[] */
124
135
private array $ iterators = [];
125
136
126
137
/** @var bool */
@@ -363,7 +374,7 @@ public function addNotPaths(array|string $patterns): self
363
374
}
364
375
365
376
/**
366
- * exclude directory names
377
+ * exclude pattern for directory names and each sub-dirs
367
378
*
368
379
* @param array|string $dirNames
369
380
*
@@ -391,7 +402,6 @@ public function ignoreVCS(bool $ignoreVCS): self
391
402
} else {
392
403
$ this ->ignore &= ~self ::IGNORE_VCS_FILES ;
393
404
}
394
-
395
405
return $ this ;
396
406
}
397
407
@@ -410,6 +420,21 @@ public function ignoreDotFiles(bool $ignoreDotFiles = true): self
410
420
return $ this ;
411
421
}
412
422
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
+
413
438
/**
414
439
* @param bool $skipUnreadableDirs
415
440
*
@@ -451,6 +476,26 @@ public function followLinks(mixed $followLinks = true): self
451
476
return $ this ;
452
477
}
453
478
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
+
454
499
/**
455
500
* @param Closure(SplFileInfo): bool $closure
456
501
*
@@ -501,7 +546,7 @@ public function append(mixed $iterator): self
501
546
$ this ->iterators [] = $ iterator ->getIterator ();
502
547
} elseif ($ iterator instanceof Iterator) {
503
548
$ this ->iterators [] = $ iterator ;
504
- // } elseif (\is_array($iterator) || $iterator instanceof Traversable) {
549
+ // } elseif (\is_array($iterator) || $iterator instanceof Traversable) {
505
550
} elseif (is_iterable ($ iterator )) {
506
551
$ it = new ArrayIterator ();
507
552
foreach ($ iterator as $ file ) {
@@ -520,18 +565,40 @@ public function append(mixed $iterator): self
520
565
*/
521
566
public function getInfo (): array
522
567
{
568
+ $ this ->initialize ();
523
569
$ info = get_object_vars ($ this );
524
570
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 ];
532
573
return $ info ;
533
574
}
534
575
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
+
535
602
/**
536
603
* @return int
537
604
*/
@@ -558,27 +625,25 @@ public function each(callable $fn): void
558
625
}
559
626
}
560
627
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
+
561
638
/**
562
639
* Retrieve an external iterator
563
640
*
564
641
* @link http://php.net/manual/en/iteratoraggregate.getiterator.php
565
- * @return Traversable An Traversable
566
- * @psalm-return SplFileInfo[]
642
+ * @return Traversable<SplFileInfo> An Traversable
567
643
*/
568
644
public function getIterator (): Traversable
569
645
{
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 ();
582
647
583
648
if (1 === count ($ this ->dirs ) && 0 === count ($ this ->iterators )) {
584
649
return $ this ->findInDirectory ($ this ->dirs [0 ]);
@@ -608,21 +673,23 @@ private function findInDirectory(string $dir): Iterator
608
673
$ flags |= RecursiveDirectoryIterator::FOLLOW_SYMLINKS ;
609
674
}
610
675
611
- $ iterator = new class ($ dir , $ flags , $ this ->skipUnreadableDirs ) extends RecursiveDirectoryIterator {
676
+ $ iterator = new class ($ dir , $ flags , $ this ->recursive , $ this -> skipUnreadableDirs ) extends RecursiveDirectoryIterator {
612
677
private string $ rootPath ;
613
678
private string $ subPath = '' ;
679
+ private bool $ recursive ;
614
680
private bool |null $ rewindable = null ;
615
681
616
682
private string $ directorySep = '/ ' ;
617
683
private bool $ skipUnreadableDirs ;
618
684
619
- public function __construct (string $ path , int $ flags , bool $ skipUnreadableDirs = true )
685
+ public function __construct (string $ path , int $ flags , bool $ recursive , bool $ skipUnreadableDirs = true )
620
686
{
621
687
if ($ flags & (self ::CURRENT_AS_PATHNAME | self ::CURRENT_AS_SELF )) {
622
688
throw new RuntimeException ('This iterator only support returning current as fileInfo. ' );
623
689
}
624
690
625
691
$ this ->rootPath = $ path ;
692
+ $ this ->recursive = $ recursive ;
626
693
$ this ->skipUnreadableDirs = $ skipUnreadableDirs ;
627
694
parent ::__construct ($ path , $ flags );
628
695
@@ -654,6 +721,14 @@ public function current(): SplFileInfo
654
721
return $ fileInfo ;
655
722
}
656
723
724
+ public function hasChildren (bool $ allowLinks = false ): bool
725
+ {
726
+ if (!$ this ->recursive ) {
727
+ return false ;
728
+ }
729
+ return parent ::hasChildren ($ allowLinks );
730
+ }
731
+
657
732
public function getChildren (): RecursiveDirectoryIterator
658
733
{
659
734
try {
@@ -668,6 +743,7 @@ public function getChildren(): RecursiveDirectoryIterator
668
743
} catch (UnexpectedValueException $ e ) {
669
744
if ($ this ->skipUnreadableDirs ) {
670
745
return new RecursiveArrayIterator ([]);
746
+ // return new RecursiveDirectoryIterator([]);
671
747
}
672
748
673
749
throw new RuntimeException ($ e ->getMessage (), $ e ->getCode (), $ e );
@@ -705,23 +781,32 @@ public function isRewindable(): ?bool
705
781
// exclude directories
706
782
if ($ this ->excludes ) {
707
783
$ iterator = new class ($ iterator , $ this ->excludes ) extends FilterIterator implements RecursiveIterator {
708
- /** @var array<string, int > */
784
+ /** @var array<string> */
709
785
private array $ excludes ;
710
786
711
787
private RecursiveIterator $ iterator ;
712
788
713
789
public function __construct (RecursiveIterator $ iterator , array $ excludes )
714
790
{
715
- $ this ->excludes = array_flip ( $ excludes) ;
791
+ $ this ->excludes = $ excludes ;
716
792
$ this ->iterator = $ iterator ;
717
793
718
794
parent ::__construct ($ iterator );
719
795
}
720
796
721
797
public function accept (): bool
722
798
{
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 ;
725
810
}
726
811
727
812
public function hasChildren (): bool
@@ -738,7 +823,6 @@ public function getChildren(): ?RecursiveIterator
738
823
$ children = new self ($ child , []);
739
824
// sync
740
825
$ children ->excludes = $ this ->excludes ;
741
-
742
826
return $ children ;
743
827
}
744
828
};
@@ -769,7 +853,6 @@ public function accept(): bool
769
853
if (FileFinder::ONLY_FILE === $ this ->mode && $ info ->isDir ()) {
770
854
return false ;
771
855
}
772
-
773
856
return true ;
774
857
}
775
858
};
@@ -791,6 +874,7 @@ public function accept(): bool
791
874
{
792
875
$ filename = $ this ->current ()->getFilename ();
793
876
foreach ($ this ->notNames as $ not ) {
877
+ // vdump($not, $this->current()->getPathname(), $filename);
794
878
if ($ not === $ filename || fnmatch ($ not , $ filename )) {
795
879
return false ;
796
880
}
0 commit comments