romansulzhyk/filament-import
A Filament plugin for importing records from CSV and Excel files into Eloquent models, with configurable columns, validation, and chunked processing for reliable admin-side data imports.
filament-import gives you a Filament action that reads a spreadsheet/CSV file, maps rows onto your models through small Importer classes, and reports per-row failures back in the UI. As of v1.1.1 the package is explicitly CSV-first: delimiter and encoding are detected automatically, .tsv is a first-class format, and imports run synchronously (no queue) — which is great for feedback, but means file size is your responsibility.
First steps:
ExcelImportAction::make()->importer(ProductImporter::class) into a resource's header actions, or into a custom page.getColumns(). This is the file you will edit for 90% of your day-to-day work: it is where header aliases, casting, and validation live.Your first use case should be the smallest one: one model, three or four columns, a required name/sku, one numeric column. Confirm the happy path and the error rendering before adding relationships, beforeCreate hooks, or multi-sheet logic.
Keep importers thin and declarative. Define columns with their header aliases and let the package do the row matching:
public function getColumns(): array
{
return [
ImportColumn::make('sku')
->requiredMapping()
->rules(['required', 'string', 'max:64']),
ImportColumn::make('name')->requiredMapping(),
ImportColumn::make('price')->castStateUsing(fn ($state) => (float) str_replace(',', '.', $state)),
ImportColumn::make('category.name')->relationship(), // resolve/create via relationship
];
}
Anything that is not a straight column cast belongs in resolveRecord() or a beforeSave() callback, not in the column definition — column callbacks re-run per row and are easy to make non-idempotent.
Header aliasing is your compatibility layer. Users will send EAN, EAN Code, ean_code, and EAN-Code for the same field, across locales. Prefer declaring aliases on ImportColumn over normalising headers in a preprocessing step; the aliasing lives next to the column and shows up in the UI's column mapper, which is what actually saves support time.
Uploads from user-facing screens: extend, don't replace, file types. Since v1.1.1:
ExcelImportAction::make()
->importer(ProductImporter::class)
->acceptedFileTypes([
...ExcelImportAction::ACCEPTED_FILE_TYPES,
'application/octet-stream', // some ERP/browser combos label CSV this way
]);
acceptedFileTypes() replaces the defaults, so always spread the constant unless you deliberately want to drop a format. If your users upload from a system that serves CSVs with application/vnd.ms-excel, text/plain, or no type at all, add those rather than telling users to rename files.
Treat acceptance as UX, not security. The reader still decides format from file contents and extension, so an accepted MIME type never makes a binary file importable — and conversely, a rejected type is a client-side guard only. Validate the parsed data (rules on columns, unique constraints at the DB level) as if the file came from the internet, because it did.
Synchronous-by-design workflow. No queue means the user sees validation errors immediately, which is the right default for admin panels. Do not "fix" it by chunking the upload UI; instead:
->maxSize() / rules) to something your PHP max_execution_time and memory_limit can finish. A few thousand rows is usually fine; tens of thousands is a queue job you should write yourself by invoking the importer programmatically.Importer class so column mapping stays single-sourced.Testing. Test the importer, not the Livewire action. Construct the importer with a row array (or parse a fixture file) and assert records/errors; keep fixture files in tests/Fixtures with names that describe the quirk they encode: google-sheets-utf8-bom.csv, excel-de-semicolon.csv, erp-tabbed.tsv, no-extension-text-file.
acceptedFileTypes() is a whole-list replacement. This is the most likely source of a "worked before, now the upload is rejected" regression. Spread ExcelImportAction::ACCEPTED_FILE_TYPES and append..tsv is tab-separated CSV. Files without .csv/.tsv/.txt are read as CSV only when the first kilobyte looks like text. Practical consequence: a .dat/.xls-mislabelled file with a long preamble, leading blank lines, or binary junk in the first KB may be rejected or mis-sniffed. If a customer's export fails to sniff, ask for the file — the cheap fix is renaming to .csv/.txt, the proper fix is upstream.file -i, or hexdump -C | head) before changing importer code — the bug is nearly always the source file.; as delimiter and , as decimal separator. Detection handles the delimiter, not the number format: a price column arriving as 1.234,56 will cast to 1.234 silently. Cast explicitly and add a validation rule with a helpful message on the column.requiredMapping() is skipped and reported; the rest still import. That is usually desirable, but it makes partial imports easy to miss — always surface the failure summary to users, and make imports idempotent (updateOrCreate on a natural key) so a corrected re-upload does not duplicate rows.->relationship() with resolve/create can silently add new categories/brands. Decide deliberately between relationship() and a manual lookup that errors on unknown values; for catalogue imports, erroring is often the safer default.Importer class itself is the extension point: subclass it for shared behaviour (trimming, tenant scoping, audit logging in beforeSave) rather than repeating castStateUsing closures in every importer.How can I help you explore Laravel packages today?