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