phpunit/php-file-iterator
php-file-iterator provides a simple, dependency-free way to filter and iterate over files by suffix, prefix, or custom filters, powering PHPUnit's test file discovery. Install it via Composer to find and list files matching patterns in any PHP project.
You almost certainly already have this package. It arrives transitively through phpunit/phpunit (PHPUnit's test discovery uses it to expand <directory> / <exclude> nodes in phpunit.xml), so the first step is not composer require but confirming what you've got:
composer show phpunit/php-file-iterator # version + dependents
composer why phpunit/php-file-iterator # who actually pulls it in
Look at vendor/phpunit/php-file-iterator/src/ first — the public surface is tiny (Factory, Iterator, and a couple of path-filter classes), and the constructor signatures have shifted across 5.x → 6.x → 7.x, so the source is more reliable than any blog post (including the snippet below).
Only add it as a direct dev dependency if your own code calls it. If you're just writing application features, use Symfony\Component\Finder\Finder — it ships with Laravel already and is the idiomatic choice. The realistic first use case for a Laravel dev is tooling: enumerating test files the same way PHPUnit does, so your custom script and your phpunit.xml agree on what "the test suite" means.
use PHPUnit\File\Iterator\Factory;
$files = (new Factory)->getFileIterator(
[base_path('tests')], // paths to traverse
['php'], // suffixes
[], // prefixes
[base_path('tests/Fixtures')], // excluded paths
);
foreach ($files as $file) {
echo $file->getPathname(), PHP_EOL;
}
Verify the exact method name against your installed version before copying — getFileIterator() vs getIterator() has changed, and the class moved to PHPUnit\File\Iterator\Factory.
Understand your phpunit.xml before reaching for the class. The <directory suffix=".php">, <exclude>, <prefix>, and glob patterns in your testsuite config are compiled by PHPUnit into exactly these constructor arguments. When a test file mysteriously isn't running, reading the iterator's semantics is faster than guessing:
<testsuite name="Unit">
<directory suffix="Test.php">tests/Unit</directory>
<exclude>tests/Unit/Fixtures</exclude>
</testsuite>
Globstar for multi-directory suites. Since 5.1.0, ** is supported — useful when you want tests/**/*Test.php without listing every nested folder. Reach for Factory (not Iterator) whenever you're passing patterns rather than plain paths.
CI test sharding. This is the most defensible day-to-day use in a Laravel app: splitting the test suite across parallel CI jobs using the same file list PHPUnit would build.
$files = iterator_to_array(
(new Factory)->getFileIterator([base_path('tests')], ['php'])
);
$shard = array_values(array_filter(
$files,
fn ($file) => crc32($file->getPathname()) % $total === $index
));
Custom Artisan commands. For "lint all PHP files" or "scan for a pattern" commands, the pattern is: use php-file-iterator only if you specifically need PHPUnit-identical filtering; otherwise Finder::files()->name('*.php')->in(...). Wrapping the iterator in a small service class keeps the dependency swappable if you later migrate to Finder.
Don't chain on the instance in a loop. The iterator/filter wrappers are effectively immutable — each add*()/filter call returns a new instance, so building the filter once outside a loop matters for anything non-trivial.
Extending. For content-based or custom rules, subclass FilterIterator (SPL) directly and implement accept(). Don't try to bolt content inspection onto the suffix/prefix filters.
composer require --dev phpunit/php-file-iterator:^5 while PHPUnit wants ^6, you get a conflict. Let PHPUnit choose the version unless you have a hard reason not to./ was recognized. If you're on < 7.0.1 with Windows CI, expect inconsistency between local and CI runs.tests/Fixtures can silently swallow tests/FixturesExtra.is_readable() or expect partial listings in containerized CI where permissions are odd.src/**/*.php works; **.php does not. Also, 7.0.1 fixed **-leading patterns resolving incorrectly — test your pattern against a real tree rather than trusting the docs.realpath()-ing every file for prefix/suffix filtering. If you depend on absolute, symlink-resolved paths, call realpath() yourself — don't assume getPathname() is normalized, especially on macOS with /var → /private/var.FilterIterator; nothing is scanned until you iterate, and exceptions surface mid-loop. Convert with iterator_to_array() early if you need to count or sort.tests/, tools/, or CI scripts, and prefer Symfony Finder (already a Laravel dependency) for anything runtime-facing — that also avoids a transitive-dependency upgrade breaking your feature because PHPUnit bumped a major.bin/phpunit --list-tests (or --testsuite) shows the resolved list directly, which is the same traversal output with none of the setup code.How can I help you explore Laravel packages today?