helios-ag/fm-elfinder-php-connector
Laravel wrapper for the elFinder PHP connector that lets you browse, upload, edit, and manage files and folders through elFinder's file-manager UI. Includes connector configuration and routes for quickly adding file management to Laravel apps.
composer require helios-ag/fm-elfinder-php-connector. This is the server-side connector only — a Composer-packaged copy of Studio-42/elFinder's PHP connector. You still need the elFinder front-end (JS/CSS/images) separately via npm (elFinder), a CDN, or by copying Studio-42/elFinder's dist assets into public/.elFinderConnector and elFinder classes (they are global, non-namespaced classes — no PSR-4, no use statements) and at Studio-42/elFinder's php/connector.minimal.php for the option-array semantics. The connector is configured entirely by that array; there is no Laravel service provider, facade, or config file shipped.roots, one route, one controller.// config/elfinder.php
return [
'roots' => [[
'driver' => 'LocalFileSystem',
'path' => public_path('uploads'),
'URL' => url('uploads'),
'accessControl' => function ($attr, $path, $data) {
return auth()->check() && auth()->user()->is_admin;
},
]],
];
Route::match(['get', 'post'], 'elfinder/connector', ElFinderController::class)
->middleware('auth');
public function __invoke()
{
$connector = new \elFinderConnector(new \elFinder(config('elfinder')));
$connector->run(); // echoes JSON + headers directly
}
public/uploads — browse, upload, rename, delete, and drag-drop — with PHP execution blocked and access gated by accessControl.Keep options in a builder class, not the controller. The connector has no Laravel integration, so the only thing worth testing is your options array. Put ElFinderConfig::forUser($user): array in a service and unit-test it; the controller stays a two-liner.
Per-user roots. The idiomatic pattern is one root per user with a dynamic path/URL and a strict accessControl callback:
'path' => storage_path("app/elfinder/{$user->id}"),
'URL' => url("user-files/{$user->id}"),
'accessControl' => fn ($attr, $path, $data) =>
str_starts_with(realpath($path), realpath(storage_path("app/elfinder/{$user->id}"))),
The realpath/prefix check matters — the callback receives arbitrary paths from the client, including .. segments.
Use attributes for extension policy, not just accessControl:
'attributes' => [
['pattern' => '/\.(php|phtml|phar|htaccess)$/i', 'read' => false, 'write' => false, 'locked' => true],
['pattern' => '/^\./', 'hidden' => true],
]
Bind events for auditing and side effects. $finder->bind('upload', fn ($cmd, &$result) => ...) (also mkdir, rm, rename, upload.presave) is where you hook Laravel logging, image resizing, or firing events. Mutate $result only if you know the connector's response shape.
Serve the connector outside the web middleware group. It emits its own Content-Type/Content-Length and streams file bodies for file/download commands. Wrap it in a lean group (auth, session, CSRF) rather than web, which drags in cookie encryption, debugbar, and response decorators that corrupt output.
Front-end init needs the connector URL and, if you don't fully exempt CSRF, customData:
elFinder.prototype.options.customData = { _token: window.csrfToken };
Lock the volume down with disabled commands — ['archive', 'extract', 'mkfile', 'netmount', 'netunmount'] removes whole classes of abuse without touching the filesystem layer. Pair with uploadMaxSize, uploadOrder, and acceptedName to constrain uploads.
Separate roots for public vs private. Public assets: LocalFileSystem under public_path(). Private documents: a second root under storage_path('app/...') with no URL set and a different accessControl. tmbPath/tmbURL must point at a writable, web-served directory or thumbnails silently fail.
each()/create_function-era helpers. Test on your actual PHP version before committing; be prepared to fork or vendor the class files.new \elFinder(...) and new \elFinderConnector(...) are the whole API. Don't expect config/elfinder.php to be read by anything — you wrote that.VerifyCsrfToken::$except (simplest) or pass _token via customData on every command. Forgetting this shows up as an HTML 419 page parsed as JSON.ShareErrorsFromSession, compression middleware, and anything that appends to the body will corrupt the connector's JSON or truncate downloads. If the file browser shows "invalid backend response," check the raw network tab first.run() echoes; it does not return a Response. You cannot return $connector->run(), and any header() already sent by Laravel will collide. Keep the controller action free of output before run().session_start() for upload chunking. With Laravel's session already started, you get warnings or a corrupted session payload. Usually harmless, but suppress or isolate if it leaks into the response body.path must be an absolute, existing, writable directory; URL must be the matching web-accessible root, no trailing slash ambiguity. Get one wrong and you get an empty pane or broken thumbnails with no error.accessControl, always block script extensions with attributes, and add nginx/Apache rules so the upload directory cannot execute PHP. Never point a root at base_path() or storage_path() wholesale.tmbPath write permissions or a too-small uploadMaxSize produce silent failures rather than exceptions. Check the php.ini upload_max_filesize/post_max_size values too — they override the connector's own limits.elFinder against this 2018 connector gives confusing "unknown command" errors.barryvdh/laravel-elfinder wraps this same generation of code (equally dated). For new work, UniSharp/laravel-filemanager, spatie/laravel-medialibrary, or a small custom Livewire/Filament file browser will cost less than carrying a 2018 connector through PHP upgrades. If you do keep it, fork it into app/Support/ElFinder and own the code — you're one PHP minor release away from needing to anyway.How can I help you explore Laravel packages today?