singlequote/laravel-filemanager
singlequote/laravel-filemanager provides a web-based file manager for Laravel applications, letting users browse, upload, and organize files and folders. It is suited to admin panels and editor integrations that need straightforward media and asset management with a simple interface for common file...
Minimal steps
composer require singlequote/laravel-filemanager:2.0.2
php artisan vendor:publish --provider="Singlequote\FileManager\FileManagerServiceProvider"
php artisan storage:link
php artisan migrate # only if the package ships tables — check the published migrations
Publish tags typically include config, views, assets, and possibly lang. Run php artisan vendor:publish with no args first and read the prompt — do not blindly publish everything.
Where to look first, in this order
config/filemanager.php — this is the entire contract of the package. Disk names, allowed extensions, max upload size, base directory, route prefix, and middleware all live here. Read it end to end before touching anything else.routes/web.php in the package (or php artisan route:list --path=filemanager) — confirm the URI prefix and, critically, whether the package ships its own auth middleware or none at all.resources/views/vendor/filemanager/ after publishing — the Blade UI is intentionally thin, so overriding layouts is cheap.First use case
The realistic day-one use is not "give users a file manager." It is "replace the hand-rolled upload form in an admin panel with a browse-and-pick modal." Concretely: an authenticated route group renders the file manager in an iframe or a modal, the user picks a file, the path is written into a hidden input on the parent form, and your own controller persists that relative path to a media or attachments table.
Route::middleware(['auth', 'can:manage-files'])
->prefix('admin/filemanager')
->group(function () {
// package routes get re-registered here, or set the prefix in config
});
Scope the spike: one authenticated admin screen, one local disk, upload + rename + delete. If the package does not install cleanly on your Laravel version in that window, stop — it is archived, and you are now maintaining it.
Never mount the package routes naked. The single most common integration is also the single most common mistake: letting the package register its own routes with its own (or no) middleware. Set the middleware and prefix in config/filemanager.php explicitly:
'middleware' => ['web', 'auth', 'can:access-filemanager'],
'route_prefix' => 'admin/filemanager',
Then define the access-filemanager gate. The package is a filesystem browser — treat it as an admin surface, not a user-facing feature.
Disk-per-context, not one global disk. Point the package at a dedicated uploads disk rather than your default disk, and derive public URLs in your own code from that disk's config. This keeps the package from being able to see application files it has no business listing:
// config/filesystems.php
'uploads' => [
'driver' => 'local',
'root' => storage_path('app/uploads'),
'url' => env('APP_URL').'/uploads',
'visibility' => 'public',
],
For S3, expect the package to use Storage::disk($name)->url(), which returns a temporary URL. Do not persist what the UI hands back — persist the relative key and generate URLs on read.
Store relative paths, resolve on render. The package returns a path relative to the disk root. Persist exactly that string. Absolute URLs go stale the moment you change bucket, domain, or CDN.
// migration: attachments.path — string, relative to the uploads disk
// accessor
public function getUrlAttribute(): string
{
return Storage::disk('uploads')->url($this->path);
}
Picker-modal pattern. The file manager UI is Blade-rendered and self-contained, so the loosest coupling is an iframe plus postMessage:
window.addEventListener('message', (e) => {
if (e.origin !== window.location.origin) return;
if (e.data?.type !== 'filemanager:selected') return;
document.querySelector('#attachment_path').value = e.data.path;
document.querySelector('#picker').close();
});
You will need to add that postMessage call yourself via a published view override — the package predates any picker-mode convention, so do not expect one.
Tenant / per-user scoping. The package has no concept of ownership. If multiple users share the file manager, enforce scoping by giving each tenant its own disk root (a per-tenant root path or an S3 prefix) rather than trying to filter the package's directory listing after the fact. Filtering after the fact is where directory-listing leaks happen.
Wrap, don't call. Do not have your application controllers hit the package's HTTP routes internally. Write your own FileStorage service that uses Storage::disk('uploads') directly for anything programmatic (thumbnails, imports, cleanup), and keep the package strictly as the human-facing UI. This means a future rip-out is a route + view deletion, not a refactor.
Deploy checklist, every deploy:
php artisan config:cache
php artisan route:cache # verify this works — closure routes in the package will break it
If route:cache fails, that is a hard signal the package is not cleanly written; you will have to exclude it or fork it.
It is archived. Treat the version pin as sacred. composer require without a constraint will resolve to 2.0.2 today and to whatever a future maintainer pushes tomorrow — except there will be no future push, so the real risk is a transitive dependency moving underneath it. Pin singlequote/laravel-filemanager:2.0.2 exactly, commit the lock diff, and read that diff. The lock diff is the actual changelog.
"Supports Laravel 8" in a patch release almost certainly means a widened constraint, not a bug fix. That has two consequences. First, illuminate/* and probably php constraints were loosened — check whether the PHP floor dropped, because a dropped floor can silently break a container build that assumed PHP 8.1+. Second, there is no evidence the Laravel 8 path is CI-tested. Your first integration is the test suite. Run it on a branch with a real composer.lock diff before it touches main.
Laravel 9+ compatibility is not claimed. Expect friction in the specific places framework majors broke packages: string-based controller route actions, the removal of Route::namespace(), $namespace on the service provider, and str_*/array_* helper migrations to Illuminate\Support\Str. These fail at runtime (class-not-found, facade errors), not at install, which makes them easy to miss in a green CI run. Smoke-test one upload, one rename, one delete on every Laravel upgrade.
Upload validation is the package's, not yours. The package validates against its own config (allowed_extensions, max size). Your application's validation rules do not apply inside the file manager. Keep the config allowlist narrow, and re-validate the path in your own controller before persisting — a client can post any string into the hidden input you populated from the picker.
Path traversal is the thing to actually test. Send ?path=../../.env, ?path=/etc/passwd, and a Unicode-normalized ..%2f..%2f at the listing and download endpoints. An unmaintained 2022-era filesystem package is exactly the profile of code where this is not handled perfectly. If it fails, add a normalizing middleware in front of the route prefix that rejects any path containing .. after rawurldecode, and never rely on the package's own sanitization.
Symlinks and permissions. storage:link is required for local public disks, and it does not survive a fresh container build unless it is in your deploy script. Writes go as the PHP-FPM user — a root-owned storage/app/uploads from a Docker build will fail with a bare "unable to write" that looks like a package bug and is not one.
Config caching bites on publish. If you published the config before setting env values, php artisan config:cache will freeze the old values. Always config:clear after editing config/filemanager.php, and keep env-driven values (FILEMANAGER_DISK) out of the published file where possible.
Debugging entry points, in order:
php artisan route:list --path=<prefix> — is it even mounted, and behind which middleware?php artisan vendor:publish --provider=... --tag=config again to diff your config against the shipped defaultStorage::disk(config('filemanager.disk'))->allFiles() in tinker — does the disk see what the UI claims to see?Extension points worth knowing. Three only: the config file (disks, extensions, middleware, prefix), published Blade views (layout, picker hooks), and the option of disabling the package's route registration entirely and writing your own thin controller over Storage for the four operations you need. For a small package like this, that last option is frequently the cheaper path than fighting the shipped UI — and it leaves you with code you can maintain after the archived dependency becomes untenable.
How can I help you explore Laravel packages today?