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

Flyfinder Laravel Package

phpdocumentor/flyfinder

FlyFinder is a utility for Flysystem that finds files by criteria such as hidden status, extension, or path, with wildcard path support. It returns files only, not directories, and integrates easily via Composer.

View on GitHub
Deep Wiki
Context7

Getting Started

FlyFinder is not a fuzzy-finder or CLI UI widget — it is a query layer on top of Flysystem: you hand it a League\Flysystem\Filesystem and a specification, and it walks the tree returning files only. Day to day you reach for it when you would otherwise write a recursive listContents() loop with a pile of if statements.

composer require phpdocumentor/flyfinder:^2.0.1

Pin to ^2.0.1: 2.0.0 had a bug where Glob wrote into Flysystem 3 attributes, and 2.0.0 is also the release where the library stopped being a Flysystem plugin (Flysystem 3 removed plugin support) and became a standalone Finder. Verify the PHP floor and the exact Flysystem constraint in the now-public composer.json before committing; 2.x targets Flysystem 3, which means Laravel 9+.

Minimal first use:

use League\Flysystem\Filesystem;
use League\Flysystem\Local\LocalFilesystemAdapter;
use Flyfinder\Finder;
use Flyfinder\Path;
use Flyfinder\Specification\HasExtension;
use Flyfinder\Specification\InPath;
use Flyfinder\Specification\IsHidden;

$filesystem = new Filesystem(new LocalFilesystemAdapter(base_path('app')));
$finder = new Finder($filesystem);

$spec = (new InPath(new Path('Models')))
    ->andSpecification(new HasExtension(['php']))
    ->andSpecification((new IsHidden())->notSpecification());

foreach ($finder->find($spec) as $file) {
    $relativePath = $file->path(); // relative to the Flysystem root
}

Where to look first: the README for the four built-in specifications (IsHidden, HasExtension, InPath, Glob), then the examples/ folder for copy-paste scripts. First real use case: an Artisan command that collects "all non-hidden PHP files under app/" — the exact thing phpDocumentor itself uses the library for.

If you are on Laravel 8 or older (Flysystem 1/2), the API is different: you register new Finder() via $filesystem->addPlugin(...) and call $filesystem->find($spec). Don't mix the two mental models.

Implementation Patterns

Bridging a Laravel disk. Illuminate\Filesystem\FilesystemAdapter exposes the underlying Flysystem instance, so you can query a configured disk instead of hand-building an adapter:

$driver = Storage::disk('local')->getDriver(); // League\Flysystem\Filesystem
$finder = new Finder($driver);
$files = $finder->find($spec);

Keep this in a small service class (SourceFileCollector, AssetScanner) rather than calling it from controllers. The package has ~30 stars and zero dependents; an abstraction layer costs you twenty lines and buys you a fork-and-replace path later.

Build specifications, don't branch in loops. The composed specification is the reusable artifact. Map CLI options onto it once:

$spec = new InPath(new Path($options['path'] ?? '.'));

if ($extensions = $options['ext'] ?? null) {
    $spec = $spec->andSpecification(new HasExtension(explode(',', $extensions)));
}

if (! $options['hidden']) {
    $spec = $spec->andSpecification((new IsHidden())->notSpecification());
}

Then a single $finder->find($spec) replaces several nested loops and keeps the filtering logic testable in isolation.

Order your andSpecification() chain deliberately. Release 1.0.0 added "detect earlier that a certain direction doesn't have to be accessed" — the traversal prunes based on what it can evaluate on directories. Put InPath first so whole subtrees are skipped before extension checks run.

