| name | filex |
| description | Async file uploads with Dropzone.js, temp-first pattern, chunked uploads, security scanning, and validation rules. |
Filex — Async File Upload System
When to use this skill
Activate this skill when:
- Adding file upload to any form (images, documents, media, attachments)
- Creating or editing models that have file/image columns
- Validating uploaded files
- Moving files between storage disks
- Building UI with drag-and-drop upload, progress bars, or file previews
Core Concept — Mandatory Temp-First Pattern
Filex uses a temp-first upload pattern. This is NOT optional — it is how the package works:
- User drops/selects files in the Blade component
- Files upload asynchronously via AJAX to
temp/ on the local disk
- The form submits temp path strings (e.g.,
temp/abc123_photo.jpg), NOT file objects
- Your controller receives string paths and moves them to permanent storage
- The controller stores the final permanent path in the database
This means: form fields for Filex uploads contain strings, not UploadedFile objects. All validation and controller logic MUST treat them as strings starting with temp/.
Strict Rules
Blade / Frontend Rules
-
ALWAYS include @filexAssets once per page before any <x-filex-uploader> component:
@filexAssets
<form method="POST" action="{{ route('posts.store') }}">
@csrf
<x-filex-uploader name="avatar" />
<button type="submit">Submit</button>
</form>
-
ALWAYS set the name prop to match the field name your controller expects.
-
ALWAYS set mimes prop to restrict file types. Never leave it open:
{{-- CORRECT — restricted to specific types --}}
<x-filex-uploader name="photo" mimes="jpeg,png,webp" :max-size="5" />
{{-- WRONG — accepts any file type --}}
<x-filex-uploader name="photo" />
-
ALWAYS set :max-size to limit file size in MB.
-
For multiple files, ALWAYS set :multiple="true" and :max-files:
<x-filex-uploader
name="documents"
:multiple="true"
:max-files="10"
:max-size="10"
mimes="pdf,doc,docx"
/>
-
For edit forms, ALWAYS pass existing files via value prop:
<x-filex-uploader
name="avatar"
mimes="jpeg,png,webp"
:max-size="5"
:value="$user->avatar"
/>
{{-- Multiple files --}}
<x-filex-uploader
name="documents"
:multiple="true"
:value="$post->documents"
/>
-
enctype="multipart/form-data" is not required — Filex handles uploads via AJAX before form submission, so the form only submits string paths. Adding it is harmless but unnecessary.
-
For image uploads, use dimension constraints when the design requires specific sizes:
<x-filex-uploader
name="banner"
mimes="jpeg,png"
:max-size="5"
dimensions="min_width=1200,min_height=630"
/>
Controller Rules
-
ALWAYS use the HasFilex trait in controllers that handle file uploads:
use DevWizard\Filex\Traits\HasFilex;
class PostController extends Controller
{
use HasFilex;
}
-
ALWAYS validate temp paths with ValidFileUpload rules before moving. Do NOT rely on a simple starts_with:temp/ check — it is vulnerable to path traversal attacks (e.g., temp/../../logs/laravel.log):
use DevWizard\Filex\Rules\ValidFileUpload;
$request->validate([
'avatar' => ['required', ValidFileUpload::forImages(maxSizeMB: 5)],
'document' => ['required', ValidFileUpload::forDocuments(maxSizeMB: 10)],
]);
-
For multiple file fields, ALWAYS validate both the array and each item:
$request->validate([
'documents' => ['required', 'array', 'max:10'],
'documents.*' => [ValidFileUpload::forDocuments(maxSizeMB: 10)],
]);
-
Use the trait's moveFile() / moveFiles() methods — NEVER manually copy or move temp files:
$path = $this->moveFile($request, 'avatar', 'avatars');
$paths = $this->moveFiles($request, 'documents', 'post-documents');
-
ALWAYS specify visibility explicitly for security-sensitive files:
$path = $this->moveFilePublic($request, 'avatar', 'avatars');
$path = $this->moveFilePrivate($request, 'contract', 'contracts');
-
To use a specific storage disk (like S3), pass it as the 4th argument:
$path = $this->moveFile($request, 'avatar', 'avatars', 's3');
$paths = $this->moveFiles($request, 'documents', 'docs', 's3');
-
Store the returned path in the model, not the temp path:
$avatarPath = $this->moveFile($request, 'avatar', 'avatars');
$post = Post::create([
'title' => $request->title,
'avatar' => $avatarPath, // Stores: "avatars/abc123_photo.jpg"
]);
$post = Post::create([
'avatar' => $request->avatar, // Stores: "temp/abc123_photo.jpg"
]);
Facade Rules
-
Use the Filex facade for file operations outside controllers (services, jobs, commands):
use DevWizard\Filex\Facades\Filex;
$result = Filex::moveFile($tempPath, 'avatars');
if ($result->isSuccess()) {
$permanentPath = $result->getPath();
}
$result = Filex::moveFiles($tempPaths, 'documents', 's3', 'private');
$successfulPaths = $result->getPaths();
$failedItems = $result->getFailed();
-
ALWAYS check FilexResult for errors when using the facade directly:
$result = Filex::moveFiles($tempPaths, 'uploads');
if (!$result->isAllSuccess()) {
$errors = $result->getErrorMessages();
Log::warning('Upload failures', ['errors' => $errors]);
}
$paths = $result->getPaths();
Complete Implementation Patterns
Pattern: Create Form with Single Image + Multiple Documents
Blade view:
@filexAssets
<form method="POST" action="{{ route('posts.store') }}">
@csrf
<div>
<x-filex-uploader
name="featured_image"
label="Featured Image"
:required="true"
:max-size="5"
mimes="jpeg,png,webp"
help-text="Recommended: 1200x630px"
/>
@error('featured_image') <span class="text-red-500">{{ $message }}</span> @enderror
</div>
<div>
<x-filex-uploader
name="attachments"
label="Attachments"
:multiple="true"
:max-files="5"
:max-size="10"
mimes="pdf,doc,docx,xlsx,zip"
/>
@error('attachments') <span class="text-red-500">{{ $message }}</span> @enderror
@error('attachments.*') <span class="text-red-500">{{ $message }}</span> @enderror
</div>
<button type="submit">Create Post</button>
</form>
Controller:
use DevWizard\Filex\Traits\HasFilex;
use DevWizard\Filex\Rules\ValidFileUpload;
class PostController extends Controller
{
use HasFilex;
public function store(Request $request)
{
$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
'featured_image' => ['required', ValidFileUpload::forImages(5)],
'attachments' => ['nullable', 'array', 'max:5'],
'attachments.*' => [ValidFileUpload::forDocuments(10)],
]);
$imagePath = $this->moveFilePublic($request, 'featured_image', 'posts/images');
$attachmentPaths = $this->moveFilesPrivate($request, 'attachments', 'posts/attachments');
$post = Post::create([
'title' => $validated['title'],
'featured_image' => $imagePath,
'attachments' => $attachmentPaths,
]);
return redirect()->route('posts.show', $post);
}
}
Pattern: Edit Form with Existing Files
Controller:
public function edit(Post $post)
{
return view('posts.edit', compact('post'));
}
public function update(Request $request, Post $post)
{
$validated = $request->validate([
'title' => ['required', 'string', 'max:255'],
'featured_image' => ['nullable', ValidFileUpload::forImages(5)],
'attachments' => ['nullable', 'array', 'max:5'],
'attachments.*' => [ValidFileUpload::forDocuments(10)],
]);
$data = ['title' => $validated['title']];
if ($request->filled('featured_image') && str_starts_with($request->featured_image, 'temp/')) {
$data['featured_image'] = $this->moveFilePublic($request, 'featured_image', 'posts/images');
}
if ($request->filled('attachments')) {
$data['attachments'] = $this->moveFilesPrivate($request, 'attachments', 'posts/attachments');
}
$post->update($data);
return redirect()->route('posts.show', $post);
}
Blade view (edit):
<x-filex-uploader
name="featured_image"
label="Featured Image"
:max-size="5"
mimes="jpeg,png,webp"
:value="$post->featured_image"
/>
<x-filex-uploader
name="attachments"
:multiple="true"
:max-files="5"
:max-size="10"
mimes="pdf,doc,docx"
:value="$post->attachments"
/>
Validation Reference
Preset rules (recommended — use these first)
| Method | Allowed Types | Default Max |
|---|
ValidFileUpload::forImages(5) | jpg, jpeg, png, gif, webp | 5 MB |
ValidFileUpload::forDocuments(10) | pdf, doc, docx, xls, xlsx, ppt, pptx, txt, rtf | 10 MB |
FileRule::forArchives(50) | zip, rar, 7z, tar, gz | 50 MB |
FileRule::forAudio(20) | mp3, wav, flac, ogg | 20 MB |
FileRule::forVideo(100) | mp4, avi, mov, mkv | 100 MB |
FileRule::forType('csv', 'text/csv', 10) | Custom single type | Custom |
FileRule::custom([exts], [mimes], maxMB) | Custom multiple types | Custom |
Granular rules (combine as needed)
use DevWizard\Filex\Support\FilexRule;
$request->validate([
'file' => [
FilexRule::file(), // File exists and readable
FilexRule::mimes('jpeg,png,pdf'), // Allowed extensions
FilexRule::max(10485760), // Max bytes
FilexRule::image(), // Must be valid image
FilexRule::dimensions('min_width=100,ratio=16/9'), // Dimension constraints
],
]);
String-based rules (for dynamic/config-driven validation)
$request->validate([
'file' => ['filex_file', 'filex_mimes:jpeg,png', 'filex_max:10485760', 'filex_image'],
]);
Configuration Reference (config/filex.php)
'storage' => [
'disks' => ['default' => 'public', 'temp' => 'local'],
'max_file_size' => 10,
'temp_expiry_hours' => 24,
'visibility' => ['default' => 'public'],
],
'upload' => [
'chunk' => ['size' => 1048576, 'max_retries' => 3, 'timeout' => 30000],
],
'routes' => [
'prefix' => 'filex',
'middleware' => [],
],
'security' => [
'suspicious_detection' => ['enabled' => true, 'quarantine_enabled' => true],
],
Artisan Commands
php artisan filex:install
php artisan filex:cleanup-temp
php artisan filex:cleanup-temp --dry-run
php artisan filex:optimize
php artisan filex:info
Common Anti-Patterns to Avoid
| Anti-Pattern | Correct Pattern |
|---|
Storing temp/ paths in database | ALWAYS moveFile() first, store the returned permanent path |
Using $request->file('avatar') | Filex fields are strings, use $request->avatar or moveFile($request, 'avatar', ...) |
Using Laravel's store() / storeAs() on Filex fields | Use $this->moveFile() or Filex::moveFile() |
| Skipping validation on temp paths | ALWAYS validate with ValidFileUpload rules to prevent path traversal |
Omitting @filexAssets in layout | Component JS/CSS won't load, uploads will silently fail |
No mimes restriction on uploader | ALWAYS set mimes to prevent unrestricted file types |
Manual Storage::move() on temp files | Use HasFilex::moveFile() — it handles metadata, security, and atomic writes |
Using required validation for edit forms | Use nullable for file fields on update — value is only present when changed |