1 <?php
  2 
  3 /**
  4  * Finder.
  5  *
  6  * @author staabm
  7  * @author gharlan
  8  *
  9  * @package redaxo\core
 10  */
 11 class rex_finder implements IteratorAggregate, Countable
 12 {
 13     use rex_factory_trait;
 14 
 15     const ALL = '__ALL__';
 16 
 17     private $dir;
 18     private $recursive = false;
 19     private $recursiveMode = RecursiveIteratorIterator::SELF_FIRST;
 20     private $dirsOnly = false;
 21     private $ignoreFiles = [];
 22     private $ignoreFilesRecursive = [];
 23     private $ignoreDirs = [];
 24     private $ignoreDirsRecursive = [];
 25     private $ignoreSystemStuff = true;
 26     private $sort = false;
 27 
 28     /**
 29      * Contructor.
 30      *
 31      * @param string $dir
 32      */
 33     private function __construct($dir)
 34     {
 35         $this->dir = $dir;
 36     }
 37 
 38     /**
 39      * Returns a new finder object.
 40      *
 41      * @param string $dir Path to a directory
 42      *
 43      * @throws InvalidArgumentException
 44      *
 45      * @return static
 46      */
 47     public static function factory($dir)
 48     {
 49         if (!is_dir($dir)) {
 50             throw new InvalidArgumentException('Folder "' . $dir . '" not found!');
 51         }
 52 
 53         $class = static::getFactoryClass();
 54         return new $class($dir);
 55     }
 56 
 57     /**
 58      * Activate/Deactivate recursive directory scanning.
 59      *
 60      * @param bool $recursive
 61      *
 62      * @return $this
 63      */
 64     public function recursive($recursive = true)
 65     {
 66         $this->recursive = $recursive;
 67 
 68         return $this;
 69     }
 70 
 71     /**
 72      * Fetch directory contents before recurse its subdirectories.
 73      *
 74      * @return $this
 75      */
 76     public function selfFirst()
 77     {
 78         $this->recursiveMode = RecursiveIteratorIterator::SELF_FIRST;
 79 
 80         return $this;
 81     }
 82 
 83     /**
 84      * Fetch child directories before their parent directory.
 85      *
 86      * @return $this
 87      */
 88     public function childFirst()
 89     {
 90         $this->recursiveMode = RecursiveIteratorIterator::CHILD_FIRST;
 91 
 92         return $this;
 93     }
 94 
 95     /**
 96      * Fetch files only.
 97      *
 98      * @return $this
 99      */
100     public function filesOnly()
101     {
102         $this->recursiveMode = RecursiveIteratorIterator::LEAVES_ONLY;
103 
104         return $this;
105     }
106 
107     /**
108      * Fetch dirs only.
109      *
110      * @return $this
111      */
112     public function dirsOnly()
113     {
114         $this->dirsOnly = true;
115 
116         return $this;
117     }
118 
119     /**
120      * Ignore all files which match the given glob pattern.
121      *
122      * @param string|array $glob      Glob pattern or an array of glob patterns
123      * @param bool         $recursive When FALSE the patterns won't be checked in child directories
124      *
125      * @return $this
126      */
127     public function ignoreFiles($glob, $recursive = true)
128     {
129         $var = $recursive ? 'ignoreFilesRecursive' : 'ignoreFiles';
130         if (is_array($glob)) {
131             $this->$var += $glob;
132         } else {
133             array_push($this->$var, $glob);
134         }
135 
136         return $this;
137     }
138 
139     /**
140      * Ignore all directories which match the given glob pattern.
141      *
142      * @param string|array $glob      Glob pattern or an array of glob patterns
143      * @param bool         $recursive When FALSE the patterns won't be checked in child directories
144      *
145      * @return $this
146      */
147     public function ignoreDirs($glob, $recursive = true)
148     {
149         $var = $recursive ? 'ignoreDirsRecursive' : 'ignoreDirs';
150         if (is_array($glob)) {
151             $this->$var += $glob;
152         } else {
153             array_push($this->$var, $glob);
154         }
155 
156         return $this;
157     }
158 
159     /**
160      * Ignores system stuff (like .DS_Store, .svn, .git etc.).
161      *
162      * @param bool $ignoreSystemStuff
163      *
164      * @return $this
165      */
166     public function ignoreSystemStuff($ignoreSystemStuff = true)
167     {
168         $this->ignoreSystemStuff = $ignoreSystemStuff;
169 
170         return $this;
171     }
172 
173     /**
174      * Sorts the elements.
175      *
176      * @param int|callable $sort Sort mode, see {@link rex_sortable_iterator::__construct()}
177      *
178      * @return $this
179      */
180     public function sort($sort = rex_sortable_iterator::KEYS)
181     {
182         $this->sort = $sort;
183 
184         return $this;
185     }
186 
187     /**
188      * @return Iterator|SplFileInfo[]
189      */
190     public function getIterator()
191     {
192         $iterator = new RecursiveDirectoryIterator($this->dir, FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS);
193 
194         $iterator = new RecursiveCallbackFilterIterator($iterator, function (SplFileInfo $current, $key, $currentIterator) use ($iterator) {
195             $filename = $current->getFilename();
196             $isRoot = $currentIterator === $iterator;
197 
198             $match = function ($pattern, $filename) {
199                 $regex = '/^'.strtr(preg_quote($pattern, '/'), ['\*' => '.*', '\?' => '.']).'$/i';
200                 return preg_match($regex, $filename);
201             };
202 
203             if ($current->isFile()) {
204                 if ($this->dirsOnly) {
205                     return false;
206                 }
207                 $ignoreFiles = $isRoot ? array_merge($this->ignoreFiles, $this->ignoreFilesRecursive) : $this->ignoreFilesRecursive;
208                 foreach ($ignoreFiles as $ignore) {
209                     if ($match($ignore, $filename)) {
210                         return false;
211                     }
212                 }
213             }
214 
215             if ($current->isDir()) {
216                 if (!$this->recursive && $this->recursiveMode === RecursiveIteratorIterator::LEAVES_ONLY) {
217                     return false;
218                 }
219                 $ignoreDirs = $isRoot ? array_merge($this->ignoreDirs, $this->ignoreDirsRecursive) : $this->ignoreDirsRecursive;
220                 foreach ($ignoreDirs as $ignore) {
221                     if ($match($ignore, $filename)) {
222                         return false;
223                     }
224                 }
225             }
226 
227             if ($this->ignoreSystemStuff) {
228                 static $systemStuff = ['.DS_Store', 'Thumbs.db', 'desktop.ini', '.svn', '_svn', 'CVS', '_darcs', '.arch-params', '.monotone', '.bzr', '.git', '.hg'];
229                 foreach ($systemStuff as $systemStuffFile) {
230                     if (stripos($filename, $systemStuffFile) === 0) {
231                         return false;
232                     }
233                 }
234             }
235 
236             return true;
237         });
238 
239         if ($this->recursive) {
240             $iterator = new RecursiveIteratorIterator($iterator, $this->recursiveMode);
241         }
242 
243         if ($this->sort) {
244             $iterator = new rex_sortable_iterator($iterator, $this->sort);
245         }
246 
247         return $iterator;
248     }
249 
250     /**
251      * {@inheritdoc}
252      */
253     public function count()
254     {
255         return iterator_count($this->getIterator());
256     }
257 }
258