Use Glob for filename patterns, HasExtension for suffixes. HasExtension(['php', 'phtml']) is a suffix test on the path; Glob is a pattern match (*.blade.php, **/migrations/*.php). Reach for Glob when a suffix test would over-match (e.g. Test.php vs TestData.php) and re-run your smoke test afterwards, since this is the code path that was patched in 2.0.1.

Pipeline pattern in commands. Don't materialize ten thousand paths if you're going to process them: iterate the result, act per file, and report progress. This keeps memory flat and lets a --dry-run flag short-circuit before any writes.

$count = 0;
foreach ($finder->find($spec) as $file) {
    $this->line($file->path());
    if ($this->option('dry-run')) { continue; }
    $this->process($file->path());
    $count++;
}

Test against an in-memory adapter. No filesystem fixtures needed — build a League\Flysystem\InMemory\InMemoryFilesystemAdapter, seed a handful of files (including a .git directory and a foo.TXT for the case-sensitivity test), and assert the returned path list. Storage::fake() gives you the same thing inside Laravel tests via Storage::fake('local')->getDriver(). This is the cheapest way to pin down Glob and IsHidden semantics before an upgrade.

Custom specifications. Implement the package's specification interface (isSatisfiedBy()-style predicate over a file entry) when you need size, mtime, or regex rules. Implementing it once and composing it with andSpecification()/orSpecification() is cleaner than post-filtering the result set, because your predicate participates in the traversal pruning.

Gotchas and Tips

  • The name lies. Despite "plugin" in the description, 2.x is not registered on the filesystem. $filesystem->find(...) will not exist. Instantiate new Finder($filesystem) and call find() on it.
  • The constructor wants League\Flysystem\Filesystem, not FilesystemOperator. Read-only Flysystem instances (e.g. an S3 disk wrapped in a reader) can produce a type error. If you only have a FilesystemReader, you can't hand it straight in — check the signature in the version you install.
  • Files only, never directories. The result will not contain the empty directories in your tree, and you cannot use it to enumerate folder structure. If you need directory listings, go back to listContents().
  • Paths are relative to the Flysystem root, not the host. new Path('Models') means <disk-root>/Models, not /var/www/app/Models. Passing Storage::path(...) or base_path() into InPath is the most common first-day bug. Path normalizes ././, so new Path('.') is the disk root.
  • Leading slashes. new Path('/Models') and new Path('Models') may not behave identically; be consistent and lean on the normalized form. Wildcards are allowed in InPath, which is useful for shallow scoping but easy to over-broaden.
  • IsHidden is name-based. It matches files beginning with a dot and files inside dot-directories, not OS/ACL "hidden" attributes. That's fine on Linux/macOS, and it's why this works the same against S3 and local adapters — but it means Windows hidden-attribute files will not match, and .-prefixed content you do want (e.g. .env.example) needs the notSpecification() removed deliberately.
  • Extension matching is case-sensitive as written. HasExtension(['jpg']) will miss IMG_0001.JPG. If your file population is mixed-case, either pass both casings or implement a custom case-insensitive specification.
  • Performance scales with tree size, not with result size. Traversal issues a listContents() per directory. On local disks that's fine; on networked or object-store disks it's a burst of API calls. Narrow with InPath first, avoid running it on every request, and benchmark against a >10k-entry tree (and a remote disk) before putting it on a hot path.
  • Narrow scope of the 2.0.1 fix. The Glob attribute-write bug is a data-integrity issue in code that could mutate Flysystem metadata. If you have 2.0.0 anywhere, bump it; if your fork or lock file pins 2.0.0, treat any Glob output as suspect.
  • No service provider, no facade, no config file. Composer autoloading is the whole integration. Don't go looking for config/flyfinder.php — there isn't one, and there's no auto-discovery to disable.
  • Debug specifications by shrinking the disk. When a spec "matches nothing," swap in an in-memory adapter seeded with three known files rather than debugging against a real multi-gigabyte disk. If it matches there, the problem is almost always Path scoping or hidden-file filtering.
  • Iterate, don't assume an array. The return value is a result collection object. If you genuinely need an array, use iterator_to_array(...) — but check whether your installed version also exposes a getFiles() style accessor, because that method name has moved between releases and copy-pasted snippets from older blog posts will not compile.
  • Small-project risk is real. Single-maintainer-adjacent project, community-only support, no security disclosure policy. For anything on a critical path: wrap the Finder behind your own interface, pin ^2.0.1, and keep a test that asserts a known set of paths from a seeded fake disk so an upstream regression is caught by your CI rather than by a user.
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.
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel