Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Ant Path Matcher Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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 /.

Implementation Patterns

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.

Gotchas and Tips

  • ** 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.
  • No braces/alternation, no Spring variable patterns. Don't assume parity with Java's AntPathMatcher ({name:regex} style path variables are not part of this port). Generate multiple patterns instead.
  • Leading/trailing slashes are significant. No normalization is done for you. A trailing-slash path (foo/bar/) won't match foo/bar.
  • Case-sensitive, byte-wise. No Unicode or case-insensitivity flags. Lowercase both sides yourself if you need case-insensitive behavior.
  • 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.
  • Debugging 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.
  • Don't reach for it when you need real filesystem globbing with {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.
  • Low-traffic dependency. Six stars, last release 2023, zero dependents — read the class (it's short) before relying on it, and pin the version. It's small enough that vendoring or reimplementing the ~100 lines is a legitimate hedge if you want zero risk; wrapping it behind your own interface makes that swap painless.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums