vendor/symfony/finder/Finder.php line 721

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\Finder;
  11. use Symfony\Component\Finder\Comparator\DateComparator;
  12. use Symfony\Component\Finder\Comparator\NumberComparator;
  13. use Symfony\Component\Finder\Iterator\CustomFilterIterator;
  14. use Symfony\Component\Finder\Iterator\DateRangeFilterIterator;
  15. use Symfony\Component\Finder\Iterator\DepthRangeFilterIterator;
  16. use Symfony\Component\Finder\Iterator\ExcludeDirectoryFilterIterator;
  17. use Symfony\Component\Finder\Iterator\FilecontentFilterIterator;
  18. use Symfony\Component\Finder\Iterator\FilenameFilterIterator;
  19. use Symfony\Component\Finder\Iterator\SizeRangeFilterIterator;
  20. use Symfony\Component\Finder\Iterator\SortableIterator;
  21. /**
  22.  * Finder allows to build rules to find files and directories.
  23.  *
  24.  * It is a thin wrapper around several specialized iterator classes.
  25.  *
  26.  * All rules may be invoked several times.
  27.  *
  28.  * All methods return the current Finder object to allow chaining:
  29.  *
  30.  *     $finder = Finder::create()->files()->name('*.php')->in(__DIR__);
  31.  *
  32.  * @author Fabien Potencier <fabien@symfony.com>
  33.  */
  34. class Finder implements \IteratorAggregate\Countable
  35. {
  36.     const IGNORE_VCS_FILES 1;
  37.     const IGNORE_DOT_FILES 2;
  38.     private $mode 0;
  39.     private $names = [];
  40.     private $notNames = [];
  41.     private $exclude = [];
  42.     private $filters = [];
  43.     private $depths = [];
  44.     private $sizes = [];
  45.     private $followLinks false;
  46.     private $sort false;
  47.     private $ignore 0;
  48.     private $dirs = [];
  49.     private $dates = [];
  50.     private $iterators = [];
  51.     private $contains = [];
  52.     private $notContains = [];
  53.     private $paths = [];
  54.     private $notPaths = [];
  55.     private $ignoreUnreadableDirs false;
  56.     private static $vcsPatterns = ['.svn''_svn''CVS''_darcs''.arch-params''.monotone''.bzr''.git''.hg'];
  57.     public function __construct()
  58.     {
  59.         $this->ignore = static::IGNORE_VCS_FILES | static::IGNORE_DOT_FILES;
  60.     }
  61.     /**
  62.      * Creates a new Finder.
  63.      *
  64.      * @return static
  65.      */
  66.     public static function create()
  67.     {
  68.         return new static();
  69.     }
  70.     /**
  71.      * Restricts the matching to directories only.
  72.      *
  73.      * @return $this
  74.      */
  75.     public function directories()
  76.     {
  77.         $this->mode Iterator\FileTypeFilterIterator::ONLY_DIRECTORIES;
  78.         return $this;
  79.     }
  80.     /**
  81.      * Restricts the matching to files only.
  82.      *
  83.      * @return $this
  84.      */
  85.     public function files()
  86.     {
  87.         $this->mode Iterator\FileTypeFilterIterator::ONLY_FILES;
  88.         return $this;
  89.     }
  90.     /**
  91.      * Adds tests for the directory depth.
  92.      *
  93.      * Usage:
  94.      *
  95.      *     $finder->depth('> 1') // the Finder will start matching at level 1.
  96.      *     $finder->depth('< 3') // the Finder will descend at most 3 levels of directories below the starting point.
  97.      *
  98.      * @param string|int $level The depth level expression
  99.      *
  100.      * @return $this
  101.      *
  102.      * @see DepthRangeFilterIterator
  103.      * @see NumberComparator
  104.      */
  105.     public function depth($level)
  106.     {
  107.         $this->depths[] = new Comparator\NumberComparator($level);
  108.         return $this;
  109.     }
  110.     /**
  111.      * Adds tests for file dates (last modified).
  112.      *
  113.      * The date must be something that strtotime() is able to parse:
  114.      *
  115.      *     $finder->date('since yesterday');
  116.      *     $finder->date('until 2 days ago');
  117.      *     $finder->date('> now - 2 hours');
  118.      *     $finder->date('>= 2005-10-15');
  119.      *
  120.      * @param string $date A date range string
  121.      *
  122.      * @return $this
  123.      *
  124.      * @see strtotime
  125.      * @see DateRangeFilterIterator
  126.      * @see DateComparator
  127.      */
  128.     public function date($date)
  129.     {
  130.         $this->dates[] = new Comparator\DateComparator($date);
  131.         return $this;
  132.     }
  133.     /**
  134.      * Adds rules that files must match.
  135.      *
  136.      * You can use patterns (delimited with / sign), globs or simple strings.
  137.      *
  138.      *     $finder->name('*.php')
  139.      *     $finder->name('/\.php$/') // same as above
  140.      *     $finder->name('test.php')
  141.      *
  142.      * @param string $pattern A pattern (a regexp, a glob, or a string)
  143.      *
  144.      * @return $this
  145.      *
  146.      * @see FilenameFilterIterator
  147.      */
  148.     public function name($pattern)
  149.     {
  150.         $this->names[] = $pattern;
  151.         return $this;
  152.     }
  153.     /**
  154.      * Adds rules that files must not match.
  155.      *
  156.      * @param string $pattern A pattern (a regexp, a glob, or a string)
  157.      *
  158.      * @return $this
  159.      *
  160.      * @see FilenameFilterIterator
  161.      */
  162.     public function notName($pattern)
  163.     {
  164.         $this->notNames[] = $pattern;
  165.         return $this;
  166.     }
  167.     /**
  168.      * Adds tests that file contents must match.
  169.      *
  170.      * Strings or PCRE patterns can be used:
  171.      *
  172.      *     $finder->contains('Lorem ipsum')
  173.      *     $finder->contains('/Lorem ipsum/i')
  174.      *
  175.      * @param string $pattern A pattern (string or regexp)
  176.      *
  177.      * @return $this
  178.      *
  179.      * @see FilecontentFilterIterator
  180.      */
  181.     public function contains($pattern)
  182.     {
  183.         $this->contains[] = $pattern;
  184.         return $this;
  185.     }
  186.     /**
  187.      * Adds tests that file contents must not match.
  188.      *
  189.      * Strings or PCRE patterns can be used:
  190.      *
  191.      *     $finder->notContains('Lorem ipsum')
  192.      *     $finder->notContains('/Lorem ipsum/i')
  193.      *
  194.      * @param string $pattern A pattern (string or regexp)
  195.      *
  196.      * @return $this
  197.      *
  198.      * @see FilecontentFilterIterator
  199.      */
  200.     public function notContains($pattern)
  201.     {
  202.         $this->notContains[] = $pattern;
  203.         return $this;
  204.     }
  205.     /**
  206.      * Adds rules that filenames must match.
  207.      *
  208.      * You can use patterns (delimited with / sign) or simple strings.
  209.      *
  210.      *     $finder->path('some/special/dir')
  211.      *     $finder->path('/some\/special\/dir/') // same as above
  212.      *
  213.      * Use only / as dirname separator.
  214.      *
  215.      * @param string $pattern A pattern (a regexp or a string)
  216.      *
  217.      * @return $this
  218.      *
  219.      * @see FilenameFilterIterator
  220.      */
  221.     public function path($pattern)
  222.     {
  223.         $this->paths[] = $pattern;
  224.         return $this;
  225.     }
  226.     /**
  227.      * Adds rules that filenames must not match.
  228.      *
  229.      * You can use patterns (delimited with / sign) or simple strings.
  230.      *
  231.      *     $finder->notPath('some/special/dir')
  232.      *     $finder->notPath('/some\/special\/dir/') // same as above
  233.      *
  234.      * Use only / as dirname separator.
  235.      *
  236.      * @param string $pattern A pattern (a regexp or a string)
  237.      *
  238.      * @return $this
  239.      *
  240.      * @see FilenameFilterIterator
  241.      */
  242.     public function notPath($pattern)
  243.     {
  244.         $this->notPaths[] = $pattern;
  245.         return $this;
  246.     }
  247.     /**
  248.      * Adds tests for file sizes.
  249.      *
  250.      *     $finder->size('> 10K');
  251.      *     $finder->size('<= 1Ki');
  252.      *     $finder->size(4);
  253.      *
  254.      * @param string|int $size A size range string or an integer
  255.      *
  256.      * @return $this
  257.      *
  258.      * @see SizeRangeFilterIterator
  259.      * @see NumberComparator
  260.      */
  261.     public function size($size)
  262.     {
  263.         $this->sizes[] = new Comparator\NumberComparator($size);
  264.         return $this;
  265.     }
  266.     /**
  267.      * Excludes directories.
  268.      *
  269.      * Directories passed as argument must be relative to the ones defined with the `in()` method. For example:
  270.      *
  271.      *     $finder->in(__DIR__)->exclude('ruby');
  272.      *
  273.      * @param string|array $dirs A directory path or an array of directories
  274.      *
  275.      * @return $this
  276.      *
  277.      * @see ExcludeDirectoryFilterIterator
  278.      */
  279.     public function exclude($dirs)
  280.     {
  281.         $this->exclude array_merge($this->exclude, (array) $dirs);
  282.         return $this;
  283.     }
  284.     /**
  285.      * Excludes "hidden" directories and files (starting with a dot).
  286.      *
  287.      * This option is enabled by default.
  288.      *
  289.      * @param bool $ignoreDotFiles Whether to exclude "hidden" files or not
  290.      *
  291.      * @return $this
  292.      *
  293.      * @see ExcludeDirectoryFilterIterator
  294.      */
  295.     public function ignoreDotFiles($ignoreDotFiles)
  296.     {
  297.         if ($ignoreDotFiles) {
  298.             $this->ignore |= static::IGNORE_DOT_FILES;
  299.         } else {
  300.             $this->ignore &= ~static::IGNORE_DOT_FILES;
  301.         }
  302.         return $this;
  303.     }
  304.     /**
  305.      * Forces the finder to ignore version control directories.
  306.      *
  307.      * This option is enabled by default.
  308.      *
  309.      * @param bool $ignoreVCS Whether to exclude VCS files or not
  310.      *
  311.      * @return $this
  312.      *
  313.      * @see ExcludeDirectoryFilterIterator
  314.      */
  315.     public function ignoreVCS($ignoreVCS)
  316.     {
  317.         if ($ignoreVCS) {
  318.             $this->ignore |= static::IGNORE_VCS_FILES;
  319.         } else {
  320.             $this->ignore &= ~static::IGNORE_VCS_FILES;
  321.         }
  322.         return $this;
  323.     }
  324.     /**
  325.      * Adds VCS patterns.
  326.      *
  327.      * @see ignoreVCS()
  328.      *
  329.      * @param string|string[] $pattern VCS patterns to ignore
  330.      */
  331.     public static function addVCSPattern($pattern)
  332.     {
  333.         foreach ((array) $pattern as $p) {
  334.             self::$vcsPatterns[] = $p;
  335.         }
  336.         self::$vcsPatterns array_unique(self::$vcsPatterns);
  337.     }
  338.     /**
  339.      * Sorts files and directories by an anonymous function.
  340.      *
  341.      * The anonymous function receives two \SplFileInfo instances to compare.
  342.      *
  343.      * This can be slow as all the matching files and directories must be retrieved for comparison.
  344.      *
  345.      * @return $this
  346.      *
  347.      * @see SortableIterator
  348.      */
  349.     public function sort(\Closure $closure)
  350.     {
  351.         $this->sort $closure;
  352.         return $this;
  353.     }
  354.     /**
  355.      * Sorts files and directories by name.
  356.      *
  357.      * This can be slow as all the matching files and directories must be retrieved for comparison.
  358.      *
  359.      * @return $this
  360.      *
  361.      * @see SortableIterator
  362.      */
  363.     public function sortByName()
  364.     {
  365.         $this->sort Iterator\SortableIterator::SORT_BY_NAME;
  366.         return $this;
  367.     }
  368.     /**
  369.      * Sorts files and directories by type (directories before files), then by name.
  370.      *
  371.      * This can be slow as all the matching files and directories must be retrieved for comparison.
  372.      *
  373.      * @return $this
  374.      *
  375.      * @see SortableIterator
  376.      */
  377.     public function sortByType()
  378.     {
  379.         $this->sort Iterator\SortableIterator::SORT_BY_TYPE;
  380.         return $this;
  381.     }
  382.     /**
  383.      * Sorts files and directories by the last accessed time.
  384.      *
  385.      * This is the time that the file was last accessed, read or written to.
  386.      *
  387.      * This can be slow as all the matching files and directories must be retrieved for comparison.
  388.      *
  389.      * @return $this
  390.      *
  391.      * @see SortableIterator
  392.      */
  393.     public function sortByAccessedTime()
  394.     {
  395.         $this->sort Iterator\SortableIterator::SORT_BY_ACCESSED_TIME;
  396.         return $this;
  397.     }
  398.     /**
  399.      * Sorts files and directories by the last inode changed time.
  400.      *
  401.      * This is the time that the inode information was last modified (permissions, owner, group or other metadata).
  402.      *
  403.      * On Windows, since inode is not available, changed time is actually the file creation time.
  404.      *
  405.      * This can be slow as all the matching files and directories must be retrieved for comparison.
  406.      *
  407.      * @return $this
  408.      *
  409.      * @see SortableIterator
  410.      */
  411.     public function sortByChangedTime()
  412.     {
  413.         $this->sort Iterator\SortableIterator::SORT_BY_CHANGED_TIME;
  414.         return $this;
  415.     }
  416.     /**
  417.      * Sorts files and directories by the last modified time.
  418.      *
  419.      * This is the last time the actual contents of the file were last modified.
  420.      *
  421.      * This can be slow as all the matching files and directories must be retrieved for comparison.
  422.      *
  423.      * @return $this
  424.      *
  425.      * @see SortableIterator
  426.      */
  427.     public function sortByModifiedTime()
  428.     {
  429.         $this->sort Iterator\SortableIterator::SORT_BY_MODIFIED_TIME;
  430.         return $this;
  431.     }
  432.     /**
  433.      * Filters the iterator with an anonymous function.
  434.      *
  435.      * The anonymous function receives a \SplFileInfo and must return false
  436.      * to remove files.
  437.      *
  438.      * @return $this
  439.      *
  440.      * @see CustomFilterIterator
  441.      */
  442.     public function filter(\Closure $closure)
  443.     {
  444.         $this->filters[] = $closure;
  445.         return $this;
  446.     }
  447.     /**
  448.      * Forces the following of symlinks.
  449.      *
  450.      * @return $this
  451.      */
  452.     public function followLinks()
  453.     {
  454.         $this->followLinks true;
  455.         return $this;
  456.     }
  457.     /**
  458.      * Tells finder to ignore unreadable directories.
  459.      *
  460.      * By default, scanning unreadable directories content throws an AccessDeniedException.
  461.      *
  462.      * @param bool $ignore
  463.      *
  464.      * @return $this
  465.      */
  466.     public function ignoreUnreadableDirs($ignore true)
  467.     {
  468.         $this->ignoreUnreadableDirs = (bool) $ignore;
  469.         return $this;
  470.     }
  471.     /**
  472.      * Searches files and directories which match defined rules.
  473.      *
  474.      * @param string|string[] $dirs A directory path or an array of directories
  475.      *
  476.      * @return $this
  477.      *
  478.      * @throws \InvalidArgumentException if one of the directories does not exist
  479.      */
  480.     public function in($dirs)
  481.     {
  482.         $resolvedDirs = [];
  483.         foreach ((array) $dirs as $dir) {
  484.             if (is_dir($dir)) {
  485.                 $resolvedDirs[] = $this->normalizeDir($dir);
  486.             } elseif ($glob glob($dir, (\defined('GLOB_BRACE') ? GLOB_BRACE 0) | GLOB_ONLYDIR GLOB_NOSORT)) {
  487.                 sort($glob);
  488.                 $resolvedDirs array_merge($resolvedDirsarray_map([$this'normalizeDir'], $glob));
  489.             } else {
  490.                 throw new \InvalidArgumentException(sprintf('The "%s" directory does not exist.'$dir));
  491.             }
  492.         }
  493.         $this->dirs array_merge($this->dirs$resolvedDirs);
  494.         return $this;
  495.     }
  496.     /**
  497.      * Returns an Iterator for the current Finder configuration.
  498.      *
  499.      * This method implements the IteratorAggregate interface.
  500.      *
  501.      * @return \Iterator|SplFileInfo[] An iterator
  502.      *
  503.      * @throws \LogicException if the in() method has not been called
  504.      */
  505.     public function getIterator()
  506.     {
  507.         if (=== \count($this->dirs) && === \count($this->iterators)) {
  508.             throw new \LogicException('You must call one of in() or append() methods before iterating over a Finder.');
  509.         }
  510.         if (=== \count($this->dirs) && === \count($this->iterators)) {
  511.             return $this->searchInDirectory($this->dirs[0]);
  512.         }
  513.         $iterator = new \AppendIterator();
  514.         foreach ($this->dirs as $dir) {
  515.             $iterator->append($this->searchInDirectory($dir));
  516.         }
  517.         foreach ($this->iterators as $it) {
  518.             $iterator->append($it);
  519.         }
  520.         return $iterator;
  521.     }
  522.     /**
  523.      * Appends an existing set of files/directories to the finder.
  524.      *
  525.      * The set can be another Finder, an Iterator, an IteratorAggregate, or even a plain array.
  526.      *
  527.      * @param iterable $iterator
  528.      *
  529.      * @return $this
  530.      *
  531.      * @throws \InvalidArgumentException when the given argument is not iterable
  532.      */
  533.     public function append($iterator)
  534.     {
  535.         if ($iterator instanceof \IteratorAggregate) {
  536.             $this->iterators[] = $iterator->getIterator();
  537.         } elseif ($iterator instanceof \Iterator) {
  538.             $this->iterators[] = $iterator;
  539.         } elseif ($iterator instanceof \Traversable || \is_array($iterator)) {
  540.             $it = new \ArrayIterator();
  541.             foreach ($iterator as $file) {
  542.                 $it->append($file instanceof \SplFileInfo $file : new \SplFileInfo($file));
  543.             }
  544.             $this->iterators[] = $it;
  545.         } else {
  546.             throw new \InvalidArgumentException('Finder::append() method wrong argument type.');
  547.         }
  548.         return $this;
  549.     }
  550.     /**
  551.      * Check if the any results were found.
  552.      *
  553.      * @return bool
  554.      */
  555.     public function hasResults()
  556.     {
  557.         foreach ($this->getIterator() as $_) {
  558.             return true;
  559.         }
  560.         return false;
  561.     }
  562.     /**
  563.      * Counts all the results collected by the iterators.
  564.      *
  565.      * @return int
  566.      */
  567.     public function count()
  568.     {
  569.         return iterator_count($this->getIterator());
  570.     }
  571.     /**
  572.      * @param string $dir
  573.      *
  574.      * @return \Iterator
  575.      */
  576.     private function searchInDirectory($dir)
  577.     {
  578.         $exclude $this->exclude;
  579.         $notPaths $this->notPaths;
  580.         if (static::IGNORE_VCS_FILES === (static::IGNORE_VCS_FILES $this->ignore)) {
  581.             $exclude array_merge($excludeself::$vcsPatterns);
  582.         }
  583.         if (static::IGNORE_DOT_FILES === (static::IGNORE_DOT_FILES $this->ignore)) {
  584.             $notPaths[] = '#(^|/)\..+(/|$)#';
  585.         }
  586.         $minDepth 0;
  587.         $maxDepth PHP_INT_MAX;
  588.         foreach ($this->depths as $comparator) {
  589.             switch ($comparator->getOperator()) {
  590.                 case '>':
  591.                     $minDepth $comparator->getTarget() + 1;
  592.                     break;
  593.                 case '>=':
  594.                     $minDepth $comparator->getTarget();
  595.                     break;
  596.                 case '<':
  597.                     $maxDepth $comparator->getTarget() - 1;
  598.                     break;
  599.                 case '<=':
  600.                     $maxDepth $comparator->getTarget();
  601.                     break;
  602.                 default:
  603.                     $minDepth $maxDepth $comparator->getTarget();
  604.             }
  605.         }
  606.         $flags \RecursiveDirectoryIterator::SKIP_DOTS;
  607.         if ($this->followLinks) {
  608.             $flags |= \RecursiveDirectoryIterator::FOLLOW_SYMLINKS;
  609.         }
  610.         $iterator = new Iterator\RecursiveDirectoryIterator($dir$flags$this->ignoreUnreadableDirs);
  611.         if ($exclude) {
  612.             $iterator = new Iterator\ExcludeDirectoryFilterIterator($iterator$exclude);
  613.         }
  614.         $iterator = new \RecursiveIteratorIterator($iterator\RecursiveIteratorIterator::SELF_FIRST);
  615.         if ($minDepth || $maxDepth PHP_INT_MAX) {
  616.             $iterator = new Iterator\DepthRangeFilterIterator($iterator$minDepth$maxDepth);
  617.         }
  618.         if ($this->mode) {
  619.             $iterator = new Iterator\FileTypeFilterIterator($iterator$this->mode);
  620.         }
  621.         if ($this->names || $this->notNames) {
  622.             $iterator = new Iterator\FilenameFilterIterator($iterator$this->names$this->notNames);
  623.         }
  624.         if ($this->contains || $this->notContains) {
  625.             $iterator = new Iterator\FilecontentFilterIterator($iterator$this->contains$this->notContains);
  626.         }
  627.         if ($this->sizes) {
  628.             $iterator = new Iterator\SizeRangeFilterIterator($iterator$this->sizes);
  629.         }
  630.         if ($this->dates) {
  631.             $iterator = new Iterator\DateRangeFilterIterator($iterator$this->dates);
  632.         }
  633.         if ($this->filters) {
  634.             $iterator = new Iterator\CustomFilterIterator($iterator$this->filters);
  635.         }
  636.         if ($this->paths || $notPaths) {
  637.             $iterator = new Iterator\PathFilterIterator($iterator$this->paths$notPaths);
  638.         }
  639.         if ($this->sort) {
  640.             $iteratorAggregate = new Iterator\SortableIterator($iterator$this->sort);
  641.             $iterator $iteratorAggregate->getIterator();
  642.         }
  643.         return $iterator;
  644.     }
  645.     /**
  646.      * Normalizes given directory names by removing trailing slashes.
  647.      *
  648.      * Excluding: (s)ftp:// or ssh2.(s)ftp:// wrapper
  649.      *
  650.      * @param string $dir
  651.      *
  652.      * @return string
  653.      */
  654.     private function normalizeDir($dir)
  655.     {
  656.         $dir rtrim($dir'/'.\DIRECTORY_SEPARATOR);
  657.         if (preg_match('#^(ssh2\.)?s?ftp://#'$dir)) {
  658.             $dir .= '/';
  659.         }
  660.         return $dir;
  661.     }
  662. }