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

Laravel Filex Laravel Package

devwizardhq/laravel-filex

Laravel Filex is a Dropzone.js-powered Blade component for Laravel with drag-and-drop uploads, chunked large-file transfers, real-time progress, multi-layer validation, quarantine security, temp storage, and auto cleanup.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish

composer require devwizardhq/laravel-filex
php artisan filex:install          # config + assets
php artisan filex:info             # confirms routes/config loaded

Then add @filexAssets once in your layout <head> (or before </body>):

{{-- resources/views/layouts/app.blade.php --}}
@filexAssets

That directive is what registers the Dropzone JS/CSS and the package's upload routes. Without it the component renders but uploads fail silently.

Where to look first

  1. config/filex.php — storage disks (default, temp), max_file_size, temp_expiry_hours, routes.prefix, security.*.
  2. The HasFilex trait — this is the API you'll actually call 90% of the time.
  3. DevWizard\Filex\Rules\ValidFileUpload — the validation presets you must use on temp paths.
  4. php artisan route:list --path=filex — confirms the AJAX endpoints exist.

First use case: avatar upload

Blade:

<form method="POST" action="{{ route('profile.update') }}">
    @csrf
    <x-filex-uploader
        name="avatar"
        label="Avatar"
        :max-size="5"
        mimes="jpeg,png,webp"
        dimensions="min_width=200,min_height=200"
    />
    @error('avatar') <span class="text-red-500">{{ $message }}</span> @enderror
    <button type="submit">Save</button>
</form>

Controller:

use DevWizard\Filex\Traits\HasFilex;
use DevWizard\Filex\Rules\ValidFileUpload;

class ProfileController extends Controller
{
    use HasFilex;

    public function update(Request $request)
    {
        $request->validate([
            'avatar' => ['nullable', ValidFileUpload::forImages(maxSizeMB: 5)],
        ]);

        $data = $request->only('name');
        if ($request->filled('avatar') && str_starts_with($request->avatar, 'temp/')) {
            $data['avatar'] = $this->moveFilePublic($request, 'avatar', 'avatars');
        }

        auth()->user()->update($data);

        return back()->with('success', 'Profile updated.');
    }
}

The mental model to internalise immediately: the form field contains a string path (temp/abc123_photo.jpg), not an UploadedFile. Everything else follows from that.

Implementation Patterns

Single vs. multiple fields

<x-filex-uploader name="documents" :multiple="true" :max-files="10" :max-size="10" mimes="pdf,doc,docx" />

Validate both the array and each element — Laravel will not apply the rule to items for you:

$request->validate([
    'documents'   => ['required', 'array', 'max:10'],
    'documents.*' => [ValidFileUpload::forDocuments(10)],
]);

$paths = $this->moveFilesPrivate($request, 'documents', 'post-documents');

Create-and-store pattern with mixed image + attachments

$imagePath  = $this->moveFilePublic($request, 'featured_image', 'posts/images');
$attachPaths = $this->moveFilesPrivate($request, 'attachments', 'posts/attachments');

$post = Post::create([
    'title'          => $validated['title'],
    'featured_image' => $imagePath,      // permanent path only
    'attachments'    => $attachPaths,
]);

Edit forms — the :value prop and the temp/ guard

Pre-populate with existing files so users see what's already attached:

<x-filex-uploader name="featured_image" :value="$post->featured_image" mimes="jpeg,png,webp" :max-size="5" />
<x-filex-uploader name="attachments" :multiple="true" :value="$post->attachments" mimes="pdf,docx" />

On update, only move when the submitted value is actually a new temp file. Existing permanent paths will be re-posted unchanged:

if ($request->filled('featured_image') && str_starts_with($request->featured_image, 'temp/')) {
    $data['featured_image'] = $this->moveFilePublic($request, 'featured_image', 'posts/images');
}
// a bare `$request->featured_image` starting with 'posts/' is already permanent — leave it alone

Make the rule nullable here, not required — the field is legitimately empty when unchanged.

Outside controllers: the facade

Use Filex in jobs, imports, console commands, and service classes:

use DevWizard\Filex\Facades\Filex;

$result = Filex::moveFiles($tempPaths, 'imports/documents', 's3', 'private');

if (! $result->isAllSuccess()) {
    Log::warning('Filex move failures', ['errors' => $result->getErrorMessages()]);
}
$paths = $result->getPaths(); // successful only

Check the result object every time — the facade does not throw on partial failure.

Disk and visibility selection

$this->moveFile($request, 'avatar', 'avatars');                       // default disk + visibility
$this->moveFile($request, 'contract', 'contracts', 's3');             // explicit disk
$this->moveFilePrivate($request, 'contract', 'contracts');            // no public URL
$this->moveFiles($request, 'docs', 'docs', 's3');                      // multiple, explicit disk

Pick private for anything user-specific or contract-like; public only for marketing/avatar assets that go through a CDN.

Large file handling

