
Two days ago, a new component for symfony 2 called
Finder appeared on github. And today on
Fabien’s twitter I saw a link to a new post on
his blog about this component. Well, let's understand. Under the cut translation of the post
Find your Files . So, let's begin.
Best practices for finding files using PHP have evolved a lot in recent years. Returning in 2004, one of the first things I did in PHP was porting the File :: Find :: Rule Perl module in PHP. File :: Find :: Rule is a great way to describe files and directories you want to work with. I used the PHP built-in functions opendir, readdir, and closedir, and it did its job quite well. The PHP class was named sfFinder, and it can still be found in symfony versions. Even if a class is attached to symfony, I know that some people use it for tasks of almost all types that are not necessarily related to symfony.
But the code began to show its age; firstly, because I learned a lot about PHP since that time, and also because now there are better options. There are
iterators ! PHP 5 comes with a whole bunch of classes for iterations that simplify all types of iterations and iterations. You can iterate through the standard foreach operator, a very powerful PHP construct.
PHP iterators
So, how to get all files and directories recursively using PHP iterators? Frankly, I do not know. Well, I more or less know which classes to use and how to build them, but in order not to bother, I always use an existing block of code to do this correctly. Here is the code:
// ... (symlink)
$flags = \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS;
//
$iterator = new \RecursiveDirectoryIterator($dir, $flags);
//
$iterator = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::SELF_FIRST);
//
foreach ($iterator as $file)
{
// - $file ( \SplFileInfo)
}
Note: Notice the freaky \ character in front of each built-in class? This is the path to accessing the built-in PHP classes when you use them in the context of the PHP 5.3 namespace.')
As you can see, nothing complicated. You just have to know which iterator to use, which flags you can use, and how to use them together. That is, the primary entry barrier is learning. There are many presentations and tutorials about iterators on the Internet, but the official documentation on php.net probably lacks a few good examples.
Another "problem" is that everything is very object oriented. And as soon as you want to filter out an iterator, you need to create your own classes, which in most cases do not look practical. This is because PHP iterators are very powerful and were written to be general purpose iterators.
What is filtering? Let's, for example, I want to exclude all files ending in .rb from the iterator. I can create a simple \ FilterIterator for this:
class ExcludeRubyFilesFilterIterator extends \FilterIterator
{
public function accept() {
$fileinfo = $ this ->getInnerIterator()->current();
if (preg_match( '/\.rb$/' , $fileinfo)) {
return false ;
}
return true ;
}
}
The filtering iterator can be used with the previous code, for example by including:
$iterator = new ExcludeRubyFilesFilterIterator($iterator);
It is quite simple. But when I need to find files or directories, I always need filters of one specialized type, like excluding files of version control systems (such as .svn and .git directories), filtering files by name or size.
Finder component in symfony
Instead of writing the same iterators again and again, I arranged them as a symfony component: as a Finder component.
The Finder component in symfony is equipped with many specialized iterator classes for locating files and directories. It also adds a wrap for ease of everyday use.
Like each symfony component, at the beginning you need to load your script with any class loader that can load classes following PHP 5.3 compatibility standards, such as the Symfony UniversalClassLoader class:
require_once '/path/to/src/Symfony/Foundation/UniversalClassLoader.php' ;
use Symfony\Foundation\UniversalClassLoader;
$classLoader = new UniversalClassLoader();
$classLoader->registerNamespace( 'Symfony' , '/path/to/src' );
$classLoader->register();
Now, let's see how to use the Finder class, the main component class:
use Symfony\Components\Finder\Finder;
$finder = new Finder();
$iterator = $finder->files()-> in (__DIR__);
foreach ($iterator as $file)
{
print $file->getRealpath(). "\n" ;
}
The code above recursively prints the names of all files in the current directory. Notice that the Finder class uses the
fluent interface (when method calls can be made as a string), which means that all methods return an instance of the Finder class. The only exception is the in () method, which builds and returns an iterator for this category, or an array of directories:
$iterator = $finder->files()-> in (array( '/path1' , '/path2' ));
Note: you can convert an iterator to an array using the iterator_to_array () method, and get the number of elements using iterator_count ().If you want to restrict an iterator to return only PHP files in the current directory, use the name () and maxDepth () methods:
$iterator = $finder
->files()
->name( '*.php' )
->maxDepth(0)
-> in (__DIR__);
The name () method supports globs, strings, or regexes:
$finder
->files()
->name( '/\.php$/' );
There are also methods to exclude files by name or to exclude entire content directories from matches:
$finder
->files()
->name( 'test.*' )
->notName( '*.rb' )
->exclude( 'ruby' );
The result will contain files with the name test of any extension, but only if they do not end in .rb (excluded test.rb), and the iterator will not match the files in ruby directories (ruby / foo / test.php for example, does not match) .
If you want to follow links, use the followLinks () method:
$finder
->files()
->followLinks();
You can also limit files by size:
$finder
->files()
->name( '/\.php$/' )
->size( '< 1.5K' );
Most methods are cumulative. That is, if you want to get all files in PHP and Python with sizes between 1 and 2 K, you can use similar code:
$finder
->files()
->name( '*.php' )
->name( '*.py/' )
->size( '>= 1K' )
->size( '<= 2K' );
Note: by default, the iterator ignores popular version control files. This can be changed using the ignoreVCS () method.Since the in () method returns an instance of \ Iterator, you can wrap it with your specialized iterator. But instead of creating a class, you can also use the filter () method:
$filter = function (\SplFileInfo $fileinfo)
{
if (strlen($fileinfo) > 10) {
return false ;
}
};
$finder
->files()
->name( '*.php' )
->filter($filter);
This example excludes all files with a file length of more than 10 characters.
If you want to sort the result by name, use the sortByName () method:
$finder
->files()
->name( '*.php' )
->sortByName();
Notice that the sort * methods require the passage of all matched elements in order to do their work. For large iterators, this can be quite slow.Behind the curtain, the Finder class uses specialized iterator classes:
- Chainiterator
- CustomFilterIterator
- DateRangeFilterIterator (coming soon)
- ExcludeDirectoryFilterIterator
- FileTypeFilterIterator
- FilenameFilterIterator
- IgnoreVcsFilterIterator
- LimitDepthFilterIterator
- SizeRangeFilterIterator
- SortableIterator
Take a look at the
code to learn more about these iterators and how they work.