dflydev/ant-path-matcher
PHP implementation of Ant-style path matching. Compile glob-like patterns using **, * and ? wildcards into matchers, then test paths against them. Useful for routing, file filtering, and access rules where simple prefix checks fall short.
Install with Composer — there are no framework bindings, service providers, or config files to publish:
composer require dflydev/ant-path-matcher
Look first at Dflydev\Util\AntPathMatcher\AntPathMatcher; it is a small class (a PHP port of Spring's AntPathMatcher) exposing essentially two methods that matter day to day:
use Dflydev\Util\AntPathMatcher\AntPathMatcher;
$matcher = new AntPathMatcher();
$matcher->match('src/**/*.php', 'src/Controller/HomeController.php'); // true
$matcher->isPattern('src/**/*.php'); // true
$matcher->isPattern('src/Controller/HomeController.php'); // false
The first real use case is anything where a user or config supplies a path pattern and you need to test paths against it: an ignore/exclude list in a CLI tool, a set of "watched" globs in a file indexer, route-like or ACL-style rules over arbitrary slash-delimited strings (URL paths, S3 keys, queue names). It is not a filesystem tool — it happily matches strings that never existed on disk, which is exactly why it beats glob()/fnmatch() here: those require real files or shell semantics, this doesn't.
Supported syntax is the Ant subset: ? = exactly one character (not /), * = zero or more characters within a single path segment, ** = zero or more path segments. Path separator is always /.
Rule lists (include/exclude). The most common shape is an array of patterns compiled once and evaluated per path. Keep the matcher instance around; constructing it is cheap but there's no reason to redo it per call.
final class PathRules
{
/** @var string[] */
private array $patterns = [];
public function __construct(private AntPathMatcher $matcher, array $patterns)
{
$this->patterns = $patterns;
}
public function matchesAny(string $path): bool
{
foreach ($this->patterns as $pattern) {
if ($this->matcher->match($pattern, $path)) {
return true;
}
}
return false;
}
}
Exclusion is just negation of inclusion — the package has no !pattern support — so !$rules->matchesAny($path) is your exclude check.
Separating literals from patterns. Use isPattern() before you decide how to handle an entry. A literal path can be compared with === (faster, and avoids subtle normalization issues); only route actual patterns through match(). This is also how you validate user input: reject entries that look like patterns when only literals are allowed.
Normalizing before matching. Convert backslashes to /, strip a leading ./, and decide once whether your paths and patterns carry a leading /. Since matching is plain string comparison with no normalization, match('foo/*', '/foo/bar') and match('/foo/*', 'foo/bar') both fail. Pick a canonical form at the boundary (the moment the path enters your code) and never let raw input reach match().
Wrapping for caching/memoization. There is no internal pattern cache. If you match the same pattern against thousands of paths, memoize in your own wrapper keyed by pattern . "\0" . path, or compile your own regex once per pattern and reuse it. Wrapping also gives you a seam to swap in a different matcher later without touching call sites — implement PathMatcherInterface from the same package if you want a drop-in replacement.
Integration points. Wire it into a Laravel command or config-driven service rather than spreading new AntPathMatcher() around: bind a singleton in a service provider, read the pattern list from config/..., and inject the wrapper. Typical fits: php artisan file-scanning commands, a files.exclude config block, asset/route middleware that should skip certain paths, or a custom filesystem adapter filtering S3 keys.
** must be its own segment. src/**.php is not "everything under src"; write src/**/*.php. Also remember ** matches zero segments, so src/**/Foo.php matches src/Foo.php.* never crosses /. match('src/*.php', 'src/A/B.php') is false. This trips people up constantly after using fnmatch.AntPathMatcher ({name:regex} style path variables are not part of this port). Generate multiple patterns instead.foo/bar/) won't match foo/bar.isPattern() is a heuristic, not a parse. A literal path containing * or ? (rare in filesystems, common in URLs/keys) will be reported as a pattern — sanitize or escape at the boundary if that matters.match() is opaque. When a match unexpectedly fails, reduce it: test the smallest pattern that should match, check segment counts, then check for the leading-slash and backslash issues above. There's no "explain why" API.{a,b} alternation or recursive directory walking — Symfony Finder's path()/name() or glob() is the better tool there. This package is for matching arbitrary strings against Ant patterns.How can I help you explore Laravel packages today?