waad/filament-import-wizard
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.
https://github.com/user-attachments/assets/f4c3de05-a682-4e87-bfa6-a433ebc856ec
Title → title, CategoryId → category_id)BelongsTo) with intelligent auto-increment ID handlingHasTranslations traits and JSON columns; supports whereJsonContains() for cross-DB translated lookupstitle_en, title_ar) into a single JSON translatable field with checkbox toggleboot(), observers, timestamps, and lifecycle hooks via individual save()composer require waad/filament-import-wizard
# 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
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(),
];
}
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
Use the wizard outside of Filament panels via Livewire:
@livewire('filament-import-wizard', ['modelClass' => \App\Models\Product::class])
// 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)
];
| 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) |
Upload your CSV or Excel file. Supported formats:
.csv) — with UTF-8 BOM auto-detection.xlsx, .xls) — trailing empty columns are automatically trimmedThe mapping step presents model fields first, with a clean table-based layout:
BelongsTo relation is shown with its FK/PK badges and a searchable field dropdownFor 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": "مرحبا"}.
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.
Preview your data before import:
Start the import process:
save(), firing boot(), events, and observersupsert() with fallback to chunked inserts on failurecompleted_with_errors statusLink 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 resolutionname field, or auto-detect a suitable string columnwhereJsonContains() for JSON fieldsUpdate existing records instead of creating duplicates:
ImportWizardAction::make()
->forModel(\App\Models\User::class)
->enableUpsert(true)
->upsertKeys(['email']); // Match users by email
The wizard will:
upsert() (with timestamp injection)The wizard automatically detects:
Spatie\Translatable\HasTranslations traitCheck 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"}
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)),
};
}
}
The wizard handles models with $guarded = [] (no fillable defined) gracefully:
Schema::getColumnListing() when getFillable() is emptyuse Filament\Support\Enums\Width;
ImportWizardAction::make()
->forModel(\App\Models\Product::class)
->setModalWidth(Width::ExtraLarge);
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');
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.
The MIT License (MIT). Please see License File for more information.
Contributions are welcome! Please open an issue or submit a pull request.
If you discover any bugs or have feature requests, please open an issue on GitHub.

How can I help you explore Laravel packages today?