2023-05-10 13:39:12 +08:00
< ? php
/*
* This file is part of the Symfony package .
*
* ( c ) Fabien Potencier < fabien @ symfony . com >
*
* For the full copyright and license information , please view the LICENSE
* file that was distributed with this source code .
*/
namespace Symfony\Component\Finder ;
use Symfony\Component\Finder\Comparator\DateComparator ;
use Symfony\Component\Finder\Comparator\NumberComparator ;
use Symfony\Component\Finder\Exception\DirectoryNotFoundException ;
use Symfony\Component\Finder\Iterator\CustomFilterIterator ;
use Symfony\Component\Finder\Iterator\DateRangeFilterIterator ;
use Symfony\Component\Finder\Iterator\DepthRangeFilterIterator ;
use Symfony\Component\Finder\Iterator\ExcludeDirectoryFilterIterator ;
use Symfony\Component\Finder\Iterator\FilecontentFilterIterator ;
use Symfony\Component\Finder\Iterator\FilenameFilterIterator ;
use Symfony\Component\Finder\Iterator\SizeRangeFilterIterator ;
use Symfony\Component\Finder\Iterator\SortableIterator ;
/**
* Finder allows to build rules to find files and directories .
*
* It is a thin wrapper around several specialized iterator classes .
*
* All rules may be invoked several times .
*
* All methods return the current Finder object to allow chaining :
*
* $finder = Finder :: create () -> files () -> name ( '*.php' ) -> in ( __DIR__ );
*
* @ author Fabien Potencier < fabien @ symfony . com >
*/
class Finder implements \IteratorAggregate , \Countable
{
2024-02-28 16:51:38 +08:00
const IGNORE_VCS_FILES = 1 ;
const IGNORE_DOT_FILES = 2 ;
const IGNORE_VCS_IGNORED_FILES = 4 ;
2023-05-10 13:39:12 +08:00
private $mode = 0 ;
private $names = [];
private $notNames = [];
private $exclude = [];
private $filters = [];
private $depths = [];
private $sizes = [];
private $followLinks = false ;
private $reverseSorting = false ;
private $sort = false ;
private $ignore = 0 ;
private $dirs = [];
private $dates = [];
private $iterators = [];
private $contains = [];
private $notContains = [];
private $paths = [];
private $notPaths = [];
private $ignoreUnreadableDirs = false ;
private static $vcsPatterns = [ '.svn' , '_svn' , 'CVS' , '_darcs' , '.arch-params' , '.monotone' , '.bzr' , '.git' , '.hg' ];
public function __construct ()
{
$this -> ignore = static :: IGNORE_VCS_FILES | static :: IGNORE_DOT_FILES ;
}
/**
* Creates a new Finder .
*
* @ return static
*/
public static function create ()
{
return new static ();
}
/**
* Restricts the matching to directories only .
*
* @ return $this
*/
public function directories ()
{
$this -> mode = Iterator\FileTypeFilterIterator :: ONLY_DIRECTORIES ;
return $this ;
}
/**
* Restricts the matching to files only .
*
* @ return $this
*/
public function files ()
{
$this -> mode = Iterator\FileTypeFilterIterator :: ONLY_FILES ;
return $this ;
}
/**
* Adds tests for the directory depth .
*
* Usage :
*
* $finder -> depth ( '> 1' ) // the Finder will start matching at level 1.
* $finder -> depth ( '< 3' ) // the Finder will descend at most 3 levels of directories below the starting point.
* $finder -> depth ([ '>= 1' , '< 3' ])
*
* @ param string | int | string [] | int [] $levels The depth level expression or an array of depth levels
*
* @ return $this
*
* @ see DepthRangeFilterIterator
* @ see NumberComparator
*/
public function depth ( $levels )
{
foreach (( array ) $levels as $level ) {
$this -> depths [] = new Comparator\NumberComparator ( $level );
}
return $this ;
}
/**
* Adds tests for file dates ( last modified ) .
*
* The date must be something that strtotime () is able to parse :
*
* $finder -> date ( 'since yesterday' );
* $finder -> date ( 'until 2 days ago' );
* $finder -> date ( '> now - 2 hours' );
* $finder -> date ( '>= 2005-10-15' );
* $finder -> date ([ '>= 2005-10-15' , '<= 2006-05-27' ]);
*
* @ param string | string [] $dates A date range string or an array of date ranges
*
* @ return $this
*
* @ see strtotime
* @ see DateRangeFilterIterator
* @ see DateComparator
*/
public function date ( $dates )
{
foreach (( array ) $dates as $date ) {
$this -> dates [] = new Comparator\DateComparator ( $date );
}
return $this ;
}
/**
* Adds rules that files must match .
*
* You can use patterns ( delimited with / sign ), globs or simple strings .
*
2024-02-28 16:51:38 +08:00
* $finder -> name ( '*.php' )
* $finder -> name ( '/\.php$/' ) // same as above
2023-05-10 13:39:12 +08:00
* $finder -> name ( 'test.php' )
* $finder -> name ([ 'test.py' , 'test.php' ])
*
* @ param string | string [] $patterns A pattern ( a regexp , a glob , or a string ) or an array of patterns
*
* @ return $this
*
* @ see FilenameFilterIterator
*/
public function name ( $patterns )
{
$this -> names = array_merge ( $this -> names , ( array ) $patterns );
return $this ;
}
/**
* Adds rules that files must not match .
*
* @ param string | string [] $patterns A pattern ( a regexp , a glob , or a string ) or an array of patterns
*
* @ return $this
*
* @ see FilenameFilterIterator
*/
public function notName ( $patterns )
{
$this -> notNames = array_merge ( $this -> notNames , ( array ) $patterns );
return $this ;
}
/**
* Adds tests that file contents must match .
*
* Strings or PCRE patterns can be used :
*
* $finder -> contains ( 'Lorem ipsum' )
* $finder -> contains ( '/Lorem ipsum/i' )
* $finder -> contains ([ 'dolor' , '/ipsum/i' ])
*
* @ param string | string [] $patterns A pattern ( string or regexp ) or an array of patterns
*
* @ return $this
*
* @ see FilecontentFilterIterator
*/
public function contains ( $patterns )
{
$this -> contains = array_merge ( $this -> contains , ( array ) $patterns );
return $this ;
}
/**
* Adds tests that file contents must not match .
*
* Strings or PCRE patterns can be used :
*
* $finder -> notContains ( 'Lorem ipsum' )
* $finder -> notContains ( '/Lorem ipsum/i' )
* $finder -> notContains ([ 'lorem' , '/dolor/i' ])
*
* @ param string | string [] $patterns A pattern ( string or regexp ) or an array of patterns
*
* @ return $this
*
* @ see FilecontentFilterIterator
*/
public function notContains ( $patterns )
{
$this -> notContains = array_merge ( $this -> notContains , ( array ) $patterns );
return $this ;
}
/**
* Adds rules that filenames must match .
*
* You can use patterns ( delimited with / sign ) or simple strings .
*
* $finder -> path ( 'some/special/dir' )
* $finder -> path ( '/some\/special\/dir/' ) // same as above
* $finder -> path ([ 'some dir' , 'another/dir' ])
*
* Use only / as dirname separator .
*
* @ param string | string [] $patterns A pattern ( a regexp or a string ) or an array of patterns
*
* @ return $this
*
* @ see FilenameFilterIterator
*/
public function path ( $patterns )
{
$this -> paths = array_merge ( $this -> paths , ( array ) $patterns );
return $this ;
}
/**
* Adds rules that filenames must not match .
*
* You can use patterns ( delimited with / sign ) or simple strings .
*
* $finder -> notPath ( 'some/special/dir' )
* $finder -> notPath ( '/some\/special\/dir/' ) // same as above
* $finder -> notPath ([ 'some/file.txt' , 'another/file.log' ])
*
* Use only / as dirname separator .
*
* @ param string | string [] $patterns A pattern ( a regexp or a string ) or an array of patterns
*
* @ return $this
*
* @ see FilenameFilterIterator
*/
public function notPath ( $patterns )
{
$this -> notPaths = array_merge ( $this -> notPaths , ( array ) $patterns );
return $this ;
}
/**
* Adds tests for file sizes .
*
* $finder -> size ( '> 10K' );
* $finder -> size ( '<= 1Ki' );
* $finder -> size ( 4 );
* $finder -> size ([ '> 10K' , '< 20K' ])
*
* @ param string | int | string [] | int [] $sizes A size range string or an integer or an array of size ranges
*
* @ return $this
*
* @ see SizeRangeFilterIterator
* @ see NumberComparator
*/
public function size ( $sizes )
{
foreach (( array ) $sizes as $size ) {
$this -> sizes [] = new Comparator\NumberComparator ( $size );
}
return $this ;
}
/**
* Excludes directories .
*
* Directories passed as argument must be relative to the ones defined with the `in()` method . For example :
*
* $finder -> in ( __DIR__ ) -> exclude ( 'ruby' );
*
* @ param string | array $dirs A directory path or an array of directories
*
* @ return $this
*
* @ see ExcludeDirectoryFilterIterator
*/
public function exclude ( $dirs )
{
$this -> exclude = array_merge ( $this -> exclude , ( array ) $dirs );
return $this ;
}
/**
* Excludes " hidden " directories and files ( starting with a dot ) .
*
* This option is enabled by default .
*
2024-02-28 16:51:38 +08:00
* @ param bool $ignoreDotFiles Whether to exclude " hidden " files or not
*
2023-05-10 13:39:12 +08:00
* @ return $this
*
* @ see ExcludeDirectoryFilterIterator
*/
2024-02-28 16:51:38 +08:00
public function ignoreDotFiles ( $ignoreDotFiles )
2023-05-10 13:39:12 +08:00
{
if ( $ignoreDotFiles ) {
$this -> ignore |= static :: IGNORE_DOT_FILES ;
} else {
$this -> ignore &= ~ static :: IGNORE_DOT_FILES ;
}
return $this ;
}
/**
* Forces the finder to ignore version control directories .
*
* This option is enabled by default .
*
2024-02-28 16:51:38 +08:00
* @ param bool $ignoreVCS Whether to exclude VCS files or not
*
2023-05-10 13:39:12 +08:00
* @ return $this
*
* @ see ExcludeDirectoryFilterIterator
*/
2024-02-28 16:51:38 +08:00
public function ignoreVCS ( $ignoreVCS )
2023-05-10 13:39:12 +08:00
{
if ( $ignoreVCS ) {
$this -> ignore |= static :: IGNORE_VCS_FILES ;
} else {
$this -> ignore &= ~ static :: IGNORE_VCS_FILES ;
}
return $this ;
}
/**
* Forces Finder to obey . gitignore and ignore files based on rules listed there .
*
* This option is disabled by default .
*
* @ return $this
*/
public function ignoreVCSIgnored ( bool $ignoreVCSIgnored )
{
if ( $ignoreVCSIgnored ) {
$this -> ignore |= static :: IGNORE_VCS_IGNORED_FILES ;
} else {
$this -> ignore &= ~ static :: IGNORE_VCS_IGNORED_FILES ;
}
return $this ;
}
/**
* Adds VCS patterns .
*
* @ see ignoreVCS ()
*
* @ param string | string [] $pattern VCS patterns to ignore
*/
public static function addVCSPattern ( $pattern )
{
foreach (( array ) $pattern as $p ) {
self :: $vcsPatterns [] = $p ;
}
self :: $vcsPatterns = array_unique ( self :: $vcsPatterns );
}
/**
* Sorts files and directories by an anonymous function .
*
* The anonymous function receives two \SplFileInfo instances to compare .
*
* This can be slow as all the matching files and directories must be retrieved for comparison .
*
* @ return $this
*
* @ see SortableIterator
*/
public function sort ( \Closure $closure )
{
$this -> sort = $closure ;
return $this ;
}
/**
* Sorts files and directories by name .
*
* This can be slow as all the matching files and directories must be retrieved for comparison .
*
2024-02-28 16:51:38 +08:00
* @ param bool $useNaturalSort Whether to use natural sort or not , disabled by default
*
2023-05-10 13:39:12 +08:00
* @ return $this
*
* @ see SortableIterator
*/
2024-02-28 16:51:38 +08:00
public function sortByName ( /* bool $useNaturalSort = false */ )
2023-05-10 13:39:12 +08:00
{
2024-02-28 16:51:38 +08:00
if ( \func_num_args () < 1 && __CLASS__ !== static :: class && __CLASS__ !== ( new \ReflectionMethod ( $this , __FUNCTION__ )) -> getDeclaringClass () -> getName () && ! $this instanceof \PHPUnit\Framework\MockObject\MockObject && ! $this instanceof \Prophecy\Prophecy\ProphecySubjectInterface ) {
@ trigger_error ( sprintf ( 'The "%s()" method will have a new "bool $useNaturalSort = false" argument in version 5.0, not defining it is deprecated since Symfony 4.2.' , __METHOD__ ), E_USER_DEPRECATED );
}
$useNaturalSort = 0 < \func_num_args () && func_get_arg ( 0 );
2023-05-10 13:39:12 +08:00
$this -> sort = $useNaturalSort ? Iterator\SortableIterator :: SORT_BY_NAME_NATURAL : Iterator\SortableIterator :: SORT_BY_NAME ;
return $this ;
}
/**
* Sorts files and directories by type ( directories before files ), then by name .
*
* This can be slow as all the matching files and directories must be retrieved for comparison .
*
* @ return $this
*
* @ see SortableIterator
*/
public function sortByType ()
{
$this -> sort = Iterator\SortableIterator :: SORT_BY_TYPE ;
return $this ;
}
/**
* Sorts files and directories by the last accessed time .
*
* This is the time that the file was last accessed , read or written to .
*
* This can be slow as all the matching files and directories must be retrieved for comparison .
*
* @ return $this
*
* @ see SortableIterator
*/
public function sortByAccessedTime ()
{
$this -> sort = Iterator\SortableIterator :: SORT_BY_ACCESSED_TIME ;
return $this ;
}
/**
* Reverses the sorting .
*
* @ return $this
*/
public function reverseSorting ()
{
$this -> reverseSorting = true ;
return $this ;
}
/**
* Sorts files and directories by the last inode changed time .
*
* This is the time that the inode information was last modified ( permissions , owner , group or other metadata ) .
*
* On Windows , since inode is not available , changed time is actually the file creation time .
*
* This can be slow as all the matching files and directories must be retrieved for comparison .
*
* @ return $this
*
* @ see SortableIterator
*/
public function sortByChangedTime ()
{
$this -> sort = Iterator\SortableIterator :: SORT_BY_CHANGED_TIME ;
return $this ;
}
/**
* Sorts files and directories by the last modified time .
*
* This is the last time the actual contents of the file were last modified .
*
* This can be slow as all the matching files and directories must be retrieved for comparison .
*
* @ return $this
*
* @ see SortableIterator
*/
public function sortByModifiedTime ()
{
$this -> sort = Iterator\SortableIterator :: SORT_BY_MODIFIED_TIME ;
return $this ;
}
/**
* Filters the iterator with an anonymous function .
*
* The anonymous function receives a \SplFileInfo and must return false
* to remove files .
*
* @ return $this
*
* @ see CustomFilterIterator
*/
public function filter ( \Closure $closure )
{
$this -> filters [] = $closure ;
return $this ;
}
/**
* Forces the following of symlinks .
*
* @ return $this
*/
public function followLinks ()
{
$this -> followLinks = true ;
return $this ;
}
/**
* Tells finder to ignore unreadable directories .
*
* By default , scanning unreadable directories content throws an AccessDeniedException .
*
2024-02-28 16:51:38 +08:00
* @ param bool $ignore
*
2023-05-10 13:39:12 +08:00
* @ return $this
*/
2024-02-28 16:51:38 +08:00
public function ignoreUnreadableDirs ( $ignore = true )
2023-05-10 13:39:12 +08:00
{
2024-02-28 16:51:38 +08:00
$this -> ignoreUnreadableDirs = ( bool ) $ignore ;
2023-05-10 13:39:12 +08:00
return $this ;
}
/**
* Searches files and directories which match defined rules .
*
* @ param string | string [] $dirs A directory path or an array of directories
*
* @ return $this
*
* @ throws DirectoryNotFoundException if one of the directories does not exist
*/
public function in ( $dirs )
{
$resolvedDirs = [];
foreach (( array ) $dirs as $dir ) {
if ( is_dir ( $dir )) {
2024-02-28 16:51:38 +08:00
$resolvedDirs [] = $this -> normalizeDir ( $dir );
} elseif ( $glob = glob ( $dir , ( \defined ( 'GLOB_BRACE' ) ? GLOB_BRACE : 0 ) | GLOB_ONLYDIR | GLOB_NOSORT )) {
2023-05-10 13:39:12 +08:00
sort ( $glob );
2024-02-28 16:51:38 +08:00
$resolvedDirs = array_merge ( $resolvedDirs , array_map ([ $this , 'normalizeDir' ], $glob ));
2023-05-10 13:39:12 +08:00
} else {
throw new DirectoryNotFoundException ( sprintf ( 'The "%s" directory does not exist.' , $dir ));
}
}
2024-02-28 16:51:38 +08:00
$this -> dirs = array_merge ( $this -> dirs , $resolvedDirs );
2023-05-10 13:39:12 +08:00
return $this ;
}
/**
* Returns an Iterator for the current Finder configuration .
*
* This method implements the IteratorAggregate interface .
*
2024-02-28 16:51:38 +08:00
* @ return \Iterator | SplFileInfo [] An iterator
2023-05-10 13:39:12 +08:00
*
* @ throws \LogicException if the in () method has not been called
*/
public function getIterator ()
{
if ( 0 === \count ( $this -> dirs ) && 0 === \count ( $this -> iterators )) {
throw new \LogicException ( 'You must call one of in() or append() methods before iterating over a Finder.' );
}
if ( 1 === \count ( $this -> dirs ) && 0 === \count ( $this -> iterators )) {
2024-02-28 16:51:38 +08:00
return $this -> searchInDirectory ( $this -> dirs [ 0 ]);
2023-05-10 13:39:12 +08:00
}
$iterator = new \AppendIterator ();
foreach ( $this -> dirs as $dir ) {
2024-02-28 16:51:38 +08:00
$iterator -> append ( $this -> searchInDirectory ( $dir ));
2023-05-10 13:39:12 +08:00
}
foreach ( $this -> iterators as $it ) {
$iterator -> append ( $it );
}
return $iterator ;
}
/**
* Appends an existing set of files / directories to the finder .
*
* The set can be another Finder , an Iterator , an IteratorAggregate , or even a plain array .
*
2024-02-28 16:51:38 +08:00
* @ param iterable $iterator
*
2023-05-10 13:39:12 +08:00
* @ return $this
*
* @ throws \InvalidArgumentException when the given argument is not iterable
*/
2024-02-28 16:51:38 +08:00
public function append ( $iterator )
2023-05-10 13:39:12 +08:00
{
if ( $iterator instanceof \IteratorAggregate ) {
$this -> iterators [] = $iterator -> getIterator ();
} elseif ( $iterator instanceof \Iterator ) {
$this -> iterators [] = $iterator ;
2024-02-28 16:51:38 +08:00
} elseif ( $iterator instanceof \Traversable || \is_array ( $iterator )) {
2023-05-10 13:39:12 +08:00
$it = new \ArrayIterator ();
foreach ( $iterator as $file ) {
2024-02-28 16:51:38 +08:00
$it -> append ( $file instanceof \SplFileInfo ? $file : new \SplFileInfo ( $file ));
2023-05-10 13:39:12 +08:00
}
$this -> iterators [] = $it ;
} else {
throw new \InvalidArgumentException ( 'Finder::append() method wrong argument type.' );
}
return $this ;
}
/**
2024-02-28 16:51:38 +08:00
* Check if the any results were found .
2023-05-10 13:39:12 +08:00
*
* @ return bool
*/
public function hasResults ()
{
foreach ( $this -> getIterator () as $_ ) {
return true ;
}
return false ;
}
/**
* Counts all the results collected by the iterators .
*
* @ return int
*/
public function count ()
{
return iterator_count ( $this -> getIterator ());
}
private function searchInDirectory ( string $dir ) : \Iterator
{
$exclude = $this -> exclude ;
$notPaths = $this -> notPaths ;
if ( static :: IGNORE_VCS_FILES === ( static :: IGNORE_VCS_FILES & $this -> ignore )) {
$exclude = array_merge ( $exclude , self :: $vcsPatterns );
}
if ( static :: IGNORE_DOT_FILES === ( static :: IGNORE_DOT_FILES & $this -> ignore )) {
$notPaths [] = '#(^|/)\..+(/|$)#' ;
}
2024-02-28 16:51:38 +08:00
if ( static :: IGNORE_VCS_IGNORED_FILES === ( static :: IGNORE_VCS_IGNORED_FILES & $this -> ignore )) {
$gitignoreFilePath = sprintf ( '%s/.gitignore' , $dir );
if ( ! is_readable ( $gitignoreFilePath )) {
throw new \RuntimeException ( sprintf ( 'The "ignoreVCSIgnored" option cannot be used by the Finder as the "%s" file is not readable.' , $gitignoreFilePath ));
}
$notPaths = array_merge ( $notPaths , [ Gitignore :: toRegex ( file_get_contents ( $gitignoreFilePath ))]);
}
2023-05-10 13:39:12 +08:00
$minDepth = 0 ;
2024-02-28 16:51:38 +08:00
$maxDepth = PHP_INT_MAX ;
2023-05-10 13:39:12 +08:00
foreach ( $this -> depths as $comparator ) {
switch ( $comparator -> getOperator ()) {
case '>' :
$minDepth = $comparator -> getTarget () + 1 ;
break ;
case '>=' :
$minDepth = $comparator -> getTarget ();
break ;
case '<' :
$maxDepth = $comparator -> getTarget () - 1 ;
break ;
case '<=' :
$maxDepth = $comparator -> getTarget ();
break ;
default :
$minDepth = $maxDepth = $comparator -> getTarget ();
}
}
$flags = \RecursiveDirectoryIterator :: SKIP_DOTS ;
if ( $this -> followLinks ) {
$flags |= \RecursiveDirectoryIterator :: FOLLOW_SYMLINKS ;
}
$iterator = new Iterator\RecursiveDirectoryIterator ( $dir , $flags , $this -> ignoreUnreadableDirs );
if ( $exclude ) {
$iterator = new Iterator\ExcludeDirectoryFilterIterator ( $iterator , $exclude );
}
$iterator = new \RecursiveIteratorIterator ( $iterator , \RecursiveIteratorIterator :: SELF_FIRST );
2024-02-28 16:51:38 +08:00
if ( $minDepth > 0 || $maxDepth < PHP_INT_MAX ) {
2023-05-10 13:39:12 +08:00
$iterator = new Iterator\DepthRangeFilterIterator ( $iterator , $minDepth , $maxDepth );
}
if ( $this -> mode ) {
$iterator = new Iterator\FileTypeFilterIterator ( $iterator , $this -> mode );
}
if ( $this -> names || $this -> notNames ) {
$iterator = new Iterator\FilenameFilterIterator ( $iterator , $this -> names , $this -> notNames );
}
if ( $this -> contains || $this -> notContains ) {
$iterator = new Iterator\FilecontentFilterIterator ( $iterator , $this -> contains , $this -> notContains );
}
if ( $this -> sizes ) {
$iterator = new Iterator\SizeRangeFilterIterator ( $iterator , $this -> sizes );
}
if ( $this -> dates ) {
$iterator = new Iterator\DateRangeFilterIterator ( $iterator , $this -> dates );
}
if ( $this -> filters ) {
$iterator = new Iterator\CustomFilterIterator ( $iterator , $this -> filters );
}
if ( $this -> paths || $notPaths ) {
$iterator = new Iterator\PathFilterIterator ( $iterator , $this -> paths , $notPaths );
}
2024-02-28 16:51:38 +08:00
if ( $this -> sort || $this -> reverseSorting ) {
$iteratorAggregate = new Iterator\SortableIterator ( $iterator , $this -> sort , $this -> reverseSorting );
$iterator = $iteratorAggregate -> getIterator ();
2023-05-10 13:39:12 +08:00
}
return $iterator ;
}
/**
* Normalizes given directory names by removing trailing slashes .
*
* Excluding : ( s ) ftp :// or ssh2 . ( s ) ftp :// wrapper
*/
private function normalizeDir ( string $dir ) : string
{
if ( '/' === $dir ) {
return $dir ;
}
$dir = rtrim ( $dir , '/' . \DIRECTORY_SEPARATOR );
if ( preg_match ( '#^(ssh2\.)?s?ftp://#' , $dir )) {
$dir .= '/' ;
}
return $dir ;
}
}