barryvdh/elfinder-builds
Prebuilt, versioned elFinder assets for Laravel. Ships compiled JavaScript and CSS for the elFinder file manager, so you can add a browser-based file manager to your app without running the front-end build yourself.
Understand what you are installing first. barryvdh/elfinder-builds contains no PHP: no service provider, no routes, no config, no commands. It is a Composer-shaped tarball of prebuilt elFinder 2.1 client assets — js/, css/, img/, sounds/, js/i18n/, themes — copied out of upstream Studio-42/elFinder builds. Everything you actually interact with day-to-day (routes, connector, volumes, ACLs) comes from barryvdh/laravel-elfinder and your own config. Treat this package as vendored static files, not as a library.
Minimal steps.
composer require barryvdh/laravel-elfinder # pulls elfinder-builds transitively
# pin the asset bundle explicitly so it can't float under you
composer require barryvdh/elfinder-builds:2.1.* --save-exact
php artisan vendor:publish # copies config + views
# and publishes the bundle into public/vendor/elfinder
The exact publish tag varies by laravel-elfinder version — run php artisan vendor:publish interactively (or read the provider's publishes() array) before scripting it into CI. What you want to end up with:
public/vendor/elfinder/{js,css,img,sounds} populatedconfig/elfinder.php presentWhere to look first, in this order:
vendor/barryvdh/elfinder-builds/ — confirm the version in composer.json, the license header in js/elfinder.min.js, and that i18n files match the same release. This is the only provenance audit you get.config/elfinder.php — defaults.roots[] is the whole game: driver, path, URL, accessControl.vendor/barryvdh/laravel-elfinder/resources/views/ — the shipped Blade views (elfinder::elfinder standalone, elfinder::standalonepopup, plus editor-picker variants) show you the canonical asset-include order. Copy that, don't improvise.auth before you browse anything.First use case (an afternoon). A read-only admin browser over storage/app/public:
// config/elfinder.php
'defaults' => [
'roots' => [[
'driver' => 'LocalFileSystem',
'path' => storage_path('app/public'),
'URL' => config('app.url').'/storage',
'accessControl' => fn($attr, $path, $data, $volume) => false, // read-only spike
]],
],
Route::middleware(['web', 'auth'])->prefix('admin')->group(function () {
Route::get('elfinder/{path?}', [ElfinderController::class, 'showIndex'])->where('path', '.*');
Route::any('elfinder/connector', [ElfinderController::class, 'showConnector'])
->name('elfinder.connector');
});
php artisan storage:link first. Ship this read-only variant before enabling writes — it validates CSP, auth, and listing performance without exposing an upload surface.
Think in three layers, and keep them separate.
| Layer | Owned by | Changes how often |
|---|---|---|
Assets in public/vendor/elfinder |
this package (published once) | only on upgrade |
| Routes / views / connector glue | laravel-elfinder |
rarely |
| Roots, ACL, upload policy | your config | whenever the product changes |
Almost every production incident here comes from blurring layers 1 and 3 — patching the minified JS instead of the config.
Asset delivery: pick one and write it down. Three viable strategies:
public/vendor/elfinder in .gitignore and commit it. Deploys can't drift; vendor:publish behaviour becomes irrelevant. Best default for a single app.php artisan vendor:publish --force
git diff --exit-code public/vendor/elfinder
/vendor/elfinder/2.1.63/... from object storage. Only worth it across multiple apps.Whatever you pick, add a cache-busting query string keyed to the bundle version (?v={{ $elfinderVersion }}), because these files are served straight from public/ with no Vite hashing.
Standalone page pattern. Serve elFinder as a full page load, from a Blade view, with a fixed-height container. Do not embed it in a shell that swaps DOM later.
@extends('layouts.admin')
@section('content')
<div id="elfinder"></div>
<script>
document.addEventListener('DOMContentLoaded', function () {
$('#elfinder').elfinder({
url: @json(route('elfinder.connector')),
lang: 'en',
height: 600,
soundPath: @json(asset('vendor/elfinder/sounds')),
});
});
</script>
@endsection
Multi-volume console. One root per storage policy, each with its own accessControl. This is the pattern that earns its keep — local uploads, an S3 media bucket, and a read-only shared-assets volume in a single tree:
'roots' => [
['driver' => 'LocalFileSystem', 'path' => storage_path('app/public/media'),
'URL' => config('app.url').'/storage/media',
'accessControl' => 'App\Elfinder\Acl@media'],
['driver' => 'LocalFileSystem', 'path' => '/mnt/shared/brand',
'URL' => config('app.url').'/storage/brand', 'readonly' => true,
'accessControl' => 'App\Elfinder\Acl@brandReadOnly'],
],
Keep ACL logic in a small class, not a closure in config — closures can't be cached and can't be tested.
Picker pattern (the actual day-to-day win). For an "Insert media" button behind CKEditor/TinyMCE or a custom form field, open a popup and hand the selection back via getFileCallback:
$('#elfinder').elfinder({
url: route,
getFileCallback: function (file) {
window.opener?.setMediaUrl?.(file.url);
window.close();
},
});
Pass any scoping context (user id, tenant, target folder) through customData on the connector request and read it in your accessControl/root config. That is the cleanest way to get per-user folders without forking views.
Harden the connector as a code change, not a checklist. Everything risky lives here:
'roots' => [[
'driver' => 'LocalFileSystem',
'path' => storage_path('app/public/uploads'),
'URL' => config('app.url').'/storage/uploads',
'uploadAllow' => ['image/jpeg', 'image/png', 'application/pdf'],
'uploadDeny' => ['all'],
'uploadOrder' => ['allow', 'deny'],
'disabled' => ['archive', 'extract', 'mkfile', 'chmod'],
'tmbPath' => storage_path('app/elfinder-thumbs'),
'tmbPathMode' => 0775,
]],
And a test that fails loudly if the route ever loses its guard:
public function test_connector_requires_authentication(): void
{
$this->get('/admin/elfinder/connector')->assertRedirect('/login');
}
Version skew is the failure mode. The JS bundle and the connector must be from the same elFinder generation. A mismatch typically surfaces as an unhelpful invalid backend response in a modal, or a silent empty tree. Before debugging anything else, open DevTools → Network → the connector POST and look at the raw response: if it's HTML (a 419, a 500 stack trace, a login redirect), the problem is server-side; if it's JSON with an unexpected error, it's a connector/config problem; if it's JSON that looks fine but the UI misbehaves, it's skew.
vendor:publish --force will silently overwrite your edits. If you must patch a published asset (e.g. a theme tweak), keep the patch as a diff file in resources/ and reapply, or better, add an override CSS/JS file loaded after the bundle and never edit public/vendor/elfinder in place. Also check whether your public/.gitignore excludes vendor/ — a very common cause of "works locally, broken in prod" after a deploy.
CSP will bite on day one. elFinder 2.1 needs inline styles and blob workers. Expect to add something like style-src 'unsafe-inline' and worker-src blob: scoped to the file-manager route only — don't loosen the policy site-wide. Document the exception with a link to this assessment; it will be questioned.
jQuery duplication breaks the widget layer. elFinder expects its own jQuery/jQuery-UI. If your admin layout already loads a different jQuery version, expect sorting, dialogs, and context menus to misbehave intermittently. Load the file-manager page as a bare layout with only elFinder's dependencies.
Partial-page navigation destroys it. Livewire wire:navigate, Turbo Drive, and Inertia client-side transitions will leave a half-initialized tree. Opt the page out (data-turbo="false", or render it outside the SPA shell) and let it be a full reload.
Large directories hang the connector. elFinder 2.1 lists a root eagerly and has no real pagination. A volume with tens of thousands of files will produce a slow connector scan and browser stalls. Keep roots shallow and folder-per-entity (media/{user_id}/, media/{year}/{month}/) rather than one flat bucket.
web middleware or nothing. The connector is a stateful, session-authenticated POST. Putting it on the api group (or VerifyCsrfToken exceptions without session) reliably yields 419s or unauthenticated connector calls. Long upload sessions can also outlive session.lifetime — check that before blaming file size.
Uploads that "do nothing" are usually PHP/nginx limits, not elFinder: upload_max_filesize, post_max_size, client_max_body_size, and the temp dir. Check those before touching config.
S3 roots: verify the driver class. elFinder 2.1's classic S3 driver assumes an older AWS SDK; if your app ships SDK v3, prefer a Flysystem-backed root or mount the bucket locally. Do not discover this mid-sprint.
Sound and thumbnail 404s are noise, not bugs. sounds/ and tmbPath requests will 404 until configured. Remove the sound path or set a writable tmbPath — thumbnails regenerated on every request are a sneaky performance tax.
Supply-chain visibility is your job. This is checked-in minified third-party JS: Dependabot and SCA won't see it, and the wrapper does not patch upstream. Put a quarterly calendar reminder against Studio-42/elFinder security releases, and know your exit: npm i elfinder, add a Vite entry that exposes the same global, and swap the asset() calls in your one Blade view. That's the whole migration — which is exactly why keeping elFinder confined to a single full-page view is worth the discipline.
How can I help you explore Laravel packages today?