├── CHANGELOG.md ├── Comparator ├── Comparator.php ├── DateComparator.php └── NumberComparator.php ├── Exception ├── AccessDeniedException.php └── DirectoryNotFoundException.php ├── Finder.php ├── Gitignore.php ├── Glob.php ├── Iterator ├── CustomFilterIterator.php ├── DateRangeFilterIterator.php ├── DepthRangeFilterIterator.php ├── ExcludeDirectoryFilterIterator.php ├── FileTypeFilterIterator.php ├── FilecontentFilterIterator.php ├── FilenameFilterIterator.php ├── LazyIterator.php ├── MultiplePcreFilterIterator.php ├── PathFilterIterator.php ├── RecursiveDirectoryIterator.php ├── SizeRangeFilterIterator.php ├── SortableIterator.php └── VcsIgnoredFilterIterator.php ├── LICENSE ├── README.md ├── SplFileInfo.php └── composer.json /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | 6.4 5 | --- 6 | 7 | * Add early directory pruning to `Finder::filter()` 8 | 9 | 6.2 10 | --- 11 | 12 | * Add `Finder::sortByExtension()` and `Finder::sortBySize()` 13 | * Add `Finder::sortByCaseInsensitiveName()` to sort by name with case insensitive sorting methods 14 | 15 | 6.0 16 | --- 17 | 18 | * Remove `Comparator::setTarget()` and `Comparator::setOperator()` 19 | 20 | 5.4.0 21 | ----- 22 | 23 | * Deprecate `Comparator::setTarget()` and `Comparator::setOperator()` 24 | * Add a constructor to `Comparator` that allows setting target and operator 25 | * Finder's iterator has now `Symfony\Component\Finder\SplFileInfo` inner type specified 26 | * Add recursive .gitignore files support 27 | 28 | 5.0.0 29 | ----- 30 | 31 | * added `$useNaturalSort` argument to `Finder::sortByName()` 32 | 33 | 4.3.0 34 | ----- 35 | 36 | * added Finder::ignoreVCSIgnored() to ignore files based on rules listed in .gitignore 37 | 38 | 4.2.0 39 | ----- 40 | 41 | * added $useNaturalSort option to Finder::sortByName() method 42 | * the `Finder::sortByName()` method will have a new `$useNaturalSort` 43 | argument in version 5.0, not defining it is deprecated 44 | * added `Finder::reverseSorting()` to reverse the sorting 45 | 46 | 4.0.0 47 | ----- 48 | 49 | * removed `ExceptionInterface` 50 | * removed `Symfony\Component\Finder\Iterator\FilterIterator` 51 | 52 | 3.4.0 53 | ----- 54 | 55 | * deprecated `Symfony\Component\Finder\Iterator\FilterIterator` 56 | * added Finder::hasResults() method to check if any results were found 57 | 58 | 3.3.0 59 | ----- 60 | 61 | * added double-star matching to Glob::toRegex() 62 | 63 | 3.0.0 64 | ----- 65 | 66 | * removed deprecated classes 67 | 68 | 2.8.0 69 | ----- 70 | 71 | * deprecated adapters and related classes 72 | 73 | 2.5.0 74 | ----- 75 | * added support for GLOB_BRACE in the paths passed to Finder::in() 76 | 77 | 2.3.0 78 | ----- 79 | 80 | * added a way to ignore unreadable directories (via Finder::ignoreUnreadableDirs()) 81 | * unified the way subfolders that are not executable are handled by always throwing an AccessDeniedException exception 82 | 83 | 2.2.0 84 | ----- 85 | 86 | * added Finder::path() and Finder::notPath() methods 87 | * added finder adapters to improve performance on specific platforms 88 | * added support for wildcard characters (glob patterns) in the paths passed 89 | to Finder::in() 90 | 91 | 2.1.0 92 | ----- 93 | 94 | * added Finder::sortByAccessedTime(), Finder::sortByChangedTime(), and 95 | Finder::sortByModifiedTime() 96 | * added Countable to Finder 97 | * added support for an array of directories as an argument to 98 | Finder::exclude() 99 | * added searching based on the file content via Finder::contains() and 100 | Finder::notContains() 101 | * added support for the != operator in the Comparator 102 | * [BC BREAK] filter expressions (used for file name and content) are no more 103 | considered as regexps but glob patterns when they are enclosed in '*' or '?' 104 | -------------------------------------------------------------------------------- /Comparator/Comparator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Finder\Comparator; 13 | 14 | /** 15 | * @author Fabien Potencier 16 | */ 17 | class Comparator 18 | { 19 | private string $operator; 20 | 21 | public function __construct( 22 | private string $target, 23 | string $operator = '==', 24 | ) { 25 | if (!\in_array($operator, ['>', '<', '>=', '<=', '==', '!='])) { 26 | throw new \InvalidArgumentException(\sprintf('Invalid operator "%s".', $operator)); 27 | } 28 | 29 | $this->operator = $operator; 30 | } 31 | 32 | /** 33 | * Gets the target value. 34 | */ 35 | public function getTarget(): string 36 | { 37 | return $this->target; 38 | } 39 | 40 | /** 41 | * Gets the comparison operator. 42 | */ 43 | public function getOperator(): string 44 | { 45 | return $this->operator; 46 | } 47 | 48 | /** 49 | * Tests against the target. 50 | */ 51 | public function test(mixed $test): bool 52 | { 53 | return match ($this->operator) { 54 | '>' => $test > $this->target, 55 | '>=' => $test >= $this->target, 56 | '<' => $test < $this->target, 57 | '<=' => $test <= $this->target, 58 | '!=' => $test != $this->target, 59 | default => $test == $this->target, 60 | }; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Comparator/DateComparator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Finder\Comparator; 13 | 14 | /** 15 | * DateCompare compiles date comparisons. 16 | * 17 | * @author Fabien Potencier 18 | */ 19 | class DateComparator extends Comparator 20 | { 21 | /** 22 | * @param string $test A comparison string 23 | * 24 | * @throws \InvalidArgumentException If the test is not understood 25 | */ 26 | public function __construct(string $test) 27 | { 28 | if (!preg_match('#^\s*(==|!=|[<>]=?|after|since|before|until)?\s*(.+?)\s*$#i', $test, $matches)) { 29 | throw new \InvalidArgumentException(\sprintf('Don\'t understand "%s" as a date test.', $test)); 30 | } 31 | 32 | try { 33 | $date = new \DateTimeImmutable($matches[2]); 34 | $target = $date->format('U'); 35 | } catch (\Exception) { 36 | throw new \InvalidArgumentException(\sprintf('"%s" is not a valid date.', $matches[2])); 37 | } 38 | 39 | $operator = $matches[1] ?: '=='; 40 | if ('since' === $operator || 'after' === $operator) { 41 | $operator = '>'; 42 | } 43 | 44 | if ('until' === $operator || 'before' === $operator) { 45 | $operator = '<'; 46 | } 47 | 48 | parent::__construct($target, $operator); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Comparator/NumberComparator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Finder\Comparator; 13 | 14 | /** 15 | * NumberComparator compiles a simple comparison to an anonymous 16 | * subroutine, which you can call with a value to be tested again. 17 | * 18 | * Now this would be very pointless, if NumberCompare didn't understand 19 | * magnitudes. 20 | * 21 | * The target value may use magnitudes of kilobytes (k, ki), 22 | * megabytes (m, mi), or gigabytes (g, gi). Those suffixed 23 | * with an i use the appropriate 2**n version in accordance with the 24 | * IEC standard: http://physics.nist.gov/cuu/Units/binary.html 25 | * 26 | * Based on the Perl Number::Compare module. 27 | * 28 | * @author Fabien Potencier PHP port 29 | * @author Richard Clamp Perl version 30 | * @copyright 2004-2005 Fabien Potencier 31 | * @copyright 2002 Richard Clamp 32 | * 33 | * @see http://physics.nist.gov/cuu/Units/binary.html 34 | */ 35 | class NumberComparator extends Comparator 36 | { 37 | /** 38 | * @param string|null $test A comparison string or null 39 | * 40 | * @throws \InvalidArgumentException If the test is not understood 41 | */ 42 | public function __construct(?string $test) 43 | { 44 | if (null === $test || !preg_match('#^\s*(==|!=|[<>]=?)?\s*([0-9\.]+)\s*([kmg]i?)?\s*$#i', $test, $matches)) { 45 | throw new \InvalidArgumentException(\sprintf('Don\'t understand "%s" as a number test.', $test ?? 'null')); 46 | } 47 | 48 | $target = $matches[2]; 49 | if (!is_numeric($target)) { 50 | throw new \InvalidArgumentException(\sprintf('Invalid number "%s".', $target)); 51 | } 52 | if (isset($matches[3])) { 53 | // magnitude 54 | switch (strtolower($matches[3])) { 55 | case 'k': 56 | $target *= 1000; 57 | break; 58 | case 'ki': 59 | $target *= 1024; 60 | break; 61 | case 'm': 62 | $target *= 1000000; 63 | break; 64 | case 'mi': 65 | $target *= 1024 * 1024; 66 | break; 67 | case 'g': 68 | $target *= 1000000000; 69 | break; 70 | case 'gi': 71 | $target *= 1024 * 1024 * 1024; 72 | break; 73 | } 74 | } 75 | 76 | parent::__construct($target, $matches[1] ?: '=='); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Exception/AccessDeniedException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Finder\Exception; 13 | 14 | /** 15 | * @author Jean-François Simon 16 | */ 17 | class AccessDeniedException extends \UnexpectedValueException 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /Exception/DirectoryNotFoundException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Finder\Exception; 13 | 14 | /** 15 | * @author Andreas Erhard 16 | */ 17 | class DirectoryNotFoundException extends \InvalidArgumentException 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /Finder.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Finder; 13 | 14 | use Symfony\Component\Finder\Comparator\DateComparator; 15 | use Symfony\Component\Finder\Comparator\NumberComparator; 16 | use Symfony\Component\Finder\Exception\DirectoryNotFoundException; 17 | use Symfony\Component\Finder\Iterator\CustomFilterIterator; 18 | use Symfony\Component\Finder\Iterator\DateRangeFilterIterator; 19 | use Symfony\Component\Finder\Iterator\DepthRangeFilterIterator; 20 | use Symfony\Component\Finder\Iterator\ExcludeDirectoryFilterIterator; 21 | use Symfony\Component\Finder\Iterator\FilecontentFilterIterator; 22 | use Symfony\Component\Finder\Iterator\FilenameFilterIterator; 23 | use Symfony\Component\Finder\Iterator\LazyIterator; 24 | use Symfony\Component\Finder\Iterator\SizeRangeFilterIterator; 25 | use Symfony\Component\Finder\Iterator\SortableIterator; 26 | 27 | /** 28 | * Finder allows to build rules to find files and directories. 29 | * 30 | * It is a thin wrapper around several specialized iterator classes. 31 | * 32 | * All rules may be invoked several times. 33 | * 34 | * All methods return the current Finder object to allow chaining: 35 | * 36 | * $finder = Finder::create()->files()->name('*.php')->in(__DIR__); 37 | * 38 | * @author Fabien Potencier 39 | * 40 | * @implements \IteratorAggregate 41 | */ 42 | class Finder implements \IteratorAggregate, \Countable 43 | { 44 | public const IGNORE_VCS_FILES = 1; 45 | public const IGNORE_DOT_FILES = 2; 46 | public const IGNORE_VCS_IGNORED_FILES = 4; 47 | 48 | private int $mode = 0; 49 | private array $names = []; 50 | private array $notNames = []; 51 | private array $exclude = []; 52 | private array $filters = []; 53 | private array $pruneFilters = []; 54 | private array $depths = []; 55 | private array $sizes = []; 56 | private bool $followLinks = false; 57 | private bool $reverseSorting = false; 58 | private \Closure|int|false $sort = false; 59 | private int $ignore = 0; 60 | private array $dirs = []; 61 | private array $dates = []; 62 | private array $iterators = []; 63 | private array $contains = []; 64 | private array $notContains = []; 65 | private array $paths = []; 66 | private array $notPaths = []; 67 | private bool $ignoreUnreadableDirs = false; 68 | 69 | private static array $vcsPatterns = ['.svn', '_svn', 'CVS', '_darcs', '.arch-params', '.monotone', '.bzr', '.git', '.hg']; 70 | 71 | public function __construct() 72 | { 73 | $this->ignore = static::IGNORE_VCS_FILES | static::IGNORE_DOT_FILES; 74 | } 75 | 76 | /** 77 | * Creates a new Finder. 78 | */ 79 | public static function create(): static 80 | { 81 | return new static(); 82 | } 83 | 84 | /** 85 | * Restricts the matching to directories only. 86 | * 87 | * @return $this 88 | */ 89 | public function directories(): static 90 | { 91 | $this->mode = Iterator\FileTypeFilterIterator::ONLY_DIRECTORIES; 92 | 93 | return $this; 94 | } 95 | 96 | /** 97 | * Restricts the matching to files only. 98 | * 99 | * @return $this 100 | */ 101 | public function files(): static 102 | { 103 | $this->mode = Iterator\FileTypeFilterIterator::ONLY_FILES; 104 | 105 | return $this; 106 | } 107 | 108 | /** 109 | * Adds tests for the directory depth. 110 | * 111 | * Usage: 112 | * 113 | * $finder->depth('> 1') // the Finder will start matching at level 1. 114 | * $finder->depth('< 3') // the Finder will descend at most 3 levels of directories below the starting point. 115 | * $finder->depth(['>= 1', '< 3']) 116 | * 117 | * @param string|int|string[]|int[] $levels The depth level expression or an array of depth levels 118 | * 119 | * @return $this 120 | * 121 | * @see DepthRangeFilterIterator 122 | * @see NumberComparator 123 | */ 124 | public function depth(string|int|array $levels): static 125 | { 126 | foreach ((array) $levels as $level) { 127 | $this->depths[] = new NumberComparator($level); 128 | } 129 | 130 | return $this; 131 | } 132 | 133 | /** 134 | * Adds tests for file dates (last modified). 135 | * 136 | * The date must be something that strtotime() is able to parse: 137 | * 138 | * $finder->date('since yesterday'); 139 | * $finder->date('until 2 days ago'); 140 | * $finder->date('> now - 2 hours'); 141 | * $finder->date('>= 2005-10-15'); 142 | * $finder->date(['>= 2005-10-15', '<= 2006-05-27']); 143 | * 144 | * @param string|string[] $dates A date range string or an array of date ranges 145 | * 146 | * @return $this 147 | * 148 | * @see strtotime 149 | * @see DateRangeFilterIterator 150 | * @see DateComparator 151 | */ 152 | public function date(string|array $dates): static 153 | { 154 | foreach ((array) $dates as $date) { 155 | $this->dates[] = new DateComparator($date); 156 | } 157 | 158 | return $this; 159 | } 160 | 161 | /** 162 | * Adds rules that files must match. 163 | * 164 | * You can use patterns (delimited with / sign), globs or simple strings. 165 | * 166 | * $finder->name('/\.php$/') 167 | * $finder->name('*.php') // same as above, without dot files 168 | * $finder->name('test.php') 169 | * $finder->name(['test.py', 'test.php']) 170 | * 171 | * @param string|string[] $patterns A pattern (a regexp, a glob, or a string) or an array of patterns 172 | * 173 | * @return $this 174 | * 175 | * @see FilenameFilterIterator 176 | */ 177 | public function name(string|array $patterns): static 178 | { 179 | $this->names = array_merge($this->names, (array) $patterns); 180 | 181 | return $this; 182 | } 183 | 184 | /** 185 | * Adds rules that files must not match. 186 | * 187 | * @param string|string[] $patterns A pattern (a regexp, a glob, or a string) or an array of patterns 188 | * 189 | * @return $this 190 | * 191 | * @see FilenameFilterIterator 192 | */ 193 | public function notName(string|array $patterns): static 194 | { 195 | $this->notNames = array_merge($this->notNames, (array) $patterns); 196 | 197 | return $this; 198 | } 199 | 200 | /** 201 | * Adds tests that file contents must match. 202 | * 203 | * Strings or PCRE patterns can be used: 204 | * 205 | * $finder->contains('Lorem ipsum') 206 | * $finder->contains('/Lorem ipsum/i') 207 | * $finder->contains(['dolor', '/ipsum/i']) 208 | * 209 | * @param string|string[] $patterns A pattern (string or regexp) or an array of patterns 210 | * 211 | * @return $this 212 | * 213 | * @see FilecontentFilterIterator 214 | */ 215 | public function contains(string|array $patterns): static 216 | { 217 | $this->contains = array_merge($this->contains, (array) $patterns); 218 | 219 | return $this; 220 | } 221 | 222 | /** 223 | * Adds tests that file contents must not match. 224 | * 225 | * Strings or PCRE patterns can be used: 226 | * 227 | * $finder->notContains('Lorem ipsum') 228 | * $finder->notContains('/Lorem ipsum/i') 229 | * $finder->notContains(['lorem', '/dolor/i']) 230 | * 231 | * @param string|string[] $patterns A pattern (string or regexp) or an array of patterns 232 | * 233 | * @return $this 234 | * 235 | * @see FilecontentFilterIterator 236 | */ 237 | public function notContains(string|array $patterns): static 238 | { 239 | $this->notContains = array_merge($this->notContains, (array) $patterns); 240 | 241 | return $this; 242 | } 243 | 244 | /** 245 | * Adds rules that filenames must match. 246 | * 247 | * You can use patterns (delimited with / sign) or simple strings. 248 | * 249 | * $finder->path('some/special/dir') 250 | * $finder->path('/some\/special\/dir/') // same as above 251 | * $finder->path(['some dir', 'another/dir']) 252 | * 253 | * Use only / as dirname separator. 254 | * 255 | * @param string|string[] $patterns A pattern (a regexp or a string) or an array of patterns 256 | * 257 | * @return $this 258 | * 259 | * @see FilenameFilterIterator 260 | */ 261 | public function path(string|array $patterns): static 262 | { 263 | $this->paths = array_merge($this->paths, (array) $patterns); 264 | 265 | return $this; 266 | } 267 | 268 | /** 269 | * Adds rules that filenames must not match. 270 | * 271 | * You can use patterns (delimited with / sign) or simple strings. 272 | * 273 | * $finder->notPath('some/special/dir') 274 | * $finder->notPath('/some\/special\/dir/') // same as above 275 | * $finder->notPath(['some/file.txt', 'another/file.log']) 276 | * 277 | * Use only / as dirname separator. 278 | * 279 | * @param string|string[] $patterns A pattern (a regexp or a string) or an array of patterns 280 | * 281 | * @return $this 282 | * 283 | * @see FilenameFilterIterator 284 | */ 285 | public function notPath(string|array $patterns): static 286 | { 287 | $this->notPaths = array_merge($this->notPaths, (array) $patterns); 288 | 289 | return $this; 290 | } 291 | 292 | /** 293 | * Adds tests for file sizes. 294 | * 295 | * $finder->size('> 10K'); 296 | * $finder->size('<= 1Ki'); 297 | * $finder->size(4); 298 | * $finder->size(['> 10K', '< 20K']) 299 | * 300 | * @param string|int|string[]|int[] $sizes A size range string or an integer or an array of size ranges 301 | * 302 | * @return $this 303 | * 304 | * @see SizeRangeFilterIterator 305 | * @see NumberComparator 306 | */ 307 | public function size(string|int|array $sizes): static 308 | { 309 | foreach ((array) $sizes as $size) { 310 | $this->sizes[] = new NumberComparator($size); 311 | } 312 | 313 | return $this; 314 | } 315 | 316 | /** 317 | * Excludes directories. 318 | * 319 | * Directories passed as argument must be relative to the ones defined with the `in()` method. For example: 320 | * 321 | * $finder->in(__DIR__)->exclude('ruby'); 322 | * 323 | * @param string|array $dirs A directory path or an array of directories 324 | * 325 | * @return $this 326 | * 327 | * @see ExcludeDirectoryFilterIterator 328 | */ 329 | public function exclude(string|array $dirs): static 330 | { 331 | $this->exclude = array_merge($this->exclude, (array) $dirs); 332 | 333 | return $this; 334 | } 335 | 336 | /** 337 | * Excludes "hidden" directories and files (starting with a dot). 338 | * 339 | * This option is enabled by default. 340 | * 341 | * @return $this 342 | * 343 | * @see ExcludeDirectoryFilterIterator 344 | */ 345 | public function ignoreDotFiles(bool $ignoreDotFiles): static 346 | { 347 | if ($ignoreDotFiles) { 348 | $this->ignore |= static::IGNORE_DOT_FILES; 349 | } else { 350 | $this->ignore &= ~static::IGNORE_DOT_FILES; 351 | } 352 | 353 | return $this; 354 | } 355 | 356 | /** 357 | * Forces the finder to ignore version control directories. 358 | * 359 | * This option is enabled by default. 360 | * 361 | * @return $this 362 | * 363 | * @see ExcludeDirectoryFilterIterator 364 | */ 365 | public function ignoreVCS(bool $ignoreVCS): static 366 | { 367 | if ($ignoreVCS) { 368 | $this->ignore |= static::IGNORE_VCS_FILES; 369 | } else { 370 | $this->ignore &= ~static::IGNORE_VCS_FILES; 371 | } 372 | 373 | return $this; 374 | } 375 | 376 | /** 377 | * Forces Finder to obey .gitignore and ignore files based on rules listed there. 378 | * 379 | * This option is disabled by default. 380 | * 381 | * @return $this 382 | */ 383 | public function ignoreVCSIgnored(bool $ignoreVCSIgnored): static 384 | { 385 | if ($ignoreVCSIgnored) { 386 | $this->ignore |= static::IGNORE_VCS_IGNORED_FILES; 387 | } else { 388 | $this->ignore &= ~static::IGNORE_VCS_IGNORED_FILES; 389 | } 390 | 391 | return $this; 392 | } 393 | 394 | /** 395 | * Adds VCS patterns. 396 | * 397 | * @see ignoreVCS() 398 | * 399 | * @param string|string[] $pattern VCS patterns to ignore 400 | */ 401 | public static function addVCSPattern(string|array $pattern): void 402 | { 403 | foreach ((array) $pattern as $p) { 404 | self::$vcsPatterns[] = $p; 405 | } 406 | 407 | self::$vcsPatterns = array_unique(self::$vcsPatterns); 408 | } 409 | 410 | /** 411 | * Sorts files and directories by an anonymous function. 412 | * 413 | * The anonymous function receives two \SplFileInfo instances to compare. 414 | * 415 | * This can be slow as all the matching files and directories must be retrieved for comparison. 416 | * 417 | * @return $this 418 | * 419 | * @see SortableIterator 420 | */ 421 | public function sort(\Closure $closure): static 422 | { 423 | $this->sort = $closure; 424 | 425 | return $this; 426 | } 427 | 428 | /** 429 | * Sorts files and directories by extension. 430 | * 431 | * This can be slow as all the matching files and directories must be retrieved for comparison. 432 | * 433 | * @return $this 434 | * 435 | * @see SortableIterator 436 | */ 437 | public function sortByExtension(): static 438 | { 439 | $this->sort = SortableIterator::SORT_BY_EXTENSION; 440 | 441 | return $this; 442 | } 443 | 444 | /** 445 | * Sorts files and directories by name. 446 | * 447 | * This can be slow as all the matching files and directories must be retrieved for comparison. 448 | * 449 | * @return $this 450 | * 451 | * @see SortableIterator 452 | */ 453 | public function sortByName(bool $useNaturalSort = false): static 454 | { 455 | $this->sort = $useNaturalSort ? SortableIterator::SORT_BY_NAME_NATURAL : SortableIterator::SORT_BY_NAME; 456 | 457 | return $this; 458 | } 459 | 460 | /** 461 | * Sorts files and directories by name case insensitive. 462 | * 463 | * This can be slow as all the matching files and directories must be retrieved for comparison. 464 | * 465 | * @return $this 466 | * 467 | * @see SortableIterator 468 | */ 469 | public function sortByCaseInsensitiveName(bool $useNaturalSort = false): static 470 | { 471 | $this->sort = $useNaturalSort ? SortableIterator::SORT_BY_NAME_NATURAL_CASE_INSENSITIVE : SortableIterator::SORT_BY_NAME_CASE_INSENSITIVE; 472 | 473 | return $this; 474 | } 475 | 476 | /** 477 | * Sorts files and directories by size. 478 | * 479 | * This can be slow as all the matching files and directories must be retrieved for comparison. 480 | * 481 | * @return $this 482 | * 483 | * @see SortableIterator 484 | */ 485 | public function sortBySize(): static 486 | { 487 | $this->sort = SortableIterator::SORT_BY_SIZE; 488 | 489 | return $this; 490 | } 491 | 492 | /** 493 | * Sorts files and directories by type (directories before files), then by name. 494 | * 495 | * This can be slow as all the matching files and directories must be retrieved for comparison. 496 | * 497 | * @return $this 498 | * 499 | * @see SortableIterator 500 | */ 501 | public function sortByType(): static 502 | { 503 | $this->sort = SortableIterator::SORT_BY_TYPE; 504 | 505 | return $this; 506 | } 507 | 508 | /** 509 | * Sorts files and directories by the last accessed time. 510 | * 511 | * This is the time that the file was last accessed, read or written to. 512 | * 513 | * This can be slow as all the matching files and directories must be retrieved for comparison. 514 | * 515 | * @return $this 516 | * 517 | * @see SortableIterator 518 | */ 519 | public function sortByAccessedTime(): static 520 | { 521 | $this->sort = SortableIterator::SORT_BY_ACCESSED_TIME; 522 | 523 | return $this; 524 | } 525 | 526 | /** 527 | * Reverses the sorting. 528 | * 529 | * @return $this 530 | */ 531 | public function reverseSorting(): static 532 | { 533 | $this->reverseSorting = true; 534 | 535 | return $this; 536 | } 537 | 538 | /** 539 | * Sorts files and directories by the last inode changed time. 540 | * 541 | * This is the time that the inode information was last modified (permissions, owner, group or other metadata). 542 | * 543 | * On Windows, since inode is not available, changed time is actually the file creation time. 544 | * 545 | * This can be slow as all the matching files and directories must be retrieved for comparison. 546 | * 547 | * @return $this 548 | * 549 | * @see SortableIterator 550 | */ 551 | public function sortByChangedTime(): static 552 | { 553 | $this->sort = SortableIterator::SORT_BY_CHANGED_TIME; 554 | 555 | return $this; 556 | } 557 | 558 | /** 559 | * Sorts files and directories by the last modified time. 560 | * 561 | * This is the last time the actual contents of the file were last modified. 562 | * 563 | * This can be slow as all the matching files and directories must be retrieved for comparison. 564 | * 565 | * @return $this 566 | * 567 | * @see SortableIterator 568 | */ 569 | public function sortByModifiedTime(): static 570 | { 571 | $this->sort = SortableIterator::SORT_BY_MODIFIED_TIME; 572 | 573 | return $this; 574 | } 575 | 576 | /** 577 | * Filters the iterator with an anonymous function. 578 | * 579 | * The anonymous function receives a \SplFileInfo and must return false 580 | * to remove files. 581 | * 582 | * @param \Closure(SplFileInfo): bool $closure 583 | * @param bool $prune Whether to skip traversing directories further 584 | * 585 | * @return $this 586 | * 587 | * @see CustomFilterIterator 588 | */ 589 | public function filter(\Closure $closure, bool $prune = false): static 590 | { 591 | $this->filters[] = $closure; 592 | 593 | if ($prune) { 594 | $this->pruneFilters[] = $closure; 595 | } 596 | 597 | return $this; 598 | } 599 | 600 | /** 601 | * Forces the following of symlinks. 602 | * 603 | * @return $this 604 | */ 605 | public function followLinks(): static 606 | { 607 | $this->followLinks = true; 608 | 609 | return $this; 610 | } 611 | 612 | /** 613 | * Tells finder to ignore unreadable directories. 614 | * 615 | * By default, scanning unreadable directories content throws an AccessDeniedException. 616 | * 617 | * @return $this 618 | */ 619 | public function ignoreUnreadableDirs(bool $ignore = true): static 620 | { 621 | $this->ignoreUnreadableDirs = $ignore; 622 | 623 | return $this; 624 | } 625 | 626 | /** 627 | * Searches files and directories which match defined rules. 628 | * 629 | * @param string|string[] $dirs A directory path or an array of directories 630 | * 631 | * @return $this 632 | * 633 | * @throws DirectoryNotFoundException if one of the directories does not exist 634 | */ 635 | public function in(string|array $dirs): static 636 | { 637 | $resolvedDirs = []; 638 | 639 | foreach ((array) $dirs as $dir) { 640 | if (is_dir($dir)) { 641 | $resolvedDirs[] = [$this->normalizeDir($dir)]; 642 | } elseif ($glob = glob($dir, (\defined('GLOB_BRACE') ? \GLOB_BRACE : 0) | \GLOB_ONLYDIR | \GLOB_NOSORT)) { 643 | sort($glob); 644 | $resolvedDirs[] = array_map($this->normalizeDir(...), $glob); 645 | } else { 646 | throw new DirectoryNotFoundException(\sprintf('The "%s" directory does not exist.', $dir)); 647 | } 648 | } 649 | 650 | $this->dirs = array_merge($this->dirs, ...$resolvedDirs); 651 | 652 | return $this; 653 | } 654 | 655 | /** 656 | * Returns an Iterator for the current Finder configuration. 657 | * 658 | * This method implements the IteratorAggregate interface. 659 | * 660 | * @return \Iterator 661 | * 662 | * @throws \LogicException if the in() method has not been called 663 | */ 664 | public function getIterator(): \Iterator 665 | { 666 | if (0 === \count($this->dirs) && 0 === \count($this->iterators)) { 667 | throw new \LogicException('You must call one of in() or append() methods before iterating over a Finder.'); 668 | } 669 | 670 | if (1 === \count($this->dirs) && 0 === \count($this->iterators)) { 671 | $iterator = $this->searchInDirectory($this->dirs[0]); 672 | 673 | if ($this->sort || $this->reverseSorting) { 674 | $iterator = (new SortableIterator($iterator, $this->sort, $this->reverseSorting))->getIterator(); 675 | } 676 | 677 | return $iterator; 678 | } 679 | 680 | $iterator = new \AppendIterator(); 681 | foreach ($this->dirs as $dir) { 682 | $iterator->append(new \IteratorIterator(new LazyIterator(fn () => $this->searchInDirectory($dir)))); 683 | } 684 | 685 | foreach ($this->iterators as $it) { 686 | $iterator->append($it); 687 | } 688 | 689 | if ($this->sort || $this->reverseSorting) { 690 | $iterator = (new SortableIterator($iterator, $this->sort, $this->reverseSorting))->getIterator(); 691 | } 692 | 693 | return $iterator; 694 | } 695 | 696 | /** 697 | * Appends an existing set of files/directories to the finder. 698 | * 699 | * The set can be another Finder, an Iterator, an IteratorAggregate, or even a plain array. 700 | * 701 | * @return $this 702 | */ 703 | public function append(iterable $iterator): static 704 | { 705 | if ($iterator instanceof \IteratorAggregate) { 706 | $this->iterators[] = $iterator->getIterator(); 707 | } elseif ($iterator instanceof \Iterator) { 708 | $this->iterators[] = $iterator; 709 | } else { 710 | $it = new \ArrayIterator(); 711 | foreach ($iterator as $file) { 712 | $file = $file instanceof \SplFileInfo ? $file : new \SplFileInfo($file); 713 | $it[$file->getPathname()] = $file; 714 | } 715 | $this->iterators[] = $it; 716 | } 717 | 718 | return $this; 719 | } 720 | 721 | /** 722 | * Check if any results were found. 723 | */ 724 | public function hasResults(): bool 725 | { 726 | foreach ($this->getIterator() as $_) { 727 | return true; 728 | } 729 | 730 | return false; 731 | } 732 | 733 | /** 734 | * Counts all the results collected by the iterators. 735 | */ 736 | public function count(): int 737 | { 738 | return iterator_count($this->getIterator()); 739 | } 740 | 741 | private function searchInDirectory(string $dir): \Iterator 742 | { 743 | $exclude = $this->exclude; 744 | $notPaths = $this->notPaths; 745 | 746 | if ($this->pruneFilters) { 747 | $exclude = array_merge($exclude, $this->pruneFilters); 748 | } 749 | 750 | if (static::IGNORE_VCS_FILES === (static::IGNORE_VCS_FILES & $this->ignore)) { 751 | $exclude = array_merge($exclude, self::$vcsPatterns); 752 | } 753 | 754 | if (static::IGNORE_DOT_FILES === (static::IGNORE_DOT_FILES & $this->ignore)) { 755 | $notPaths[] = '#(^|/)\..+(/|$)#'; 756 | } 757 | 758 | $minDepth = 0; 759 | $maxDepth = \PHP_INT_MAX; 760 | 761 | foreach ($this->depths as $comparator) { 762 | switch ($comparator->getOperator()) { 763 | case '>': 764 | $minDepth = $comparator->getTarget() + 1; 765 | break; 766 | case '>=': 767 | $minDepth = $comparator->getTarget(); 768 | break; 769 | case '<': 770 | $maxDepth = $comparator->getTarget() - 1; 771 | break; 772 | case '<=': 773 | $maxDepth = $comparator->getTarget(); 774 | break; 775 | default: 776 | $minDepth = $maxDepth = $comparator->getTarget(); 777 | } 778 | } 779 | 780 | $flags = \RecursiveDirectoryIterator::SKIP_DOTS; 781 | 782 | if ($this->followLinks) { 783 | $flags |= \RecursiveDirectoryIterator::FOLLOW_SYMLINKS; 784 | } 785 | 786 | $iterator = new Iterator\RecursiveDirectoryIterator($dir, $flags, $this->ignoreUnreadableDirs); 787 | 788 | if ($exclude) { 789 | $iterator = new ExcludeDirectoryFilterIterator($iterator, $exclude); 790 | } 791 | 792 | $iterator = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::SELF_FIRST); 793 | 794 | if ($minDepth > 0 || $maxDepth < \PHP_INT_MAX) { 795 | $iterator = new DepthRangeFilterIterator($iterator, $minDepth, $maxDepth); 796 | } 797 | 798 | if ($this->mode) { 799 | $iterator = new Iterator\FileTypeFilterIterator($iterator, $this->mode); 800 | } 801 | 802 | if ($this->names || $this->notNames) { 803 | $iterator = new FilenameFilterIterator($iterator, $this->names, $this->notNames); 804 | } 805 | 806 | if ($this->contains || $this->notContains) { 807 | $iterator = new FilecontentFilterIterator($iterator, $this->contains, $this->notContains); 808 | } 809 | 810 | if ($this->sizes) { 811 | $iterator = new SizeRangeFilterIterator($iterator, $this->sizes); 812 | } 813 | 814 | if ($this->dates) { 815 | $iterator = new DateRangeFilterIterator($iterator, $this->dates); 816 | } 817 | 818 | if ($this->filters) { 819 | $iterator = new CustomFilterIterator($iterator, $this->filters); 820 | } 821 | 822 | if ($this->paths || $notPaths) { 823 | $iterator = new Iterator\PathFilterIterator($iterator, $this->paths, $notPaths); 824 | } 825 | 826 | if (static::IGNORE_VCS_IGNORED_FILES === (static::IGNORE_VCS_IGNORED_FILES & $this->ignore)) { 827 | $iterator = new Iterator\VcsIgnoredFilterIterator($iterator, $dir); 828 | } 829 | 830 | return $iterator; 831 | } 832 | 833 | /** 834 | * Normalizes given directory names by removing trailing slashes. 835 | * 836 | * Excluding: (s)ftp:// or ssh2.(s)ftp:// wrapper 837 | */ 838 | private function normalizeDir(string $dir): string 839 | { 840 | if ('/' === $dir) { 841 | return $dir; 842 | } 843 | 844 | $dir = rtrim($dir, '/'.\DIRECTORY_SEPARATOR); 845 | 846 | if (preg_match('#^(ssh2\.)?s?ftp://#', $dir)) { 847 | $dir .= '/'; 848 | } 849 | 850 | return $dir; 851 | } 852 | } 853 | -------------------------------------------------------------------------------- /Gitignore.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Finder; 13 | 14 | /** 15 | * Gitignore matches against text. 16 | * 17 | * @author Michael Voříšek 18 | * @author Ahmed Abdou 19 | */ 20 | class Gitignore 21 | { 22 | /** 23 | * Returns a regexp which is the equivalent of the gitignore pattern. 24 | * 25 | * Format specification: https://git-scm.com/docs/gitignore#_pattern_format 26 | */ 27 | public static function toRegex(string $gitignoreFileContent): string 28 | { 29 | return self::buildRegex($gitignoreFileContent, false); 30 | } 31 | 32 | public static function toRegexMatchingNegatedPatterns(string $gitignoreFileContent): string 33 | { 34 | return self::buildRegex($gitignoreFileContent, true); 35 | } 36 | 37 | private static function buildRegex(string $gitignoreFileContent, bool $inverted): string 38 | { 39 | $gitignoreFileContent = preg_replace('~(? '['.('' !== $matches[1] ? '^' : '').str_replace('\\-', '-', $matches[2]).']', $regex); 83 | $regex = preg_replace('~(?:(?:\\\\\*){2,}(/?))+~', '(?:(?:(?!//).(? 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Finder; 13 | 14 | /** 15 | * Glob matches globbing patterns against text. 16 | * 17 | * if match_glob("foo.*", "foo.bar") echo "matched\n"; 18 | * 19 | * // prints foo.bar and foo.baz 20 | * $regex = glob_to_regex("foo.*"); 21 | * for (['foo.bar', 'foo.baz', 'foo', 'bar'] as $t) 22 | * { 23 | * if (/$regex/) echo "matched: $car\n"; 24 | * } 25 | * 26 | * Glob implements glob(3) style matching that can be used to match 27 | * against text, rather than fetching names from a filesystem. 28 | * 29 | * Based on the Perl Text::Glob module. 30 | * 31 | * @author Fabien Potencier PHP port 32 | * @author Richard Clamp Perl version 33 | * @copyright 2004-2005 Fabien Potencier 34 | * @copyright 2002 Richard Clamp 35 | */ 36 | class Glob 37 | { 38 | /** 39 | * Returns a regexp which is the equivalent of the glob pattern. 40 | */ 41 | public static function toRegex(string $glob, bool $strictLeadingDot = true, bool $strictWildcardSlash = true, string $delimiter = '#'): string 42 | { 43 | $firstByte = true; 44 | $escaping = false; 45 | $inCurlies = 0; 46 | $regex = ''; 47 | $sizeGlob = \strlen($glob); 48 | for ($i = 0; $i < $sizeGlob; ++$i) { 49 | $car = $glob[$i]; 50 | if ($firstByte && $strictLeadingDot && '.' !== $car) { 51 | $regex .= '(?=[^\.])'; 52 | } 53 | 54 | $firstByte = '/' === $car; 55 | 56 | if ($firstByte && $strictWildcardSlash && isset($glob[$i + 2]) && '**' === $glob[$i + 1].$glob[$i + 2] && (!isset($glob[$i + 3]) || '/' === $glob[$i + 3])) { 57 | $car = '[^/]++/'; 58 | if (!isset($glob[$i + 3])) { 59 | $car .= '?'; 60 | } 61 | 62 | if ($strictLeadingDot) { 63 | $car = '(?=[^\.])'.$car; 64 | } 65 | 66 | $car = '/(?:'.$car.')*'; 67 | $i += 2 + isset($glob[$i + 3]); 68 | 69 | if ('/' === $delimiter) { 70 | $car = str_replace('/', '\\/', $car); 71 | } 72 | } 73 | 74 | if ($delimiter === $car || '.' === $car || '(' === $car || ')' === $car || '|' === $car || '+' === $car || '^' === $car || '$' === $car) { 75 | $regex .= "\\$car"; 76 | } elseif ('*' === $car) { 77 | $regex .= $escaping ? '\\*' : ($strictWildcardSlash ? '[^/]*' : '.*'); 78 | } elseif ('?' === $car) { 79 | $regex .= $escaping ? '\\?' : ($strictWildcardSlash ? '[^/]' : '.'); 80 | } elseif ('{' === $car) { 81 | $regex .= $escaping ? '\\{' : '('; 82 | if (!$escaping) { 83 | ++$inCurlies; 84 | } 85 | } elseif ('}' === $car && $inCurlies) { 86 | $regex .= $escaping ? '}' : ')'; 87 | if (!$escaping) { 88 | --$inCurlies; 89 | } 90 | } elseif (',' === $car && $inCurlies) { 91 | $regex .= $escaping ? ',' : '|'; 92 | } elseif ('\\' === $car) { 93 | if ($escaping) { 94 | $regex .= '\\\\'; 95 | $escaping = false; 96 | } else { 97 | $escaping = true; 98 | } 99 | 100 | continue; 101 | } else { 102 | $regex .= $car; 103 | } 104 | $escaping = false; 105 | } 106 | 107 | return $delimiter.'^'.$regex.'$'.$delimiter; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Iterator/CustomFilterIterator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Finder\Iterator; 13 | 14 | /** 15 | * CustomFilterIterator filters files by applying anonymous functions. 16 | * 17 | * The anonymous function receives a \SplFileInfo and must return false 18 | * to remove files. 19 | * 20 | * @author Fabien Potencier 21 | * 22 | * @extends \FilterIterator 23 | */ 24 | class CustomFilterIterator extends \FilterIterator 25 | { 26 | private array $filters = []; 27 | 28 | /** 29 | * @param \Iterator $iterator The Iterator to filter 30 | * @param callable[] $filters An array of PHP callbacks 31 | * 32 | * @throws \InvalidArgumentException 33 | */ 34 | public function __construct(\Iterator $iterator, array $filters) 35 | { 36 | foreach ($filters as $filter) { 37 | if (!\is_callable($filter)) { 38 | throw new \InvalidArgumentException('Invalid PHP callback.'); 39 | } 40 | } 41 | $this->filters = $filters; 42 | 43 | parent::__construct($iterator); 44 | } 45 | 46 | /** 47 | * Filters the iterator values. 48 | */ 49 | public function accept(): bool 50 | { 51 | $fileinfo = $this->current(); 52 | 53 | foreach ($this->filters as $filter) { 54 | if (false === $filter($fileinfo)) { 55 | return false; 56 | } 57 | } 58 | 59 | return true; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Iterator/DateRangeFilterIterator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Finder\Iterator; 13 | 14 | use Symfony\Component\Finder\Comparator\DateComparator; 15 | 16 | /** 17 | * DateRangeFilterIterator filters out files that are not in the given date range (last modified dates). 18 | * 19 | * @author Fabien Potencier 20 | * 21 | * @extends \FilterIterator 22 | */ 23 | class DateRangeFilterIterator extends \FilterIterator 24 | { 25 | private array $comparators = []; 26 | 27 | /** 28 | * @param \Iterator $iterator 29 | * @param DateComparator[] $comparators 30 | */ 31 | public function __construct(\Iterator $iterator, array $comparators) 32 | { 33 | $this->comparators = $comparators; 34 | 35 | parent::__construct($iterator); 36 | } 37 | 38 | /** 39 | * Filters the iterator values. 40 | */ 41 | public function accept(): bool 42 | { 43 | $fileinfo = $this->current(); 44 | 45 | if (!file_exists($fileinfo->getPathname())) { 46 | return false; 47 | } 48 | 49 | $filedate = $fileinfo->getMTime(); 50 | foreach ($this->comparators as $compare) { 51 | if (!$compare->test($filedate)) { 52 | return false; 53 | } 54 | } 55 | 56 | return true; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Iterator/DepthRangeFilterIterator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Finder\Iterator; 13 | 14 | /** 15 | * DepthRangeFilterIterator limits the directory depth. 16 | * 17 | * @author Fabien Potencier 18 | * 19 | * @template-covariant TKey 20 | * @template-covariant TValue 21 | * 22 | * @extends \FilterIterator 23 | */ 24 | class DepthRangeFilterIterator extends \FilterIterator 25 | { 26 | private int $minDepth = 0; 27 | 28 | /** 29 | * @param \RecursiveIteratorIterator<\RecursiveIterator> $iterator The Iterator to filter 30 | * @param int $minDepth The min depth 31 | * @param int $maxDepth The max depth 32 | */ 33 | public function __construct(\RecursiveIteratorIterator $iterator, int $minDepth = 0, int $maxDepth = \PHP_INT_MAX) 34 | { 35 | $this->minDepth = $minDepth; 36 | $iterator->setMaxDepth(\PHP_INT_MAX === $maxDepth ? -1 : $maxDepth); 37 | 38 | parent::__construct($iterator); 39 | } 40 | 41 | /** 42 | * Filters the iterator values. 43 | */ 44 | public function accept(): bool 45 | { 46 | return $this->getInnerIterator()->getDepth() >= $this->minDepth; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Iterator/ExcludeDirectoryFilterIterator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Finder\Iterator; 13 | 14 | use Symfony\Component\Finder\SplFileInfo; 15 | 16 | /** 17 | * ExcludeDirectoryFilterIterator filters out directories. 18 | * 19 | * @author Fabien Potencier 20 | * 21 | * @extends \FilterIterator 22 | * 23 | * @implements \RecursiveIterator 24 | */ 25 | class ExcludeDirectoryFilterIterator extends \FilterIterator implements \RecursiveIterator 26 | { 27 | /** @var \Iterator */ 28 | private \Iterator $iterator; 29 | private bool $isRecursive; 30 | /** @var array */ 31 | private array $excludedDirs = []; 32 | private ?string $excludedPattern = null; 33 | /** @var list */ 34 | private array $pruneFilters = []; 35 | 36 | /** 37 | * @param \Iterator $iterator The Iterator to filter 38 | * @param list $directories An array of directories to exclude 39 | */ 40 | public function __construct(\Iterator $iterator, array $directories) 41 | { 42 | $this->iterator = $iterator; 43 | $this->isRecursive = $iterator instanceof \RecursiveIterator; 44 | $patterns = []; 45 | foreach ($directories as $directory) { 46 | if (!\is_string($directory)) { 47 | if (!\is_callable($directory)) { 48 | throw new \InvalidArgumentException('Invalid PHP callback.'); 49 | } 50 | 51 | $this->pruneFilters[] = $directory; 52 | 53 | continue; 54 | } 55 | 56 | $directory = rtrim($directory, '/'); 57 | if (!$this->isRecursive || str_contains($directory, '/')) { 58 | $patterns[] = preg_quote($directory, '#'); 59 | } else { 60 | $this->excludedDirs[$directory] = true; 61 | } 62 | } 63 | if ($patterns) { 64 | $this->excludedPattern = '#(?:^|/)(?:'.implode('|', $patterns).')(?:/|$)#'; 65 | } 66 | 67 | parent::__construct($iterator); 68 | } 69 | 70 | /** 71 | * Filters the iterator values. 72 | */ 73 | public function accept(): bool 74 | { 75 | if ($this->isRecursive && isset($this->excludedDirs[$this->getFilename()]) && $this->isDir()) { 76 | return false; 77 | } 78 | 79 | if ($this->excludedPattern) { 80 | $path = $this->isDir() ? $this->current()->getRelativePathname() : $this->current()->getRelativePath(); 81 | $path = str_replace('\\', '/', $path); 82 | 83 | return !preg_match($this->excludedPattern, $path); 84 | } 85 | 86 | if ($this->pruneFilters && $this->hasChildren()) { 87 | foreach ($this->pruneFilters as $pruneFilter) { 88 | if (!$pruneFilter($this->current())) { 89 | return false; 90 | } 91 | } 92 | } 93 | 94 | return true; 95 | } 96 | 97 | public function hasChildren(): bool 98 | { 99 | return $this->isRecursive && $this->iterator->hasChildren(); 100 | } 101 | 102 | public function getChildren(): self 103 | { 104 | $children = new self($this->iterator->getChildren(), []); 105 | $children->excludedDirs = $this->excludedDirs; 106 | $children->excludedPattern = $this->excludedPattern; 107 | 108 | return $children; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Iterator/FileTypeFilterIterator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Finder\Iterator; 13 | 14 | /** 15 | * FileTypeFilterIterator only keeps files, directories, or both. 16 | * 17 | * @author Fabien Potencier 18 | * 19 | * @extends \FilterIterator 20 | */ 21 | class FileTypeFilterIterator extends \FilterIterator 22 | { 23 | public const ONLY_FILES = 1; 24 | public const ONLY_DIRECTORIES = 2; 25 | 26 | /** 27 | * @param \Iterator $iterator The Iterator to filter 28 | * @param int $mode The mode (self::ONLY_FILES or self::ONLY_DIRECTORIES) 29 | */ 30 | public function __construct( 31 | \Iterator $iterator, 32 | private int $mode, 33 | ) { 34 | parent::__construct($iterator); 35 | } 36 | 37 | /** 38 | * Filters the iterator values. 39 | */ 40 | public function accept(): bool 41 | { 42 | $fileinfo = $this->current(); 43 | if (self::ONLY_DIRECTORIES === (self::ONLY_DIRECTORIES & $this->mode) && $fileinfo->isFile()) { 44 | return false; 45 | } elseif (self::ONLY_FILES === (self::ONLY_FILES & $this->mode) && $fileinfo->isDir()) { 46 | return false; 47 | } 48 | 49 | return true; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Iterator/FilecontentFilterIterator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Finder\Iterator; 13 | 14 | use Symfony\Component\Finder\SplFileInfo; 15 | 16 | /** 17 | * FilecontentFilterIterator filters files by their contents using patterns (regexps or strings). 18 | * 19 | * @author Fabien Potencier 20 | * @author Włodzimierz Gajda 21 | * 22 | * @extends MultiplePcreFilterIterator 23 | */ 24 | class FilecontentFilterIterator extends MultiplePcreFilterIterator 25 | { 26 | /** 27 | * Filters the iterator values. 28 | */ 29 | public function accept(): bool 30 | { 31 | if (!$this->matchRegexps && !$this->noMatchRegexps) { 32 | return true; 33 | } 34 | 35 | $fileinfo = $this->current(); 36 | 37 | if ($fileinfo->isDir() || !$fileinfo->isReadable()) { 38 | return false; 39 | } 40 | 41 | $content = $fileinfo->getContents(); 42 | if (!$content) { 43 | return false; 44 | } 45 | 46 | return $this->isAccepted($content); 47 | } 48 | 49 | /** 50 | * Converts string to regexp if necessary. 51 | * 52 | * @param string $str Pattern: string or regexp 53 | */ 54 | protected function toRegex(string $str): string 55 | { 56 | return $this->isRegex($str) ? $str : '/'.preg_quote($str, '/').'/'; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Iterator/FilenameFilterIterator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Finder\Iterator; 13 | 14 | use Symfony\Component\Finder\Glob; 15 | 16 | /** 17 | * FilenameFilterIterator filters files by patterns (a regexp, a glob, or a string). 18 | * 19 | * @author Fabien Potencier 20 | * 21 | * @extends MultiplePcreFilterIterator 22 | */ 23 | class FilenameFilterIterator extends MultiplePcreFilterIterator 24 | { 25 | /** 26 | * Filters the iterator values. 27 | */ 28 | public function accept(): bool 29 | { 30 | return $this->isAccepted($this->current()->getFilename()); 31 | } 32 | 33 | /** 34 | * Converts glob to regexp. 35 | * 36 | * PCRE patterns are left unchanged. 37 | * Glob strings are transformed with Glob::toRegex(). 38 | * 39 | * @param string $str Pattern: glob or regexp 40 | */ 41 | protected function toRegex(string $str): string 42 | { 43 | return $this->isRegex($str) ? $str : Glob::toRegex($str); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Iterator/LazyIterator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Finder\Iterator; 13 | 14 | /** 15 | * @author Jérémy Derussé 16 | * 17 | * @internal 18 | */ 19 | class LazyIterator implements \IteratorAggregate 20 | { 21 | private \Closure $iteratorFactory; 22 | 23 | public function __construct(callable $iteratorFactory) 24 | { 25 | $this->iteratorFactory = $iteratorFactory(...); 26 | } 27 | 28 | public function getIterator(): \Traversable 29 | { 30 | yield from ($this->iteratorFactory)(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Iterator/MultiplePcreFilterIterator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Finder\Iterator; 13 | 14 | /** 15 | * MultiplePcreFilterIterator filters files using patterns (regexps, globs or strings). 16 | * 17 | * @author Fabien Potencier 18 | * 19 | * @template-covariant TKey 20 | * @template-covariant TValue 21 | * 22 | * @extends \FilterIterator 23 | */ 24 | abstract class MultiplePcreFilterIterator extends \FilterIterator 25 | { 26 | protected array $matchRegexps = []; 27 | protected array $noMatchRegexps = []; 28 | 29 | /** 30 | * @param \Iterator $iterator The Iterator to filter 31 | * @param string[] $matchPatterns An array of patterns that need to match 32 | * @param string[] $noMatchPatterns An array of patterns that need to not match 33 | */ 34 | public function __construct(\Iterator $iterator, array $matchPatterns, array $noMatchPatterns) 35 | { 36 | foreach ($matchPatterns as $pattern) { 37 | $this->matchRegexps[] = $this->toRegex($pattern); 38 | } 39 | 40 | foreach ($noMatchPatterns as $pattern) { 41 | $this->noMatchRegexps[] = $this->toRegex($pattern); 42 | } 43 | 44 | parent::__construct($iterator); 45 | } 46 | 47 | /** 48 | * Checks whether the string is accepted by the regex filters. 49 | * 50 | * If there is no regexps defined in the class, this method will accept the string. 51 | * Such case can be handled by child classes before calling the method if they want to 52 | * apply a different behavior. 53 | */ 54 | protected function isAccepted(string $string): bool 55 | { 56 | // should at least not match one rule to exclude 57 | foreach ($this->noMatchRegexps as $regex) { 58 | if (preg_match($regex, $string)) { 59 | return false; 60 | } 61 | } 62 | 63 | // should at least match one rule 64 | if ($this->matchRegexps) { 65 | foreach ($this->matchRegexps as $regex) { 66 | if (preg_match($regex, $string)) { 67 | return true; 68 | } 69 | } 70 | 71 | return false; 72 | } 73 | 74 | // If there is no match rules, the file is accepted 75 | return true; 76 | } 77 | 78 | /** 79 | * Checks whether the string is a regex. 80 | */ 81 | protected function isRegex(string $str): bool 82 | { 83 | $availableModifiers = 'imsxuADUn'; 84 | 85 | if (preg_match('/^(.{3,}?)['.$availableModifiers.']*$/', $str, $m)) { 86 | $start = substr($m[1], 0, 1); 87 | $end = substr($m[1], -1); 88 | 89 | if ($start === $end) { 90 | return !preg_match('/[*?[:alnum:] \\\\]/', $start); 91 | } 92 | 93 | foreach ([['{', '}'], ['(', ')'], ['[', ']'], ['<', '>']] as $delimiters) { 94 | if ($start === $delimiters[0] && $end === $delimiters[1]) { 95 | return true; 96 | } 97 | } 98 | } 99 | 100 | return false; 101 | } 102 | 103 | /** 104 | * Converts string into regexp. 105 | */ 106 | abstract protected function toRegex(string $str): string; 107 | } 108 | -------------------------------------------------------------------------------- /Iterator/PathFilterIterator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Finder\Iterator; 13 | 14 | use Symfony\Component\Finder\SplFileInfo; 15 | 16 | /** 17 | * PathFilterIterator filters files by path patterns (e.g. some/special/dir). 18 | * 19 | * @author Fabien Potencier 20 | * @author Włodzimierz Gajda 21 | * 22 | * @extends MultiplePcreFilterIterator 23 | */ 24 | class PathFilterIterator extends MultiplePcreFilterIterator 25 | { 26 | /** 27 | * Filters the iterator values. 28 | */ 29 | public function accept(): bool 30 | { 31 | $filename = $this->current()->getRelativePathname(); 32 | 33 | if ('\\' === \DIRECTORY_SEPARATOR) { 34 | $filename = str_replace('\\', '/', $filename); 35 | } 36 | 37 | return $this->isAccepted($filename); 38 | } 39 | 40 | /** 41 | * Converts strings to regexp. 42 | * 43 | * PCRE patterns are left unchanged. 44 | * 45 | * Default conversion: 46 | * 'lorem/ipsum/dolor' ==> 'lorem\/ipsum\/dolor/' 47 | * 48 | * Use only / as directory separator (on Windows also). 49 | * 50 | * @param string $str Pattern: regexp or dirname 51 | */ 52 | protected function toRegex(string $str): string 53 | { 54 | return $this->isRegex($str) ? $str : '/'.preg_quote($str, '/').'/'; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Iterator/RecursiveDirectoryIterator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Finder\Iterator; 13 | 14 | use Symfony\Component\Finder\Exception\AccessDeniedException; 15 | use Symfony\Component\Finder\SplFileInfo; 16 | 17 | /** 18 | * Extends the \RecursiveDirectoryIterator to support relative paths. 19 | * 20 | * @author Victor Berchet 21 | * 22 | * @extends \RecursiveDirectoryIterator 23 | */ 24 | class RecursiveDirectoryIterator extends \RecursiveDirectoryIterator 25 | { 26 | private bool $ignoreUnreadableDirs; 27 | private bool $ignoreFirstRewind = true; 28 | 29 | // these 3 properties take part of the performance optimization to avoid redoing the same work in all iterations 30 | private string $rootPath; 31 | private string $subPath; 32 | private string $directorySeparator = '/'; 33 | 34 | /** 35 | * @throws \RuntimeException 36 | */ 37 | public function __construct(string $path, int $flags, bool $ignoreUnreadableDirs = false) 38 | { 39 | if ($flags & (self::CURRENT_AS_PATHNAME | self::CURRENT_AS_SELF)) { 40 | throw new \RuntimeException('This iterator only support returning current as fileinfo.'); 41 | } 42 | 43 | parent::__construct($path, $flags); 44 | $this->ignoreUnreadableDirs = $ignoreUnreadableDirs; 45 | $this->rootPath = $path; 46 | if ('/' !== \DIRECTORY_SEPARATOR && !($flags & self::UNIX_PATHS)) { 47 | $this->directorySeparator = \DIRECTORY_SEPARATOR; 48 | } 49 | } 50 | 51 | /** 52 | * Return an instance of SplFileInfo with support for relative paths. 53 | */ 54 | public function current(): SplFileInfo 55 | { 56 | // the logic here avoids redoing the same work in all iterations 57 | 58 | if (!isset($this->subPath)) { 59 | $this->subPath = $this->getSubPath(); 60 | } 61 | $subPathname = $this->subPath; 62 | if ('' !== $subPathname) { 63 | $subPathname .= $this->directorySeparator; 64 | } 65 | $subPathname .= $this->getFilename(); 66 | $basePath = $this->rootPath; 67 | 68 | if ('/' !== $basePath && !str_ends_with($basePath, $this->directorySeparator) && !str_ends_with($basePath, '/')) { 69 | $basePath .= $this->directorySeparator; 70 | } 71 | 72 | return new SplFileInfo($basePath.$subPathname, $this->subPath, $subPathname); 73 | } 74 | 75 | public function hasChildren(bool $allowLinks = false): bool 76 | { 77 | $hasChildren = parent::hasChildren($allowLinks); 78 | 79 | if (!$hasChildren || !$this->ignoreUnreadableDirs) { 80 | return $hasChildren; 81 | } 82 | 83 | try { 84 | parent::getChildren(); 85 | 86 | return true; 87 | } catch (\UnexpectedValueException) { 88 | // If directory is unreadable and finder is set to ignore it, skip children 89 | return false; 90 | } 91 | } 92 | 93 | /** 94 | * @throws AccessDeniedException 95 | */ 96 | public function getChildren(): \RecursiveDirectoryIterator 97 | { 98 | try { 99 | $children = parent::getChildren(); 100 | 101 | if ($children instanceof self) { 102 | // parent method will call the constructor with default arguments, so unreadable dirs won't be ignored anymore 103 | $children->ignoreUnreadableDirs = $this->ignoreUnreadableDirs; 104 | 105 | // performance optimization to avoid redoing the same work in all children 106 | $children->rootPath = $this->rootPath; 107 | } 108 | 109 | return $children; 110 | } catch (\UnexpectedValueException $e) { 111 | throw new AccessDeniedException($e->getMessage(), $e->getCode(), $e); 112 | } 113 | } 114 | 115 | public function next(): void 116 | { 117 | $this->ignoreFirstRewind = false; 118 | 119 | parent::next(); 120 | } 121 | 122 | public function rewind(): void 123 | { 124 | // some streams like FTP are not rewindable, ignore the first rewind after creation, 125 | // as newly created DirectoryIterator does not need to be rewound 126 | if ($this->ignoreFirstRewind) { 127 | $this->ignoreFirstRewind = false; 128 | 129 | return; 130 | } 131 | 132 | parent::rewind(); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Iterator/SizeRangeFilterIterator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Finder\Iterator; 13 | 14 | use Symfony\Component\Finder\Comparator\NumberComparator; 15 | 16 | /** 17 | * SizeRangeFilterIterator filters out files that are not in the given size range. 18 | * 19 | * @author Fabien Potencier 20 | * 21 | * @extends \FilterIterator 22 | */ 23 | class SizeRangeFilterIterator extends \FilterIterator 24 | { 25 | private array $comparators = []; 26 | 27 | /** 28 | * @param \Iterator $iterator 29 | * @param NumberComparator[] $comparators 30 | */ 31 | public function __construct(\Iterator $iterator, array $comparators) 32 | { 33 | $this->comparators = $comparators; 34 | 35 | parent::__construct($iterator); 36 | } 37 | 38 | /** 39 | * Filters the iterator values. 40 | */ 41 | public function accept(): bool 42 | { 43 | $fileinfo = $this->current(); 44 | if (!$fileinfo->isFile()) { 45 | return true; 46 | } 47 | 48 | $filesize = $fileinfo->getSize(); 49 | foreach ($this->comparators as $compare) { 50 | if (!$compare->test($filesize)) { 51 | return false; 52 | } 53 | } 54 | 55 | return true; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Iterator/SortableIterator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Finder\Iterator; 13 | 14 | /** 15 | * SortableIterator applies a sort on a given Iterator. 16 | * 17 | * @author Fabien Potencier 18 | * 19 | * @implements \IteratorAggregate 20 | */ 21 | class SortableIterator implements \IteratorAggregate 22 | { 23 | public const SORT_BY_NONE = 0; 24 | public const SORT_BY_NAME = 1; 25 | public const SORT_BY_TYPE = 2; 26 | public const SORT_BY_ACCESSED_TIME = 3; 27 | public const SORT_BY_CHANGED_TIME = 4; 28 | public const SORT_BY_MODIFIED_TIME = 5; 29 | public const SORT_BY_NAME_NATURAL = 6; 30 | public const SORT_BY_NAME_CASE_INSENSITIVE = 7; 31 | public const SORT_BY_NAME_NATURAL_CASE_INSENSITIVE = 8; 32 | public const SORT_BY_EXTENSION = 9; 33 | public const SORT_BY_SIZE = 10; 34 | 35 | /** @var \Traversable */ 36 | private \Traversable $iterator; 37 | private \Closure|int $sort; 38 | 39 | /** 40 | * @param \Traversable $iterator 41 | * @param int|callable $sort The sort type (SORT_BY_NAME, SORT_BY_TYPE, or a PHP callback) 42 | * 43 | * @throws \InvalidArgumentException 44 | */ 45 | public function __construct(\Traversable $iterator, int|callable $sort, bool $reverseOrder = false) 46 | { 47 | $this->iterator = $iterator; 48 | $order = $reverseOrder ? -1 : 1; 49 | 50 | if (self::SORT_BY_NAME === $sort) { 51 | $this->sort = static fn (\SplFileInfo $a, \SplFileInfo $b) => $order * strcmp($a->getRealPath() ?: $a->getPathname(), $b->getRealPath() ?: $b->getPathname()); 52 | } elseif (self::SORT_BY_NAME_NATURAL === $sort) { 53 | $this->sort = static fn (\SplFileInfo $a, \SplFileInfo $b) => $order * strnatcmp($a->getRealPath() ?: $a->getPathname(), $b->getRealPath() ?: $b->getPathname()); 54 | } elseif (self::SORT_BY_NAME_CASE_INSENSITIVE === $sort) { 55 | $this->sort = static fn (\SplFileInfo $a, \SplFileInfo $b) => $order * strcasecmp($a->getRealPath() ?: $a->getPathname(), $b->getRealPath() ?: $b->getPathname()); 56 | } elseif (self::SORT_BY_NAME_NATURAL_CASE_INSENSITIVE === $sort) { 57 | $this->sort = static fn (\SplFileInfo $a, \SplFileInfo $b) => $order * strnatcasecmp($a->getRealPath() ?: $a->getPathname(), $b->getRealPath() ?: $b->getPathname()); 58 | } elseif (self::SORT_BY_TYPE === $sort) { 59 | $this->sort = static function (\SplFileInfo $a, \SplFileInfo $b) use ($order) { 60 | if ($a->isDir() && $b->isFile()) { 61 | return -$order; 62 | } elseif ($a->isFile() && $b->isDir()) { 63 | return $order; 64 | } 65 | 66 | return $order * strcmp($a->getRealPath() ?: $a->getPathname(), $b->getRealPath() ?: $b->getPathname()); 67 | }; 68 | } elseif (self::SORT_BY_ACCESSED_TIME === $sort) { 69 | $this->sort = static fn (\SplFileInfo $a, \SplFileInfo $b) => $order * ($a->getATime() - $b->getATime()); 70 | } elseif (self::SORT_BY_CHANGED_TIME === $sort) { 71 | $this->sort = static fn (\SplFileInfo $a, \SplFileInfo $b) => $order * ($a->getCTime() - $b->getCTime()); 72 | } elseif (self::SORT_BY_MODIFIED_TIME === $sort) { 73 | $this->sort = static fn (\SplFileInfo $a, \SplFileInfo $b) => $order * ($a->getMTime() - $b->getMTime()); 74 | } elseif (self::SORT_BY_EXTENSION === $sort) { 75 | $this->sort = static fn (\SplFileInfo $a, \SplFileInfo $b) => $order * strnatcmp($a->getExtension(), $b->getExtension()); 76 | } elseif (self::SORT_BY_SIZE === $sort) { 77 | $this->sort = static fn (\SplFileInfo $a, \SplFileInfo $b) => $order * ($a->getSize() - $b->getSize()); 78 | } elseif (self::SORT_BY_NONE === $sort) { 79 | $this->sort = $order; 80 | } elseif (\is_callable($sort)) { 81 | $this->sort = $reverseOrder ? static fn (\SplFileInfo $a, \SplFileInfo $b) => -$sort($a, $b) : $sort(...); 82 | } else { 83 | throw new \InvalidArgumentException('The SortableIterator takes a PHP callable or a valid built-in sort algorithm as an argument.'); 84 | } 85 | } 86 | 87 | public function getIterator(): \Traversable 88 | { 89 | if (1 === $this->sort) { 90 | return $this->iterator; 91 | } 92 | 93 | $array = iterator_to_array($this->iterator, true); 94 | 95 | if (-1 === $this->sort) { 96 | $array = array_reverse($array); 97 | } else { 98 | uasort($array, $this->sort); 99 | } 100 | 101 | return new \ArrayIterator($array); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Iterator/VcsIgnoredFilterIterator.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Finder\Iterator; 13 | 14 | use Symfony\Component\Finder\Gitignore; 15 | 16 | /** 17 | * @extends \FilterIterator 18 | */ 19 | final class VcsIgnoredFilterIterator extends \FilterIterator 20 | { 21 | private string $baseDir; 22 | 23 | /** 24 | * @var array 25 | */ 26 | private array $gitignoreFilesCache = []; 27 | 28 | /** 29 | * @var array 30 | */ 31 | private array $ignoredPathsCache = []; 32 | 33 | /** 34 | * @param \Iterator $iterator 35 | */ 36 | public function __construct(\Iterator $iterator, string $baseDir) 37 | { 38 | $this->baseDir = $this->normalizePath($baseDir); 39 | 40 | foreach ([$this->baseDir, ...$this->parentDirectoriesUpwards($this->baseDir)] as $directory) { 41 | if (@is_dir("{$directory}/.git")) { 42 | $this->baseDir = $directory; 43 | break; 44 | } 45 | } 46 | 47 | parent::__construct($iterator); 48 | } 49 | 50 | public function accept(): bool 51 | { 52 | $file = $this->current(); 53 | 54 | $fileRealPath = $this->normalizePath($file->getRealPath()); 55 | 56 | return !$this->isIgnored($fileRealPath); 57 | } 58 | 59 | private function isIgnored(string $fileRealPath): bool 60 | { 61 | if (is_dir($fileRealPath) && !str_ends_with($fileRealPath, '/')) { 62 | $fileRealPath .= '/'; 63 | } 64 | 65 | if (isset($this->ignoredPathsCache[$fileRealPath])) { 66 | return $this->ignoredPathsCache[$fileRealPath]; 67 | } 68 | 69 | $ignored = false; 70 | 71 | foreach ($this->parentDirectoriesDownwards($fileRealPath) as $parentDirectory) { 72 | if ($this->isIgnored($parentDirectory)) { 73 | // rules in ignored directories are ignored, no need to check further. 74 | break; 75 | } 76 | 77 | $fileRelativePath = substr($fileRealPath, \strlen($parentDirectory) + 1); 78 | 79 | if (null === $regexps = $this->readGitignoreFile("{$parentDirectory}/.gitignore")) { 80 | continue; 81 | } 82 | 83 | [$exclusionRegex, $inclusionRegex] = $regexps; 84 | 85 | if (preg_match($exclusionRegex, $fileRelativePath)) { 86 | $ignored = true; 87 | 88 | continue; 89 | } 90 | 91 | if (preg_match($inclusionRegex, $fileRelativePath)) { 92 | $ignored = false; 93 | } 94 | } 95 | 96 | return $this->ignoredPathsCache[$fileRealPath] = $ignored; 97 | } 98 | 99 | /** 100 | * @return list 101 | */ 102 | private function parentDirectoriesUpwards(string $from): array 103 | { 104 | $parentDirectories = []; 105 | 106 | $parentDirectory = $from; 107 | 108 | while (true) { 109 | $newParentDirectory = \dirname($parentDirectory); 110 | 111 | // dirname('/') = '/' 112 | if ($newParentDirectory === $parentDirectory) { 113 | break; 114 | } 115 | 116 | $parentDirectories[] = $parentDirectory = $newParentDirectory; 117 | } 118 | 119 | return $parentDirectories; 120 | } 121 | 122 | private function parentDirectoriesUpTo(string $from, string $upTo): array 123 | { 124 | return array_filter( 125 | $this->parentDirectoriesUpwards($from), 126 | static fn (string $directory): bool => str_starts_with($directory, $upTo) 127 | ); 128 | } 129 | 130 | /** 131 | * @return list 132 | */ 133 | private function parentDirectoriesDownwards(string $fileRealPath): array 134 | { 135 | return array_reverse( 136 | $this->parentDirectoriesUpTo($fileRealPath, $this->baseDir) 137 | ); 138 | } 139 | 140 | /** 141 | * @return array{0: string, 1: string}|null 142 | */ 143 | private function readGitignoreFile(string $path): ?array 144 | { 145 | if (\array_key_exists($path, $this->gitignoreFilesCache)) { 146 | return $this->gitignoreFilesCache[$path]; 147 | } 148 | 149 | if (!file_exists($path)) { 150 | return $this->gitignoreFilesCache[$path] = null; 151 | } 152 | 153 | if (!is_file($path) || !is_readable($path)) { 154 | throw new \RuntimeException("The \"ignoreVCSIgnored\" option cannot be used by the Finder as the \"{$path}\" file is not readable."); 155 | } 156 | 157 | $gitignoreFileContent = file_get_contents($path); 158 | 159 | return $this->gitignoreFilesCache[$path] = [ 160 | Gitignore::toRegex($gitignoreFileContent), 161 | Gitignore::toRegexMatchingNegatedPatterns($gitignoreFileContent), 162 | ]; 163 | } 164 | 165 | private function normalizePath(string $path): string 166 | { 167 | if ('\\' === \DIRECTORY_SEPARATOR) { 168 | return str_replace('\\', '/', $path); 169 | } 170 | 171 | return $path; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2004-present Fabien Potencier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Finder Component 2 | ================ 3 | 4 | The Finder component finds files and directories via an intuitive fluent 5 | interface. 6 | 7 | Resources 8 | --------- 9 | 10 | * [Documentation](https://symfony.com/doc/current/components/finder.html) 11 | * [Contributing](https://symfony.com/doc/current/contributing/index.html) 12 | * [Report issues](https://github.com/symfony/symfony/issues) and 13 | [send Pull Requests](https://github.com/symfony/symfony/pulls) 14 | in the [main Symfony repository](https://github.com/symfony/symfony) 15 | -------------------------------------------------------------------------------- /SplFileInfo.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Symfony\Component\Finder; 13 | 14 | /** 15 | * Extends \SplFileInfo to support relative paths. 16 | * 17 | * @author Fabien Potencier 18 | */ 19 | class SplFileInfo extends \SplFileInfo 20 | { 21 | /** 22 | * @param string $file The file name 23 | * @param string $relativePath The relative path 24 | * @param string $relativePathname The relative path name 25 | */ 26 | public function __construct( 27 | string $file, 28 | private string $relativePath, 29 | private string $relativePathname, 30 | ) { 31 | parent::__construct($file); 32 | } 33 | 34 | /** 35 | * Returns the relative path. 36 | * 37 | * This path does not contain the file name. 38 | */ 39 | public function getRelativePath(): string 40 | { 41 | return $this->relativePath; 42 | } 43 | 44 | /** 45 | * Returns the relative path name. 46 | * 47 | * This path contains the file name. 48 | */ 49 | public function getRelativePathname(): string 50 | { 51 | return $this->relativePathname; 52 | } 53 | 54 | public function getFilenameWithoutExtension(): string 55 | { 56 | $filename = $this->getFilename(); 57 | 58 | return pathinfo($filename, \PATHINFO_FILENAME); 59 | } 60 | 61 | /** 62 | * Returns the contents of the file. 63 | * 64 | * @throws \RuntimeException 65 | */ 66 | public function getContents(): string 67 | { 68 | set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; }); 69 | try { 70 | $content = file_get_contents($this->getPathname()); 71 | } finally { 72 | restore_error_handler(); 73 | } 74 | if (false === $content) { 75 | throw new \RuntimeException($error); 76 | } 77 | 78 | return $content; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symfony/finder", 3 | "type": "library", 4 | "description": "Finds files and directories via an intuitive fluent interface", 5 | "keywords": [], 6 | "homepage": "https://symfony.com", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Fabien Potencier", 11 | "email": "fabien@symfony.com" 12 | }, 13 | { 14 | "name": "Symfony Community", 15 | "homepage": "https://symfony.com/contributors" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=8.2" 20 | }, 21 | "require-dev": { 22 | "symfony/filesystem": "^6.4|^7.0" 23 | }, 24 | "autoload": { 25 | "psr-4": { "Symfony\\Component\\Finder\\": "" }, 26 | "exclude-from-classmap": [ 27 | "/Tests/" 28 | ] 29 | }, 30 | "minimum-stability": "dev" 31 | } 32 | --------------------------------------------------------------------------------