waad / filament-import-wizard
A powerful, queue-powered CSV and Excel import wizard for Filament with smart column mapping, relationship linking, and background processing for 100K+ rows.
Requires
- php: ^8.2
- filament/filament: ^4.0|^5.0
- laravel/framework: *
- league/csv: ^9.0
- livewire/livewire: ^3.0|^4.0
- phpoffice/phpspreadsheet: ^5.0
Requires (Dev)
- laravel/pint: ^1.0
README
A powerful, queue-powered CSV and Excel import wizard for Filament with smart column mapping, relationship linking, Spatie Translatable support, locale merge, dynamic schema validation, and background processing for 100K+ rows.
2026-05-21.02.35.19.mp4
✨ Features
- 4-Step Import Wizard — Upload, map columns, review data, and import inside a Filament modal
- Reverse Mapping UI — Model fields shown first with CSV column dropdowns, not the other way around
- Smart Auto-Mapping — Automatically maps CSV headers to model fields using snake_case matching (e.g.,
Title→title,CategoryId→category_id) - Relationship Linking — Link or create related records (
BelongsTo) with intelligent auto-increment ID handling - Spatie Translatable Support — Detect
HasTranslationstraits and JSON columns; supportswhereJsonContains()for cross-DB translated lookups - Locale Merge — Combine multiple language columns (e.g.,
title_en,title_ar) into a single JSON translatable field with checkbox toggle - Locale-Aware Relation Lookup — Find or create related models using translated column values
- Upsert Support — Update existing records instead of creating duplicates via configurable keys
- Queue-Powered Processing — Chunked batch execution with live progress tracking for large datasets
- Eloquent Event Firing — Inserts fire model
boot(), observers, timestamps, and lifecycle hooks via individualsave() - Dynamic Schema Validation — Auto-generate validation rules from database column types (required, numeric, boolean, date, email, unique)
- Error Review — Inline validation and error download before import
- Multi-Tenancy Ready — Built-in team and tenant scoping across queue boundaries
- Standalone Mode — Use as a Livewire component outside Filament panels
- Dark Mode — Full dark theme support out of the box
- Custom Queue Configuration — Configurable queue connection and queue name per import
- Excel Empty Column Trimming — Automatically strips trailing empty Excel columns (no more 256-column UI)
📦 Installation
composer require waad/filament-import-wizard
Publish Configuration & Migrations
# Publish config file (optional) php artisan vendor:publish --tag="filament-import-wizard-config" # Publish and run migrations php artisan vendor:publish --tag="filament-import-wizard-migrations" php artisan migrate
⚠️ If there are errors for CSS, try rebuilding Filament assets:
php artisan filament:assets
🚀 Usage
Basic Usage in Filament Resource
Add the import action to your Filament resource's table.
Example (app/Filament/Resources/Posts/Pages/ListPosts.php):
use Waad\FilamentImportWizard\Actions\ImportWizardAction; protected function getHeaderActions(): array { return [ ImportWizardAction::make(), ]; }
Advanced Configuration
ImportWizardAction::make() ->forModel(\App\Models\Product::class) ->chunkSize(500) // Process 500 rows per job ->enableUpsert(true) // Update existing records ->upsertKeys(['sku']) // Match by SKU field ->queueConnection('redis') // Custom queue connection ->queueName('imports'); // Custom queue name
Standalone Usage
Use the wizard outside of Filament panels via Livewire:
@livewire('filament-import-wizard', ['modelClass' => \App\Models\Product::class])
⚙️ Configuration
// config/filament-import-wizard.php return [ 'modal_width' => Width::Full, // Modal width (Filament Width enum) 'chunk_size' => 1000, // Rows per queue job 'default_csv_delimiter' => ',', // CSV delimiter (comma, semicolon, tab) 'queue_connection' => null, // Queue connection (null = default) 'queue_name' => null, // Queue name (null = default) ];
Configuration Options
| Option | Default | Description |
|---|---|---|
modal_width |
Width::Full |
Width of the import wizard modal |
chunk_size |
1000 |
Number of rows processed per queue job |
default_csv_delimiter |
, |
Default CSV delimiter for parsing |
queue_connection |
null |
Queue connection to use (null = Laravel default) |
queue_name |
null |
Specific queue name (null = default queue) |
📋 Import Steps
Step 1: Upload
Upload your CSV or Excel file. Supported formats:
- CSV (
.csv) — with UTF-8 BOM auto-detection - Excel (
.xlsx,.xls) — trailing empty columns are automatically trimmed
Step 2: Map Columns
The mapping step presents model fields first, with a clean table-based layout:
- Model Fields Section — Each model field is displayed with its label, code name, and a dropdown to select which CSV column maps to it
- Relations Section — Each
BelongsTorelation is shown with its FK/PK badges and a searchable field dropdown - Unmapped Columns Section — CSV columns that are not mapped appear at the bottom
Locale Merge (Translatable Fields)
For translatable/JSON columns, toggle the Merge Translation switch to split a field into multiple locale→column mappings:
Title (translatable) → Merge Translation ON
[en] → title_en
[ar] → title_ar
[fr] → title_fr
The locale can be auto-detected from header names (title_en, titleAr, etc.) or entered manually. This stores data as JSON: {"en": "Hello", "ar": "مرحبا"}.
Relation Mapping
For BelongsTo relations:
Category → [Select CSV column...]
FK: category_id PK: id
[Search fields...] [name ▼]
Relations support auto-increment ID detection (if CSV has numeric IDs) and intelligent fallback to string field matching.
Step 3: Review & Validate
Preview your data before import:
- View first 100 rows with mapped columns
- See validation errors per row with error badges
- Dynamic rules auto-generated from database schema (column types, nullable, unique)
- Relation foreign keys bypass type constraints in validation (labels are resolved later)
- Configure upsert settings (enable/disable, keys)
Step 4: Import
Start the import process:
- Background queue processing with live progress polling every 5 seconds
- Sync queue runs jobs immediately for testing
- Eloquent mode (non-upsert): Records are saved individually via
save(), firingboot(), events, and observers - Upsert mode: Bulk
upsert()with fallback to chunked inserts on failure - Error tracking with downloadable CSV error logs
- Final summary with animated success/error counts and
completed_with_errorsstatus
🔗 Relationship Linking
Link related records during import:
// Example: Import products and link to categories CSV Column: "Category Name" → Relation: category → Field: name
Supported relationship types:
BelongsTo— with automatic FK/PK resolution
How Relation Resolution Works
- If the owner key is an auto-incrementing primary key:
- Numeric CSV values → find or create by ID
- String CSV values → find or create by
namefield, or auto-detect a suitable string column
- If the owner key is a non-ID column → direct lookup by that column
- Translatable/locale-aware resolution via
whereJsonContains()for JSON fields
🔃 Upsert (Match & Merge)
Update existing records instead of creating duplicates:
ImportWizardAction::make() ->forModel(\App\Models\User::class) ->enableUpsert(true) ->upsertKeys(['email']); // Match users by email
The wizard will:
- Find existing records by the specified keys
- Update matching records via
upsert()(with timestamp injection) - Create new records only when no match is found
- Fall back to chunked inserts on upsert failure
🌍 Spatie Translatable & Locale Merge
Automatic Detection
The wizard automatically detects:
- Models using
Spatie\Translatable\HasTranslationstrait - JSON/JSONB columns from the database schema
Locale Merge Mode
Check Merge Translation on any translatable field to split it into locale-specific mappings:
| CSV Header | Maps To |
|---|---|
title_en |
title with locale en |
title_ar |
title with locale ar |
title_fr |
title with locale fr |
The result is stored as a single JSON column: {"en": "value", "ar": "قيمة", "fr": "valeur"}
Custom Import Field Labels
Add a getImportFieldLabel() method to your model for custom display names:
class Product extends Model { public function getImportFieldLabel(string $field): string { return match($field) { 'sku' => 'SKU (Stock Keeping Unit)', 'price_in_cents' => 'Price (cents)', default => Str::title(str_replace(['_', '.'], ' ', $field)), }; } }
🧠 Smart Defaults & Guarded Model Support
The wizard handles models with $guarded = [] (no fillable defined) gracefully:
- Dynamic fillable resolution: Falls back to
Schema::getColumnListing()whengetFillable()is empty - All columns allowed: When fillable is empty, any CSV column can map to any database column
- Foreign key resolution: Bypasses fillable checks when model is fully unguarded
🛠️ Customization
Custom Modal Width
use Filament\Support\Enums\Width; ImportWizardAction::make() ->forModel(\App\Models\Product::class) ->setModalWidth(Width::ExtraLarge);
Queue Configuration
Set queue connection and name globally via config:
// config/filament-import-wizard.php return [ 'queue_connection' => 'redis', 'queue_name' => 'imports', ];
Or per import action:
ImportWizardAction::make() ->forModel(\App\Models\Product::class) ->queueConnection('redis') ->queueName('imports');
Custom Validation Rules
Add a getImportRules() method to your model for custom import validation:
class Product extends Model { public function getImportRules(): array { return [ 'sku' => ['required', 'string', 'max:50'], 'price' => ['required', 'numeric', 'min:0'], 'stock' => ['nullable', 'integer', 'min:0'], ]; } }
If no getImportRules() method exists, rules are auto-generated from the database schema.
📝 Requirements
- PHP: 8.2+
- Laravel: 10+
- Filament: 4.x or 5.x
📄 License
The MIT License (MIT). Please see License File for more information.
🤝 Contributing
Contributions are welcome! Please open an issue or submit a pull request.
📧 Support
If you discover any bugs or have feature requests, please open an issue on GitHub.



