hayderhatem/filament-excel-import
A Filament plugin for importing Excel and CSV files with a guided, multi-step flow: upload, map columns to model fields, preview, then import. Includes validation, chunked processing, and progress feedback for large files.
Install and pin. The package is a thin trait, so the install is one command, but pin the minor version — with only 27 stars and no dependents, there is no downstream ecosystem absorbing breaking changes for you.
composer require hayderhatem/filament-excel-import
Check composer.json after install to confirm which Filament major and which spreadsheet engine it pulled in (PhpSpreadsheet vs. maatwebsite/excel). That single fact determines most of the gotchas you'll hit later, so resolve it before writing any import code.
Where to look first. In this order:
vendor/hayderhatem/filament-excel-import/src/Traits/ — the trait itself is the entire API surface. Read it end to end; it's small, and the hook method names (column definitions, row transformation, model resolution) are what you'll actually be extending.php artisan vendor:publish --tag=filament-excel-import-config (verify the tag in the service provider) to see whether sheet name, header offset, and disk are configurable or hardcoded.First use case. Import a flat list of records — users, products, categories — into an existing Filament resource. Add the trait to your ListRecords page:
use HayderHatem\FilamentExcelImport\Traits\FilamentExcelImport;
class ListUsers extends ListRecords
{
use FilamentExcelImport;
}
That should surface an import action in the page header. Seed a three-row .xlsx whose header row matches your model's column names exactly, upload it, and confirm the rows land. Do this before touching hooks — it establishes the baseline column-mapping behaviour (snake_case vs. labels, header row index) that everything else builds on.
Trait-on-list-page is the default shape. Because the trait attaches to a Filament page rather than a resource, you can scope imports per page: put it only on the pages where importing makes sense, and use separate list pages or custom pages when different imports need different column sets or different authorization.
Define an explicit column contract. Don't rely on the spreadsheet's header row matching your schema — one renamed column in a user's file silently drops data. Enumerate the accepted columns and their required/optional status in the trait's hook, and reject or warn on unknown headers. If the trait doesn't expose a hook for this, override the action's form to require a template download, or pre-validate the header row yourself before delegating.
Transform rows before persistence. Real spreadsheets are not database rows. Use the row-transform hook for:
"true"/"false"/"1"/"0" to booleans.country => id) rather than expecting users to know IDs — cache the lookup map outside the loop.created_by, imported_at, tenant ID).Handle failures per row, not per file. Wrap each row's save in its own try/catch and collect failures into a report you can show in a Filament notification: "412 imported, 8 skipped (3 invalid email, 5 unknown category)". A single DB::transaction() around the whole file means one bad row rejects 500 good ones — fine for a 10-row config import, wrong for operational data.
Gate it. The trait adds a UI action; it does not necessarily consult your create policy. Add an explicit ->visible(fn () => auth()->user()->can('import', User::class)) (or a Gate check in the page's mount) so import isn't a policy bypass for users who can view but not create.
Test with a committed fixture. Keep a small .xlsx in tests/Fixtures/ and write a feature test that hits the import action with UploadedFile::fake() or Storage::fake() + Livewire::test(ListUsers::class)->callAction(...). This is the only thing that will catch a Filament minor upgrade breaking the upload component, and given the package's adoption level, you will be the one who notices.
Excel serial dates. Cells formatted as dates may arrive as integers (45292) depending on the reader and cell format. Never trust the raw value; either detect numeric date cells and convert with PhpOffice\PhpSpreadsheet\Shared\Date::excelToDateTimeObject(), or make the accepted format explicit in a downloadable template and validate against it.
Numeric strings lose their leading zeros. Phone numbers, ZIP codes, SKUs, and employee IDs become 7911123456 or 1234. There is no fix inside the importer — you must instruct users to format those columns as Text, or accept the corruption and normalise downstream. Call this out in the UI next to the upload field.
Header row drift. Extra blank rows above the header, a title row ("User Export — March"), or trailing empty rows from a deleted range will break naive parsing. Trim to the last non-empty row and validate the header position explicitly rather than assuming row 1.
Memory and file size. If the package reads the whole workbook into memory (typical for PhpSpreadsheet-based importers), a 20k-row file can exhaust PHP's limit. Check whether chunked/streamed reading is available; if not, cap upload size in the action's ->maxSize() / ->acceptedFileTypes() and plan a queued custom importer for bulk loads.
Filament version coupling. Filament moves fast across majors; action APIs, notification signatures, and form component names change. Pin both filament/filament and this package, and add an upgrade note to run the fixture test first. The package's release cadence is not a signal of compatibility — verify against your installed Filament version's changelog.
Silent no-ops. If the trait's model resolution or column mapping can't find a match, some implementations skip the row without an error. Log every skipped row with its line number and reason, or you'll debug "why did only 300 of 310 rows import" from scratch.
Extension points to prefer. In rough order of safety: (1) row-transform hooks exposed by the trait, (2) a subclassed list page that overrides the action, (3) forking the trait into your app. Avoid editing vendor code — with this few dependents, the maintainer has no signal that a change breaks anyone, so patches can be aggressive.
Sanity-check the transaction boundary. Verify whether the package wraps the import in a transaction by reading the trait. If it doesn't, and you need atomicity, wrap the call yourself — but only for small, all-or-nothing imports.
How can I help you explore Laravel packages today?