Chunking is on by default (chunkSize 1 MB, parallelUploads 2, retries 3). Tune per component for big media rather than globally:

<x-filex-uploader name="video" :max-size="500" mimes="mp4,mov" :chunk-size="5242880" :retries="5" timeout="120000" />

JavaScript hooks

onSuccess/onError/onComplete/onFileAdded/onFileRemoved/onUpload take the name of a global function as a string prop:

<x-filex-uploader name="files" onSuccess="handleUploaded" />
window.handleUploaded = (file, response) => { /* update a counter, refresh a preview */ };

Maintenance

php artisan filex:cleanup-temp --dry-run   # preview first, always
php artisan filex:cleanup-temp

Schedule the non-dry-run version daily; temp files expire after storage.temp_expiry_hours (24 h default) but only get reaped by this command.

Gotchas and Tips

$request->file('avatar') returns null. This is the number-one trap. Filex fields are strings; Laravel's store(), storeAs(), and UploadedFile methods do not apply. Use moveFile()/moveFiles() or Filex::moveFile().

Never store a temp/ path in the database. Temp files are deleted by the cleanup command. If moveFile() returns null (missing temp file, expired, or failed validation), handle it — don't fall through to $request->avatar.

Validate with ValidFileUpload, not starts_with:temp/. A plain prefix check is path-traversal vulnerable (temp/../../.env). The rule class re-checks the file exists, is inside the temp directory, and matches MIME/extension/size.

Client-side restrictions are cosmetic. mimes and :max-size on the component are UX guardrails. The server-side rule in the controller is the only enforcement that matters. Set both, and keep them in sync — mismatches produce confusing "upload succeeded but validation failed" flows.

@filexAssets exactly once. Twice causes duplicated Dropzone registration and broken drop zones; zero times causes silent AJAX failures with no console error beyond a 404.

No enctype="multipart/form-data" needed. Files travel via AJAX to temp/ before submit; the form only posts path strings. Adding the enctype is harmless but misleading to the next developer.

Edit forms need nullable, not required, plus the str_starts_with($value, 'temp/') guard. Otherwise re-saving a record with untouched files either fails validation or re-moves nonexistent temp paths.

dimensions accepts either a string ("min_width=200,min_height=200") or an array, but the array keys use underscores (min_width, max_height) while the Blade prop in the README shows nested arrays — pick one form and stay consistent within a project, and verify against your installed version.

Visibility must match where you serve from. Moving an avatar with moveFilePrivate() and then rendering Storage::url($path) gives a broken image. Public files need public visibility (and storage:link); private files need signed URLs or a streaming controller.

Chunk size must sit under upload_max_filesize/post_max_size — but chunking means individual requests are small, so the real ceiling is max_file_size in config/filex.php plus memory (README asks for 256 MB, 512 MB recommended for large media). Bump PHP limits and the config together.

Quarantine produces "file not found" downstream. Files flagged by security.suspicious_detection get quarantined rather than moved; moveFile() returns null. If users report inexplicable failures on specific PDFs/ZIPs, check the quarantine directory and the package log channel before assuming a bug.

Route prefix collisions. routes.prefix defaults to filex. If you already own /filex, change it in config/filex.php and republish assets (php artisan filex:install --only-assets) — the compiled JS embeds the endpoints.

onSuccess handlers must be globals, defined before the component initialises. A handler defined inside a deferred module or a DOMContentLoaded callback may not exist when Dropzone fires.

Debug prop for silent failures. :debug="true" on the component plus tail -f storage/logs/laravel.log is the fastest path when uploads stall at 0% (usually missing assets or a CSRF/middleware issue on the upload route) versus 100%-but-no-file (usually a validation rejection).

Add your own middleware to the upload routes. routes.middleware in config is where auth belongs if uploads should require login — the package does not assume it.

Maturity caveat. At 7 stars and zero dependents, treat this as a young package: pin the exact version in composer.json, read CHANGELOG.md before upgrading (the last release is dated 2026-09-20 and the API has room to shift), and wrap moveFile()/moveFiles() behind a thin application-level service so a future swap to plain UploadedFile handling is a one-file change rather than a grep across every controller.

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.
dakiquang/laravel-oauth-refresh-lock
woda/laravel-worktrees
dalefranks/ramasite
faghy/laracashbook
roundly-consulting/google-places-for-laravel
roundly-consulting/kubernetes-api-for-laravel
roundly-consulting/plausible-for-laravel
gvp/gvp-laravel-template
metasyncsite/laravel-metasync-client
roundly-consulting/onboarding-for-laravel
andydefer/laravel-notification
adachsoft/cli-process-tester
jhaskell13/laraflow
sebastienheyd/boilerplate-media-manager
ccast/tagixo-primix
pretendtrue/laravel-builder
sawmainek/laravel-scout-tntsearch-driver
tomshaw/laravel-calendar-table
dibakar/laravel-ownership
adachsoft/changelog-linter