studio-42/elfinder
elFinder is an open-source file manager for the web, built in JavaScript with jQuery UI and inspired by Mac OS X Finder. It offers a fast, convenient interface for managing files and folders, with PHP connector support via Composer and a wide ecosystem of third-party connectors and themes.
elFinder is not a Laravel package — it is a jQuery UI application plus a standalone PHP connector (php/connector.minimal.php) that speaks a JSON command protocol. To use it in Laravel you almost always go through barryvdh/laravel-elfinder, which wraps studio-42/elfinder and exposes a config-driven connector.
composer require barryvdh/laravel-elfinder
php artisan vendor:publish --provider="Barryvdh\Elfinder\ElfinderServiceProvider"
php artisan elfinder:publish # copies js/css/img to public/vendor/elfinder
Where to look first, in this order:
config/elfinder.php — dir, roots, route, disks. This is the whole integration surface; everything else is defaults.vendor/studio-42/elfinder/php/connector.minimal.php-dist — the canonical example of every volume option (roots, URL, uploadAllow, accessControl, tmbPath, plugins). The Laravel config array is fed straight into this.vendor/studio-42/elfinder/js/elfinder.full.js (or .min.js) — client options live here; the wiki is thin, the source is the documentation.@include('elfinder.elfinder') — the client bootstrap and how options reach $().elfinder(...).Minimal first use case: an admin-only media browser over storage/app/public.
// config/elfinder.php
'dir' => storage_path('app/public'),
'roots' => [[
'driver' => 'LocalFileSystem',
'path' => storage_path('app/public'),
'URL' => env('APP_URL').'/storage',
'tmbPath' => storage_path('app/public/.tmb'),
'tmbURL' => env('APP_URL').'/storage/.tmb',
'accessControl' => function ($attr, $path) {
return auth()->check() && auth()->user()->is_admin;
},
]],
'route' => ['prefix' => 'elfinder', 'middleware' => ['web', 'auth']],
Then render it on a page. Guard the route with auth yourself — the package's default route is not protected.
Per-user / multi-tenant roots. Don't branch on $_SESSION inside the config. Build roots in a service provider (or a config/elfinder.php that reads the authenticated user) and key paths per tenant:
'roots' => [[
'driver' => 'LocalFileSystem',
'path' => storage_path('app/tenants/'.auth()->id()),
'URL' => url('tenant-files/'.auth()->id()),
'accessControl' => fn($attr, $path, $data) => auth()->id() === $data['user_id'],
]],
accessControl runs for every ls/open/file call, so it's the right place for path-level rules, but it is not a substitute for the root being outside public_html when the files are sensitive — elFinder can be told to serve content through the connector ('URL' => false) instead of a direct web path.
ACL by attribute. The $attr argument is one of read, write, hidden, locked. Typical pattern: read-only archive volume, hidden dotfiles, locked system folders.
'accessControl' => function ($attr, $path, $data) {
if (str_starts_with(basename($path), '.')) return false; // no dotfiles
if ($attr === 'write' && $data['readonly']) return false;
return true;
},
'attributes' => [
['pattern' => '/^\/system$/', 'read' => true, 'write' => false, 'locked' => true],
],
Upload hardening. Never ship defaults. Set uploadAllow (MIME allowlist), uploadOrder (usually ['deny', 'allow']), uploadMaxSize, acceptedName (regex), and disable the commands you don't need ('disabled' => ['netmount', 'edit', 'archive', 'extract']). Note that ZIP extraction and URL upload have had their own security advisories — see Gotchas.
File picker in an admin form. This is the highest-value day-to-day use: elFinder as a reusable "choose a file" dialog instead of a bespoke modal.
const fm = $('#elfinder').elfinder({
url: '/elfinder/connector',
lang: 'en',
onlyMimes: ['image', 'application/pdf'],
getFileCallback: function (file) {
$('#hero_image').val(file.url); // hidden input → normal Laravel validation
fm.dialog('close');
},
handlers: { open: function () { /* lazily fetch current value */ } },
}).elfinder('instance');
barryvdh/laravel-elfinder ships an elfinderDialog() helper built on jQuery UI dialog for exactly this flow; use it rather than reimplementing.
CKEditor / TinyMCE integration. Set the editor's filebrowserImageBrowseUrl to a Blade page that renders elFinder with getFileCallback and calls back via window.opener. In a Livewire/Inertia app, instead render a standalone Blade route and use postMessage — elFinder already has a postMessage bridge, but treat it as a security-sensitive surface (see Gotchas).
Persisting uploads to your own model. elFinder doesn't know about Eloquent. Two options:
upload / add and POST the returned file.hash/file.path to a MediaController@store that creates the record and re-derives metadata.elFinderVolumeLocalFileSystem, override _save, call parent::_save(), then fire an event / dispatch a job. Register it with 'driver' => App\Filesystem\MediaVolume::class and composer-autoload it. This keeps uploads atomic with the file write.Plugins. AutoResize, AutoRotate, Normalizer, Sanitizer, Watermark are plain PHP classes in vendor/studio-42/elfinder/php/plugins/. Enable per volume with 'plugin' => [['class' => ..., 'config' => [...]]]; Sanitizer + Normalizer are near-mandatory for user uploads on macOS-origin filenames (NFD, colons, emoji).
Custom commands / business logic. Rather than editing vendor code, wrap the connector: define your own controller that includes the connector under output buffering, or (cleaner) bind a custom elFinderConnector and register extra commands. For anything complex — moving a file should update a DB row — prefer filesystem events on the volume over client events, so a drag-and-drop and a programmatic move behave identically.
Pin the version and update aggressively. The upstream README leads with an explicit warning that old versions "may cause serious damage to your server." 2.1.70 fixed CSRF on netmount, MIME validation during ZIP extraction, an SSRF bypass in fsock_get_contents(), and a postMessage origin bypass plus DOM XSS in playsound. If you're on anything < 2.1.70, that's a security ticket, not a chore. Pin with "studio-42/elfinder": "2.1.70" in composer.json — a ^2.1 constraint will happily pull nightly-ish patch releases.
License is BSD-3-Clause, not "NOASSERTION". Packagist reports NOASSERTION; the LICENSE.md is 3-clause BSD. Attribution text must be retained — matters if legal scans your dependency tree.
CSRF → 419, and it looks like an elFinder bug. The connector issues POSTs that Laravel's VerifyCsrfToken rejects. Either add 'elfinder/*' to $except (the connector has its own session-based CSRF token flow since 2.1.68, which is separate), or pass Laravel's token through customData and validate in a middleware. Don't do both and assume they interact.
Debugbar and output buffering break the JSON stream. The connector writes JSON and calls exit. Any middleware that appends HTML, or Telescope/Debugbar injecting a toolbar, produces Unexpected token < in JSON. Exclude the connector route from those middlewares explicitly.
Session locking on large uploads. PHP holds the session file lock for the whole request; a 500 MB chunked upload blocks every other AJAX call for that user. Call session_write_close() early in the connector path or give the connector its own session driver/cookie.
path must be an absolute, existing, writable directory — storage:link is not enough. The URL must correspond to that path or thumbnails and getContentUrl() produce broken links (there was a fix for exactly this in 2.1.68). Make tmbPath writable by the PHP-FPM user; forgetting it gives silent "no thumbnail" icons rather than an error.
Local-only by default. The bundled driver is LocalFileSystem. For S3 you need barryvdh/elfinder-flysystem-driver wrapping Storage::disk('s3'). It works, but chunked uploads, archive/extract, and directory-size calculation are inconsistent across non-local drivers — plan for degraded features and hide those toolbar buttons per volume.
The client is jQuery + jQuery UI. If your admin is Vue/React/Bootstrap 5, expect CSS collisions (elFinder ships its own theme, resets, and z-index assumptions) and the $ global conflict. Cleanest path: mount elFinder on its own Blade route, not inside your SPA, and use it as a dialog. Also check jQuery UI modules — draggable, droppable, resizable, selectable, button, slider are required; a partial build of jQuery UI fails with cryptic errors on load.
Debugging: read the connector JSON, not the UI. Every failure surfaces in the Network tab as the connector response with {"error": [...]}. Set client debug: true to see the command payloads. Server-side, 'debug' => true in the connector config dumps errors instead of swallowing them — never leave it on in production, it leaks paths.
Don't extend by editing vendor/. The five extension points that survive composer update: custom volume drivers (subclass), the accessControl/attributes config callbacks, per-volume plugin array, client handlers/commandsOptions (toolbar, contextmenu, ui arrays), and a thin wrapper controller for anything else. Themes go in a separate package (RobiNN1/elFinder-Material-Theme etc.) loaded after the base CSS.
Testability is poor by design. The connector is a front-controller you can't easily feature-test. Keep custom business logic in your own service classes invoked from a volume driver or a wrapper controller, so php artisan test covers the logic and the connector remains a thin, untested shim you smoke-test manually.
How can I help you explore Laravel packages today?