1<?php 2declare(strict_types=1); 3 4namespace ILIAS\Filesystem\Finder; 5 6use ILIAS\Filesystem\DTO\Metadata; 7use ILIAS\Filesystem\Filesystem; 8use ILIAS\Filesystem\MetadataType; 9 10/** 11 * Class Finder 12 * Port of the Symfony2 bundle to work with the ILIAS FileSystem abstraction 13 * @package ILIAS\Filesystem\Finder 14 * @see : https://github.com/symfony/finder 15 * @author Michael Jansen <mjansen@databay.de> 16 */ 17final class Finder implements \IteratorAggregate, \Countable 18{ 19 const IGNORE_VCS_FILES = 1; 20 const IGNORE_DOT_FILES = 2; 21 22 /** @var Filesystem */ 23 private $filesystem; 24 25 /** @var string[] */ 26 private $vcsPatterns = ['.svn', '_svn', 'CVS', '_darcs', '.arch-params', '.monotone', '.bzr', '.git', '.hg']; 27 28 /** @var \Iterator[] */ 29 private $iterators = []; 30 31 /** @var string[] */ 32 protected $dirs = []; 33 34 /** @var string[] */ 35 private $exclude = []; 36 37 /** @var int */ 38 private $ignore = 0; 39 40 /** @var int */ 41 private $mode = Iterator\FileTypeFilterIterator::ALL; 42 43 /** @var bool */ 44 private $reverseSorting = false; 45 46 /** @var Comparator\DateComparator[] */ 47 private $dates = []; 48 49 /** @var Comparator\NumberComparator[] */ 50 private $sizes = []; 51 52 /** @var Comparator\NumberComparator[] */ 53 private $depths = []; 54 55 /** @var bool */ 56 private $sort = false; 57 58 /** 59 * Finder constructor. 60 * @param Filesystem $filesystem 61 */ 62 public function __construct(Filesystem $filesystem) 63 { 64 $this->filesystem = $filesystem; 65 $this->ignore = static::IGNORE_VCS_FILES | static::IGNORE_DOT_FILES; 66 } 67 68 /** 69 * @return Finder 70 */ 71 public function files() : self 72 { 73 $clone = clone $this; 74 $clone->mode = Iterator\FileTypeFilterIterator::ONLY_FILES; 75 76 return $clone; 77 } 78 79 /** 80 * @return Finder 81 */ 82 public function directories() : self 83 { 84 $clone = clone $this; 85 $clone->mode = Iterator\FileTypeFilterIterator::ONLY_DIRECTORIES; 86 87 return $clone; 88 } 89 90 /** 91 * @return Finder 92 */ 93 public function allTypes() : self 94 { 95 $clone = clone $this; 96 $clone->mode = Iterator\FileTypeFilterIterator::ALL; 97 98 return $clone; 99 } 100 101 /** 102 * @param string[] $directories 103 * @return Finder 104 */ 105 public function exclude(array $directories) : self 106 { 107 array_walk($directories, function ($directory) { 108 if (!is_string($directory)) { 109 if (is_object($directory)) { 110 throw new \InvalidArgumentException(sprintf('Invalid directory given: %s', get_class($directory))); 111 } 112 113 throw new \InvalidArgumentException(sprintf('Invalid directory given: %s', gettype($directory))); 114 } 115 }); 116 117 $clone = clone $this; 118 $clone->exclude = array_merge($clone->exclude, $directories); 119 120 return $clone; 121 } 122 123 /** 124 * @param string[] $directories 125 * @return Finder 126 */ 127 public function in(array $directories) : self 128 { 129 array_walk($directories, function ($directory) { 130 if (!is_string($directory)) { 131 if (is_object($directory)) { 132 throw new \InvalidArgumentException(sprintf('Invalid directory given: %s', get_class($directory))); 133 } 134 135 throw new \InvalidArgumentException(sprintf('Invalid directory given: %s', gettype($directory))); 136 } 137 }); 138 139 $clone = clone $this; 140 $clone->dirs = array_unique(array_merge($clone->dirs, $directories)); 141 142 return $clone; 143 } 144 145 /** 146 * Adds tests for the directory depth. 147 * Usage: 148 * 149 * $finder->depth('> 1') // the Finder will start matching at level 1. 150 * $finder->depth('< 3') // the Finder will descend at most 3 levels of directories below the starting point. 151 * 152 * @param string|int $level The depth level expression 153 * @return Finder 154 * @see DepthRangeFilterIterator 155 * @see NumberComparator 156 */ 157 public function depth($level) : self 158 { 159 $clone = clone $this; 160 $clone->depths[] = new Comparator\NumberComparator((string) $level); 161 162 return $clone; 163 } 164 165 /** 166 * Adds tests for file dates. 167 * The date must be something that strtotime() is able to parse: 168 * 169 * $finder->date('since yesterday'); 170 * $finder->date('until 2 days ago'); 171 * $finder->date('> now - 2 hours'); 172 * $finder->date('>= 2005-10-15'); 173 * 174 * @param string $date A date range string 175 * @return Finder 176 * @see strtotime 177 * @see DateRangeFilterIterator 178 * @see DateComparator 179 * @see \ILIAS\FileSystem\Filesystem::getTimestamp() 180 */ 181 public function date(string $date) : self 182 { 183 $clone = clone $this; 184 $clone->dates[] = new Comparator\DateComparator($date); 185 186 return $clone; 187 } 188 189 /** 190 * Adds tests for file sizes. 191 * 192 * $finder->size('> 10K'); 193 * $finder->size('<= 1Ki'); 194 * $finder->size(4); 195 * $finder->size(['> 10K', '< 20K']) 196 * 197 * @param string|int|string[]|int[] $sizes A size range string or an integer or an array of size ranges 198 * @return Finder 199 * @see SizeRangeFilterIterator 200 * @see NumberComparator 201 * @see \ILIAS\FileSystem\Filesystem::getSize() 202 */ 203 public function size($sizes) : self 204 { 205 if (!is_array($sizes)) { 206 $sizes = [$sizes]; 207 } 208 209 $clone = clone $this; 210 211 foreach ($sizes as $size) { 212 $clone->sizes[] = new Comparator\NumberComparator((string) $size); 213 } 214 215 return $clone; 216 } 217 218 /** 219 * @return Finder 220 */ 221 public function reverseSorting() : self 222 { 223 $clone = clone $this; 224 $clone->reverseSorting = true; 225 226 return $clone; 227 } 228 229 /** 230 * @param bool $ignoreVCS 231 * @return Finder 232 */ 233 public function ignoreVCS(bool $ignoreVCS) : self 234 { 235 $clone = clone $this; 236 if ($ignoreVCS) { 237 $clone->ignore |= static::IGNORE_VCS_FILES; 238 } else { 239 $clone->ignore &= ~static::IGNORE_VCS_FILES; 240 } 241 242 return $clone; 243 } 244 245 /** 246 * @param string[] $pattern 247 * @return Finder 248 */ 249 public function addVCSPattern(array $pattern) : self 250 { 251 array_walk($pattern, function ($p) { 252 if (!is_string($p)) { 253 if (is_object($p)) { 254 throw new \InvalidArgumentException(sprintf('Invalid pattern given: %s', get_class($p))); 255 } 256 257 throw new \InvalidArgumentException(sprintf('Invalid pattern given: %s', gettype($p))); 258 } 259 }); 260 261 $clone = clone $this; 262 foreach ($pattern as $p) { 263 $clone->vcsPatterns[] = $p; 264 } 265 266 $clone->vcsPatterns = array_unique($clone->vcsPatterns); 267 268 return $clone; 269 } 270 271 /** 272 * Sorts files and directories by an anonymous function. 273 * The anonymous function receives two Metadata instances to compare. 274 * This can be slow as all the matching files and directories must be retrieved for comparison. 275 * @param \Closure $closure 276 * @return Finder 277 */ 278 public function sort(\Closure $closure) : self 279 { 280 $clone = clone $this; 281 $clone->sort = $closure; 282 283 return $clone; 284 } 285 286 /** 287 * @param bool $useNaturalSort 288 * @return Finder 289 */ 290 public function sortByName(bool $useNaturalSort = false) : self 291 { 292 $clone = clone $this; 293 $clone->sort = Iterator\SortableIterator::SORT_BY_NAME; 294 if ($useNaturalSort) { 295 $clone->sort = Iterator\SortableIterator::SORT_BY_NAME_NATURAL; 296 } 297 298 return $clone; 299 } 300 301 /** 302 * @return Finder 303 */ 304 public function sortByType() : self 305 { 306 $clone = clone $this; 307 $clone->sort = Iterator\SortableIterator::SORT_BY_TYPE; 308 309 return $clone; 310 } 311 312 /** 313 * @return Finder 314 */ 315 public function sortByTime() : self 316 { 317 $clone = clone $this; 318 $clone->sort = Iterator\SortableIterator::SORT_BY_TIME; 319 320 return $clone; 321 } 322 323 /** 324 * Appends an existing set of files/directories to the finder. 325 * The set can be another Finder, an Iterator, an IteratorAggregate, or even a plain array. 326 * @param iterable $iterator 327 * @return Finder 328 * @throws \InvalidArgumentException when the given argument is not iterable 329 */ 330 public function append(iterable $iterator) : self 331 { 332 $clone = clone $this; 333 334 if ($iterator instanceof \IteratorAggregate) { 335 $clone->iterators[] = $iterator->getIterator(); 336 } elseif ($iterator instanceof \Iterator) { 337 $clone->iterators[] = $iterator; 338 } elseif ($iterator instanceof \Traversable || is_array($iterator)) { 339 $it = new \ArrayIterator(); 340 foreach ($iterator as $file) { 341 if ($file instanceof MetadataType) { 342 $it->append($file); 343 } else { 344 throw new \InvalidArgumentException('Finder::append() method wrong argument type in passed iterator.'); 345 } 346 } 347 $clone->iterators[] = $it; 348 } else { 349 throw new \InvalidArgumentException('Finder::append() method wrong argument type.'); 350 } 351 352 return $clone; 353 } 354 355 /** 356 * @param string $dir 357 * @return \Iterator 358 */ 359 private function searchInDirectory(string $dir) : \Iterator 360 { 361 if (static::IGNORE_VCS_FILES === (static::IGNORE_VCS_FILES & $this->ignore)) { 362 $this->exclude = array_merge($this->exclude, $this->vcsPatterns); 363 } 364 365 $iterator = new Iterator\RecursiveDirectoryIterator($this->filesystem, $dir); 366 367 if ($this->exclude) { 368 $iterator = new Iterator\ExcludeDirectoryFilterIterator($iterator, $this->exclude); 369 } 370 371 $iterator = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::SELF_FIRST); 372 373 if ($this->depths) { 374 $iterator = new Iterator\DepthRangeFilterIterator($iterator, $this->depths); 375 } 376 377 if ($this->mode) { 378 $iterator = new Iterator\FileTypeFilterIterator($iterator, $this->mode); 379 } 380 381 if ($this->dates) { 382 $iterator = new Iterator\DateRangeFilterIterator($this->filesystem, $iterator, $this->dates); 383 } 384 385 if ($this->sizes) { 386 $iterator = new Iterator\SizeRangeFilterIterator($this->filesystem, $iterator, $this->sizes); 387 } 388 389 if ($this->sort || $this->reverseSorting) { 390 $iteratorAggregate = new Iterator\SortableIterator( 391 $this->filesystem, 392 $iterator, 393 $this->sort, 394 $this->reverseSorting 395 ); 396 $iterator = $iteratorAggregate->getIterator(); 397 } 398 399 return $iterator; 400 } 401 402 /** 403 * @inheritdoc 404 * @return \Iterator|Metadata[] 405 */ 406 public function getIterator() 407 { 408 if (0 === count($this->dirs) && 0 === count($this->iterators)) { 409 throw new \LogicException('You must call one of in() or append() methods before iterating over a Finder.'); 410 } 411 412 if (1 === count($this->dirs) && 0 === count($this->iterators)) { 413 return $this->searchInDirectory($this->dirs[0]); 414 } 415 416 $iterator = new \AppendIterator(); 417 foreach ($this->dirs as $dir) { 418 $iterator->append($this->searchInDirectory($dir)); 419 } 420 421 foreach ($this->iterators as $it) { 422 $iterator->append($it); 423 } 424 425 return $iterator; 426 } 427 428 /** 429 * @inheritdoc 430 */ 431 public function count() 432 { 433 return iterator_count($this->getIterator()); 434 } 435} 436