waad/filament-import-wizard
Queue-powered 4-step CSV and Excel import wizard for Filament with smart column mapping, relationship linking, Spatie Translatable and locale merge support, dynamic schema validation, upsert, and live progress tracking for 100K+ row imports.
Install and publish what the wizard needs:
composer require waad/filament-import-wizard
php artisan vendor:publish --tag="filament-import-wizard-config"
php artisan vendor:publish --tag="filament-import-wizard-migrations"
php artisan migrate
If Filament styles break after installation:
php artisan filament:assets
Look first at these package surfaces:
Waad\FilamentImportWizard\Actions\ImportWizardAction — the main Filament table/header action.config/filament-import-wizard.php — modal width, chunk size, CSV delimiter, queue connection, queue name.The fastest first use case is a Filament resource list page with a simple CSV import:
use Waad\FilamentImportWizard\Actions\ImportWizardAction;
protected function getHeaderActions(): array
{
return [
ImportWizardAction::make()
->forModel(\App\Models\Product::class),
];
}
Then open the resource list, click the import action, upload a CSV, map name, sku, price, review, and run the import. For day-to-day work, treat the wizard as a replacement for hand-written CSV import screens, not as a data migration tool.
Before relying on it in production, verify:
The normal pattern is to attach ImportWizardAction to a Filament resource page:
use Waad\FilamentImportWizard\Actions\ImportWizardAction;
protected function getHeaderActions(): array
{
return [
ImportWizardAction::make()
->forModel(\App\Models\Product::class)
->chunkSize(500)
->queueConnection('redis')
->queueName('imports'),
];
}
Use per-action queue settings when imports are expensive and should not compete with default jobs. Use the config defaults for consistency across the panel.
Instead of scattering validation logic across resources, define import-specific behavior on the model:
class Product extends Model
{
public function getImportRules(): array
{
return [
'sku' => ['required', 'string', 'max:50'],
'price' => ['required', 'numeric', 'min:0'],
'stock' => ['nullable', 'integer', 'min:0'],
];
}
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)),
};
}
}
This keeps import rules close to the model and makes the mapping step more understandable for non-technical users.
For supplier feeds, nightly syncs, or any file that may contain existing records, configure upsert explicitly:
ImportWizardAction::make()
->forModel(\App\Models\User::class)
->enableUpsert(true)
->upsertKeys(['email']);
Pick stable natural keys such as email, sku, or an external ID. Avoid using nullable or mutable columns as upsert keys.
For BelongsTo relations, map the CSV column to the relation field rather than the foreign key directly:
CSV Column: "Category Name" → Relation: category → Field: name
The wizard resolves numeric values against auto-incrementing IDs and string values against suitable string columns. Name your CSV columns clearly, and prefer a stable related field such as name or code. If relation resolution is ambiguous, provide an explicit related model field in the mapping step.
For Spatie Translatable models or JSON columns, enable the merge translation toggle on the translatable field:
| CSV Header | Maps To |
|---|---|
title_en |
title with locale en |
title_ar |
title with locale ar |
The stored value becomes:
{"en": "Hello", "ar": "مرحبا"}
This is useful for multilingual product/category imports. Keep locale suffixes consistent (_en, _ar) so auto-detection works.
Outside a Filament panel:
@livewire('filament-import-wizard', ['modelClass' => \App\Models\Product::class])
Use this only if you need the wizard in a non-Filament admin page. Otherwise, stay with the Filament action so you inherit panel styling, auth, and tenant scoping.
The package advertises tenant scoping across queue boundaries, but your application still owns tenant context:
tenant_id/team_id from the mapping or model defaults.Build a small pre-flight process for operational imports:
For 100K+ row files, keep chunk_size moderate (500–1000) and ensure the queue worker timeout and memory limits are sufficient.
If the queue connection is sync, imports run immediately and can timeout or exhaust memory. For real imports, configure a real queue connection and run workers:
php artisan queue:work redis --queue=imports
Keep the queue name in config and in the worker command aligned.
The wizard tracks import state, progress, and errors. If the migration is not published and run, imports may fail or progress will not be available. Always run:
php artisan vendor:publish --tag="filament-import-wizard-migrations"
php artisan migrate
The package handles $guarded = [] by falling back to schema columns, but this can mask unintended mass assignment. For production models:
$fillable.getImportRules() to constrain values.String values for an auto-incrementing relation may be matched against a suitable string column and existing records may be reused. Numeric values are matched by ID. This is convenient, but it can also link to the wrong record if names are not unique.
For critical relations:
Without getImportRules(), rules are inferred from database column types, nullability, and uniqueness. That is a good default, but it may miss domain rules such as:
Add getImportRules() whenever business rules matter.
Non-upsert imports save records individually as models, firing model events, observers, and timestamps. Upsert mode uses bulk upsert() and may not fire the same Eloquent lifecycle events per row.
If your application depends on observers for search indexing, notifications, or audit logs, decide whether upsert is appropriate or whether you need a post-import reconciliation job.
The mapping step auto-matches headers such as Title → title and CategoryId → category_id, but always review the mapping for:
name on both the model and a relation.Validation errors are shown per row and can be downloaded as CSV. Treat that file as the correction artifact:
The review step can catch many validation errors, but database-level unique constraints may still fail during processing. If imports complete with errors, inspect both the wizard’s error output and application logs.
A common failure mode is a successful local import using sync and a broken production import using Redis/database queues. Test with:
If the modal renders incorrectly or styles are missing, run:
php artisan filament:assets
This is especially relevant after upgrading Filament or changing package assets.
The package is early-stage and has a small public footprint. Before standardizing on it:
composer.json.Use these model methods and action methods before forking the package:
getImportRules() — custom validation.getImportFieldLabel() — nicer mapping labels.forModel() — explicit target model.chunkSize() — tune for file size and worker memory.enableUpsert() / upsertKeys() — idempotent imports.queueConnection() / queueName() — isolate import workloads.setModalWidth() — adjust UX for wide mapping tables.If the package’s field derivation or relation resolution does not fit your domain, prefer model-level rules and clean CSV contracts over patching package internals.
How can I help you explore Laravel packages today?