1<?php 2 3/* 4 * This file is part of the Symfony package. 5 * 6 * (c) Fabien Potencier <fabien@symfony.com> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Symfony\Component\Finder; 13 14use Symfony\Component\Finder\Adapter\AdapterInterface; 15use Symfony\Component\Finder\Adapter\GnuFindAdapter; 16use Symfony\Component\Finder\Adapter\BsdFindAdapter; 17use Symfony\Component\Finder\Adapter\PhpAdapter; 18use Symfony\Component\Finder\Comparator\DateComparator; 19use Symfony\Component\Finder\Comparator\NumberComparator; 20use Symfony\Component\Finder\Exception\ExceptionInterface; 21use Symfony\Component\Finder\Iterator\CustomFilterIterator; 22use Symfony\Component\Finder\Iterator\DateRangeFilterIterator; 23use Symfony\Component\Finder\Iterator\DepthRangeFilterIterator; 24use Symfony\Component\Finder\Iterator\ExcludeDirectoryFilterIterator; 25use Symfony\Component\Finder\Iterator\FilecontentFilterIterator; 26use Symfony\Component\Finder\Iterator\FilenameFilterIterator; 27use Symfony\Component\Finder\Iterator\SizeRangeFilterIterator; 28use Symfony\Component\Finder\Iterator\SortableIterator; 29 30/** 31 * Finder allows to build rules to find files and directories. 32 * 33 * It is a thin wrapper around several specialized iterator classes. 34 * 35 * All rules may be invoked several times. 36 * 37 * All methods return the current Finder object to allow easy chaining: 38 * 39 * $finder = Finder::create()->files()->name('*.php')->in(__DIR__); 40 * 41 * @author Fabien Potencier <fabien@symfony.com> 42 * 43 * @api 44 */ 45class Finder implements \IteratorAggregate, \Countable 46{ 47 const IGNORE_VCS_FILES = 1; 48 const IGNORE_DOT_FILES = 2; 49 50 private $mode = 0; 51 private $names = array(); 52 private $notNames = array(); 53 private $exclude = array(); 54 private $filters = array(); 55 private $depths = array(); 56 private $sizes = array(); 57 private $followLinks = false; 58 private $sort = false; 59 private $ignore = 0; 60 private $dirs = array(); 61 private $dates = array(); 62 private $iterators = array(); 63 private $contains = array(); 64 private $notContains = array(); 65 private $adapters = array(); 66 private $paths = array(); 67 private $notPaths = array(); 68 private $ignoreUnreadableDirs = false; 69 70 private static $vcsPatterns = array('.svn', '_svn', 'CVS', '_darcs', '.arch-params', '.monotone', '.bzr', '.git', '.hg'); 71 72 /** 73 * Constructor. 74 */ 75 public function __construct() 76 { 77 $this->ignore = static::IGNORE_VCS_FILES | static::IGNORE_DOT_FILES; 78 79 $this 80 ->addAdapter(new GnuFindAdapter()) 81 ->addAdapter(new BsdFindAdapter()) 82 ->addAdapter(new PhpAdapter(), -50) 83 ->setAdapter('php') 84 ; 85 } 86 87 /** 88 * Creates a new Finder. 89 * 90 * @return Finder A new Finder instance 91 * 92 * @api 93 */ 94 public static function create() 95 { 96 return new static(); 97 } 98 99 /** 100 * Registers a finder engine implementation. 101 * 102 * @param AdapterInterface $adapter An adapter instance 103 * @param int $priority Highest is selected first 104 * 105 * @return Finder The current Finder instance 106 */ 107 public function addAdapter(AdapterInterface $adapter, $priority = 0) 108 { 109 $this->adapters[$adapter->getName()] = array( 110 'adapter' => $adapter, 111 'priority' => $priority, 112 'selected' => false, 113 ); 114 115 return $this->sortAdapters(); 116 } 117 118 /** 119 * Sets the selected adapter to the best one according to the current platform the code is run on. 120 * 121 * @return Finder The current Finder instance 122 */ 123 public function useBestAdapter() 124 { 125 $this->resetAdapterSelection(); 126 127 return $this->sortAdapters(); 128 } 129 130 /** 131 * Selects the adapter to use. 132 * 133 * @param string $name 134 * 135 * @throws \InvalidArgumentException 136 * 137 * @return Finder The current Finder instance 138 */ 139 public function setAdapter($name) 140 { 141 if (!isset($this->adapters[$name])) { 142 throw new \InvalidArgumentException(sprintf('Adapter "%s" does not exist.', $name)); 143 } 144 145 $this->resetAdapterSelection(); 146 $this->adapters[$name]['selected'] = true; 147 148 return $this->sortAdapters(); 149 } 150 151 /** 152 * Removes all adapters registered in the finder. 153 * 154 * @return Finder The current Finder instance 155 */ 156 public function removeAdapters() 157 { 158 $this->adapters = array(); 159 160 return $this; 161 } 162 163 /** 164 * Returns registered adapters ordered by priority without extra information. 165 * 166 * @return AdapterInterface[] 167 */ 168 public function getAdapters() 169 { 170 return array_values(array_map(function (array $adapter) { 171 return $adapter['adapter']; 172 }, $this->adapters)); 173 } 174 175 /** 176 * Restricts the matching to directories only. 177 * 178 * @return Finder The current Finder instance 179 * 180 * @api 181 */ 182 public function directories() 183 { 184 $this->mode = Iterator\FileTypeFilterIterator::ONLY_DIRECTORIES; 185 186 return $this; 187 } 188 189 /** 190 * Restricts the matching to files only. 191 * 192 * @return Finder The current Finder instance 193 * 194 * @api 195 */ 196 public function files() 197 { 198 $this->mode = Iterator\FileTypeFilterIterator::ONLY_FILES; 199 200 return $this; 201 } 202 203 /** 204 * Adds tests for the directory depth. 205 * 206 * Usage: 207 * 208 * $finder->depth('> 1') // the Finder will start matching at level 1. 209 * $finder->depth('< 3') // the Finder will descend at most 3 levels of directories below the starting point. 210 * 211 * @param int $level The depth level expression 212 * 213 * @return Finder The current Finder instance 214 * 215 * @see DepthRangeFilterIterator 216 * @see NumberComparator 217 * 218 * @api 219 */ 220 public function depth($level) 221 { 222 $this->depths[] = new Comparator\NumberComparator($level); 223 224 return $this; 225 } 226 227 /** 228 * Adds tests for file dates (last modified). 229 * 230 * The date must be something that strtotime() is able to parse: 231 * 232 * $finder->date('since yesterday'); 233 * $finder->date('until 2 days ago'); 234 * $finder->date('> now - 2 hours'); 235 * $finder->date('>= 2005-10-15'); 236 * 237 * @param string $date A date range string 238 * 239 * @return Finder The current Finder instance 240 * 241 * @see strtotime 242 * @see DateRangeFilterIterator 243 * @see DateComparator 244 * 245 * @api 246 */ 247 public function date($date) 248 { 249 $this->dates[] = new Comparator\DateComparator($date); 250 251 return $this; 252 } 253 254 /** 255 * Adds rules that files must match. 256 * 257 * You can use patterns (delimited with / sign), globs or simple strings. 258 * 259 * $finder->name('*.php') 260 * $finder->name('/\.php$/') // same as above 261 * $finder->name('test.php') 262 * 263 * @param string $pattern A pattern (a regexp, a glob, or a string) 264 * 265 * @return Finder The current Finder instance 266 * 267 * @see FilenameFilterIterator 268 * 269 * @api 270 */ 271 public function name($pattern) 272 { 273 $this->names[] = $pattern; 274 275 return $this; 276 } 277 278 /** 279 * Adds rules that files must not match. 280 * 281 * @param string $pattern A pattern (a regexp, a glob, or a string) 282 * 283 * @return Finder The current Finder instance 284 * 285 * @see FilenameFilterIterator 286 * 287 * @api 288 */ 289 public function notName($pattern) 290 { 291 $this->notNames[] = $pattern; 292 293 return $this; 294 } 295 296 /** 297 * Adds tests that file contents must match. 298 * 299 * Strings or PCRE patterns can be used: 300 * 301 * $finder->contains('Lorem ipsum') 302 * $finder->contains('/Lorem ipsum/i') 303 * 304 * @param string $pattern A pattern (string or regexp) 305 * 306 * @return Finder The current Finder instance 307 * 308 * @see FilecontentFilterIterator 309 */ 310 public function contains($pattern) 311 { 312 $this->contains[] = $pattern; 313 314 return $this; 315 } 316 317 /** 318 * Adds tests that file contents must not match. 319 * 320 * Strings or PCRE patterns can be used: 321 * 322 * $finder->notContains('Lorem ipsum') 323 * $finder->notContains('/Lorem ipsum/i') 324 * 325 * @param string $pattern A pattern (string or regexp) 326 * 327 * @return Finder The current Finder instance 328 * 329 * @see FilecontentFilterIterator 330 */ 331 public function notContains($pattern) 332 { 333 $this->notContains[] = $pattern; 334 335 return $this; 336 } 337 338 /** 339 * Adds rules that filenames must match. 340 * 341 * You can use patterns (delimited with / sign) or simple strings. 342 * 343 * $finder->path('some/special/dir') 344 * $finder->path('/some\/special\/dir/') // same as above 345 * 346 * Use only / as dirname separator. 347 * 348 * @param string $pattern A pattern (a regexp or a string) 349 * 350 * @return Finder The current Finder instance 351 * 352 * @see FilenameFilterIterator 353 */ 354 public function path($pattern) 355 { 356 $this->paths[] = $pattern; 357 358 return $this; 359 } 360 361 /** 362 * Adds rules that filenames must not match. 363 * 364 * You can use patterns (delimited with / sign) or simple strings. 365 * 366 * $finder->notPath('some/special/dir') 367 * $finder->notPath('/some\/special\/dir/') // same as above 368 * 369 * Use only / as dirname separator. 370 * 371 * @param string $pattern A pattern (a regexp or a string) 372 * 373 * @return Finder The current Finder instance 374 * 375 * @see FilenameFilterIterator 376 */ 377 public function notPath($pattern) 378 { 379 $this->notPaths[] = $pattern; 380 381 return $this; 382 } 383 384 /** 385 * Adds tests for file sizes. 386 * 387 * $finder->size('> 10K'); 388 * $finder->size('<= 1Ki'); 389 * $finder->size(4); 390 * 391 * @param string $size A size range string 392 * 393 * @return Finder The current Finder instance 394 * 395 * @see SizeRangeFilterIterator 396 * @see NumberComparator 397 * 398 * @api 399 */ 400 public function size($size) 401 { 402 $this->sizes[] = new Comparator\NumberComparator($size); 403 404 return $this; 405 } 406 407 /** 408 * Excludes directories. 409 * 410 * @param string|array $dirs A directory path or an array of directories 411 * 412 * @return Finder The current Finder instance 413 * 414 * @see ExcludeDirectoryFilterIterator 415 * 416 * @api 417 */ 418 public function exclude($dirs) 419 { 420 $this->exclude = array_merge($this->exclude, (array) $dirs); 421 422 return $this; 423 } 424 425 /** 426 * Excludes "hidden" directories and files (starting with a dot). 427 * 428 * @param bool $ignoreDotFiles Whether to exclude "hidden" files or not 429 * 430 * @return Finder The current Finder instance 431 * 432 * @see ExcludeDirectoryFilterIterator 433 * 434 * @api 435 */ 436 public function ignoreDotFiles($ignoreDotFiles) 437 { 438 if ($ignoreDotFiles) { 439 $this->ignore |= static::IGNORE_DOT_FILES; 440 } else { 441 $this->ignore &= ~static::IGNORE_DOT_FILES; 442 } 443 444 return $this; 445 } 446 447 /** 448 * Forces the finder to ignore version control directories. 449 * 450 * @param bool $ignoreVCS Whether to exclude VCS files or not 451 * 452 * @return Finder The current Finder instance 453 * 454 * @see ExcludeDirectoryFilterIterator 455 * 456 * @api 457 */ 458 public function ignoreVCS($ignoreVCS) 459 { 460 if ($ignoreVCS) { 461 $this->ignore |= static::IGNORE_VCS_FILES; 462 } else { 463 $this->ignore &= ~static::IGNORE_VCS_FILES; 464 } 465 466 return $this; 467 } 468 469 /** 470 * Adds VCS patterns. 471 * 472 * @see ignoreVCS() 473 * 474 * @param string|string[] $pattern VCS patterns to ignore 475 */ 476 public static function addVCSPattern($pattern) 477 { 478 foreach ((array) $pattern as $p) { 479 self::$vcsPatterns[] = $p; 480 } 481 482 self::$vcsPatterns = array_unique(self::$vcsPatterns); 483 } 484 485 /** 486 * Sorts files and directories by an anonymous function. 487 * 488 * The anonymous function receives two \SplFileInfo instances to compare. 489 * 490 * This can be slow as all the matching files and directories must be retrieved for comparison. 491 * 492 * @param \Closure $closure An anonymous function 493 * 494 * @return Finder The current Finder instance 495 * 496 * @see SortableIterator 497 * 498 * @api 499 */ 500 public function sort(\Closure $closure) 501 { 502 $this->sort = $closure; 503 504 return $this; 505 } 506 507 /** 508 * Sorts files and directories by name. 509 * 510 * This can be slow as all the matching files and directories must be retrieved for comparison. 511 * 512 * @return Finder The current Finder instance 513 * 514 * @see SortableIterator 515 * 516 * @api 517 */ 518 public function sortByName() 519 { 520 $this->sort = Iterator\SortableIterator::SORT_BY_NAME; 521 522 return $this; 523 } 524 525 /** 526 * Sorts files and directories by type (directories before files), then by name. 527 * 528 * This can be slow as all the matching files and directories must be retrieved for comparison. 529 * 530 * @return Finder The current Finder instance 531 * 532 * @see SortableIterator 533 * 534 * @api 535 */ 536 public function sortByType() 537 { 538 $this->sort = Iterator\SortableIterator::SORT_BY_TYPE; 539 540 return $this; 541 } 542 543 /** 544 * Sorts files and directories by the last accessed time. 545 * 546 * This is the time that the file was last accessed, read or written to. 547 * 548 * This can be slow as all the matching files and directories must be retrieved for comparison. 549 * 550 * @return Finder The current Finder instance 551 * 552 * @see SortableIterator 553 * 554 * @api 555 */ 556 public function sortByAccessedTime() 557 { 558 $this->sort = Iterator\SortableIterator::SORT_BY_ACCESSED_TIME; 559 560 return $this; 561 } 562 563 /** 564 * Sorts files and directories by the last inode changed time. 565 * 566 * This is the time that the inode information was last modified (permissions, owner, group or other metadata). 567 * 568 * On Windows, since inode is not available, changed time is actually the file creation time. 569 * 570 * This can be slow as all the matching files and directories must be retrieved for comparison. 571 * 572 * @return Finder The current Finder instance 573 * 574 * @see SortableIterator 575 * 576 * @api 577 */ 578 public function sortByChangedTime() 579 { 580 $this->sort = Iterator\SortableIterator::SORT_BY_CHANGED_TIME; 581 582 return $this; 583 } 584 585 /** 586 * Sorts files and directories by the last modified time. 587 * 588 * This is the last time the actual contents of the file were last modified. 589 * 590 * This can be slow as all the matching files and directories must be retrieved for comparison. 591 * 592 * @return Finder The current Finder instance 593 * 594 * @see SortableIterator 595 * 596 * @api 597 */ 598 public function sortByModifiedTime() 599 { 600 $this->sort = Iterator\SortableIterator::SORT_BY_MODIFIED_TIME; 601 602 return $this; 603 } 604 605 /** 606 * Filters the iterator with an anonymous function. 607 * 608 * The anonymous function receives a \SplFileInfo and must return false 609 * to remove files. 610 * 611 * @param \Closure $closure An anonymous function 612 * 613 * @return Finder The current Finder instance 614 * 615 * @see CustomFilterIterator 616 * 617 * @api 618 */ 619 public function filter(\Closure $closure) 620 { 621 $this->filters[] = $closure; 622 623 return $this; 624 } 625 626 /** 627 * Forces the following of symlinks. 628 * 629 * @return Finder The current Finder instance 630 * 631 * @api 632 */ 633 public function followLinks() 634 { 635 $this->followLinks = true; 636 637 return $this; 638 } 639 640 /** 641 * Tells finder to ignore unreadable directories. 642 * 643 * By default, scanning unreadable directories content throws an AccessDeniedException. 644 * 645 * @param bool $ignore 646 * 647 * @return Finder The current Finder instance 648 */ 649 public function ignoreUnreadableDirs($ignore = true) 650 { 651 $this->ignoreUnreadableDirs = (bool) $ignore; 652 653 return $this; 654 } 655 656 /** 657 * Searches files and directories which match defined rules. 658 * 659 * @param string|array $dirs A directory path or an array of directories 660 * 661 * @return Finder The current Finder instance 662 * 663 * @throws \InvalidArgumentException if one of the directories does not exist 664 * 665 * @api 666 */ 667 public function in($dirs) 668 { 669 $resolvedDirs = array(); 670 671 foreach ((array) $dirs as $dir) { 672 if (is_dir($dir)) { 673 $resolvedDirs[] = $dir; 674 } elseif ($glob = glob($dir, (defined('GLOB_BRACE') ? GLOB_BRACE : 0) | GLOB_ONLYDIR)) { 675 $resolvedDirs = array_merge($resolvedDirs, $glob); 676 } else { 677 throw new \InvalidArgumentException(sprintf('The "%s" directory does not exist.', $dir)); 678 } 679 } 680 681 $this->dirs = array_merge($this->dirs, $resolvedDirs); 682 683 return $this; 684 } 685 686 /** 687 * Returns an Iterator for the current Finder configuration. 688 * 689 * This method implements the IteratorAggregate interface. 690 * 691 * @return \Iterator An iterator 692 * 693 * @throws \LogicException if the in() method has not been called 694 */ 695 public function getIterator() 696 { 697 if (0 === count($this->dirs) && 0 === count($this->iterators)) { 698 throw new \LogicException('You must call one of in() or append() methods before iterating over a Finder.'); 699 } 700 701 if (1 === count($this->dirs) && 0 === count($this->iterators)) { 702 return $this->searchInDirectory($this->dirs[0]); 703 } 704 705 $iterator = new \AppendIterator(); 706 foreach ($this->dirs as $dir) { 707 $iterator->append($this->searchInDirectory($dir)); 708 } 709 710 foreach ($this->iterators as $it) { 711 $iterator->append($it); 712 } 713 714 return $iterator; 715 } 716 717 /** 718 * Appends an existing set of files/directories to the finder. 719 * 720 * The set can be another Finder, an Iterator, an IteratorAggregate, or even a plain array. 721 * 722 * @param mixed $iterator 723 * 724 * @return Finder The finder 725 * 726 * @throws \InvalidArgumentException When the given argument is not iterable. 727 */ 728 public function append($iterator) 729 { 730 if ($iterator instanceof \IteratorAggregate) { 731 $this->iterators[] = $iterator->getIterator(); 732 } elseif ($iterator instanceof \Iterator) { 733 $this->iterators[] = $iterator; 734 } elseif ($iterator instanceof \Traversable || is_array($iterator)) { 735 $it = new \ArrayIterator(); 736 foreach ($iterator as $file) { 737 $it->append($file instanceof \SplFileInfo ? $file : new \SplFileInfo($file)); 738 } 739 $this->iterators[] = $it; 740 } else { 741 throw new \InvalidArgumentException('Finder::append() method wrong argument type.'); 742 } 743 744 return $this; 745 } 746 747 /** 748 * Counts all the results collected by the iterators. 749 * 750 * @return int 751 */ 752 public function count() 753 { 754 return iterator_count($this->getIterator()); 755 } 756 757 /** 758 * @return Finder The current Finder instance 759 */ 760 private function sortAdapters() 761 { 762 uasort($this->adapters, function (array $a, array $b) { 763 if ($a['selected'] || $b['selected']) { 764 return $a['selected'] ? -1 : 1; 765 } 766 767 return $a['priority'] > $b['priority'] ? -1 : 1; 768 }); 769 770 return $this; 771 } 772 773 /** 774 * @param $dir 775 * 776 * @return \Iterator 777 * 778 * @throws \RuntimeException When none of the adapters are supported 779 */ 780 private function searchInDirectory($dir) 781 { 782 if (static::IGNORE_VCS_FILES === (static::IGNORE_VCS_FILES & $this->ignore)) { 783 $this->exclude = array_merge($this->exclude, self::$vcsPatterns); 784 } 785 786 if (static::IGNORE_DOT_FILES === (static::IGNORE_DOT_FILES & $this->ignore)) { 787 $this->notPaths[] = '#(^|/)\..+(/|$)#'; 788 } 789 790 foreach ($this->adapters as $adapter) { 791 if ($adapter['adapter']->isSupported()) { 792 try { 793 return $this 794 ->buildAdapter($adapter['adapter']) 795 ->searchInDirectory($dir); 796 } catch (ExceptionInterface $e) { 797 } 798 } 799 } 800 801 throw new \RuntimeException('No supported adapter found.'); 802 } 803 804 /** 805 * @param AdapterInterface $adapter 806 * 807 * @return AdapterInterface 808 */ 809 private function buildAdapter(AdapterInterface $adapter) 810 { 811 return $adapter 812 ->setFollowLinks($this->followLinks) 813 ->setDepths($this->depths) 814 ->setMode($this->mode) 815 ->setExclude($this->exclude) 816 ->setNames($this->names) 817 ->setNotNames($this->notNames) 818 ->setContains($this->contains) 819 ->setNotContains($this->notContains) 820 ->setSizes($this->sizes) 821 ->setDates($this->dates) 822 ->setFilters($this->filters) 823 ->setSort($this->sort) 824 ->setPath($this->paths) 825 ->setNotPath($this->notPaths) 826 ->ignoreUnreadableDirs($this->ignoreUnreadableDirs); 827 } 828 829 /** 830 * Unselects all adapters. 831 */ 832 private function resetAdapterSelection() 833 { 834 $this->adapters = array_map(function (array $properties) { 835 $properties['selected'] = false; 836 837 return $properties; 838 }, $this->adapters); 839 } 840} 841