Import and export · Open source
XLSX Import
A drop-in replacement for Filament's ImportAction that also reads Excel (.xlsx) and OpenDocument (.ods) workbooks, with a sheet picker, through the Importer classes you already have.
What it solves
Filament's ImportAction reads CSV files only.
Users who keep their data in Excel have to save every sheet as CSV before they can import it, and an old .xls file fails with nothing more than a file-type error.
XlsxImportAction extends Filament's ImportAction and replaces only the part that reads the uploaded file. An .xlsx or .ods workbook is read with openspout and its rows reach your Importer as the same header => string maps a CSV would give; a .csv file still goes through core's own reader. Everything after that is core's code: your Importer class, the queued jobs, the completion notification and the failed-rows download. A workbook with several visible sheets gets a Sheet select that starts on the sheet that best matches the Importer. Dates, numbers, booleans and formulas arrive as CSV-style text. openspout/openspout is already required by every Filament 4 and 5 release of filament/actions, so in a Filament app the plugin installs no other package. Old .xls files are not supported. An independent plugin, not made or endorsed by Filament.
- .xlsx, .ods and .csv in one upload field; CSV files still go through core's own reader
- A Sheet select for workbooks with more than one visible sheet, starting on the sheet whose headings match the Importer best and re-guessing the column mapping when another sheet is picked; hidden sheets are not offered
- sheet() always reads one sheet, by name or index from 0, and hides the picker; sheetPicker(false) hides the picker and reads the best-matching sheet
- Cell values as a CSV would give them: dates as Y-m-d (configurable), whole numbers without decimals, decimals to 15 significant digits, long numbers without scientific notation, TRUE/FALSE as 1/0, formulas as their saved value, error cells empty
- Empty rows skipped, columns without a heading ignored, repeated headings numbered as Name, Name (2)
- The workbook is streamed by openspout, and at submit its rows are read in one pass; reading stops as soon as maxRows() is exceeded
- timeLimit() (300 seconds by default) raises a lower PHP time limit while a workbook is read
- "Download example .xlsx file" built from the Importer's columns and examples; exampleFormat('csv') gives a CSV example like core's
- A message that says how to resave an old .xls file as .xlsx, and one for files that are not valid workbooks
- Your Importer classes, queued jobs, completion notifications and failed-rows download are core's own and keep working
Install
Requires PHP 8.2+, Filament 4 and 5, and Laravel 12 and 13.
composer require amjadiqbal/filament-xlsx-import
Usage
use AmjadIqbal\FilamentXlsxImport\Actions\XlsxImportAction;
use App\Filament\Imports\ProductImporter;
protected function getHeaderActions(): array
{
return [
XlsxImportAction::make()
->importer(ProductImporter::class),
];
}
Setup
There is no plugin to register on the panel: the service provider is discovered automatically and only loads the plugin's translations. Replace ImportAction with XlsxImportAction wherever you use it; the methods you already chain on ImportAction, such as importer(), chunkSize(), maxRows(), headerOffset() and fileRules(), still work because XlsxImportAction is a subclass. If you have not used Filament imports before, set them up as Filament's documentation describes first. The PHP zip extension is needed to read workbooks.
- 1 composer require amjadiqbal/filament-xlsx-import
- 2 Replace use Filament\Actions\ImportAction; with use AmjadIqbal\FilamentXlsxImport\Actions\XlsxImportAction;
- 3 Change ImportAction::make() to XlsxImportAction::make(), keeping ->importer(YourImporter::class) and any other options.
Accepted files
The upload field takes .xlsx, .ods and .csv (and .txt, as core does). The file's own extension decides how it is read: .xlsx and .ods go to openspout, anything else goes to core's CSV reader with its encoding detection, delimiter guessing and duplicate-heading check unchanged. openspout/openspout ^4.23 is a requirement of the plugin, and every Filament 4 and 5 release of filament/actions already requires the same version range, so it is normally installed already.
- ->acceptCsv(false) accepts workbooks only; ->acceptOds(false) stops accepting .ods. .xlsx is always accepted. The browser lets .xls files through in every case, so an .xls upload gets the message below rather than a file-type refusal.
- Old Excel 97-2003 .xls files are not supported, because openspout cannot read them. An .xls upload is refused with: "Old .xls workbooks cannot be read. Save the file as .xlsx (Excel: File > Save As > Excel Workbook) and upload it again."
- A file that cannot be opened as a workbook, such as a broken or renamed file, is refused with "This file could not be read as a spreadsheet. Check that it is a valid .xlsx, .ods or .csv file."
- Two openspout 4 bugs in .ods files are worked around by reading a corrected temporary copy: a FALSE cell no longer reads as true, and a row with no cells no longer crashes the reader.
The sheet picker
When a workbook has more than one visible sheet, a required Sheet select appears under the upload. On upload, the plugin reads the header row of every visible sheet and starts on the one where the most Importer columns can be guessed from the headings, so a cover or notes sheet is passed over. On a tie it keeps the sheet the workbook opens on. Picking another sheet re-guesses the column mapping from that sheet's headings. Hidden sheets are not listed. A workbook with one visible sheet shows no select.
- ->sheet('Products') or ->sheet(0) always reads that sheet, by name or by index from 0, and hides the picker. A workbook without that visible sheet is refused at upload with "The workbook has no sheet called "Products"."
- ->sheetPicker(false) hides the picker; the best-matching sheet, chosen as above, is read.
XlsxImportAction::make()
->importer(ProductImporter::class)
->sheet('Products');
Cell values
Every cell reaches the Importer as a string, the way the same column would arrive from a CSV file, so existing rules and casts keep working.
- Dates as Y-m-d, date-times as Y-m-d H:i:s and times as H:i:s. ->dateFormat(), ->dateTimeFormat() and ->timeFormat() take any PHP date format. A date typed as text in the spreadsheet arrives as typed.
- Whole numbers without a decimal point (1500, not 1500.0) and long numbers without scientific notation.
- Decimals to 15 significant digits, the precision Excel shows, so 0.1 + 0.2 arrives as 0.3, not 0.30000000000000004.
- TRUE and FALSE as 1 and 0.
- A formula as the value saved with it in the file, not the formula text. Error cells such as #N/A arrive empty.
XlsxImportAction::make()
->importer(ProductImporter::class)
->dateFormat('d/m/Y')
->dateTimeFormat('d/m/Y H:i')
->timeFormat('H:i');
Rows and headings
The heading row gives the column names, and the column mapping is guessed from it the same way core does for CSV, so ImportColumn::guess() works as before.
- A row with no value in any headed column is skipped, and does not count towards maxRows().
- A column with an empty heading is ignored.
- Repeated headings are numbered, ignoring case: Name, Name (2), Name (3). For CSV files core's duplicate-heading error still applies.
- headerOffset() counts from 0 over every row of the sheet, empty rows included, so ->headerOffset(2) reads the headings from spreadsheet row 3.
Large workbooks
openspout streams the sheet row by row, so the plugin never loads the whole workbook. When the import is submitted, the chosen sheet's rows are read in one pass and written to a temporary file on disk, which gives core the row count it needs before the rows are handed to core's ImportDispatcher in chunks. The file is read in the request that starts the import, as core does for CSV, so queue workers never open the upload.
- maxRows(): reading stops as soon as one row more than the limit has been read, and core's "too many rows" notification is shown.
- ->timeLimit(600) sets the seconds the request may run while it reads a workbook. The default is 300, applied only when PHP's max_execution_time is lower and not unlimited; null leaves it alone. A web server timeout still applies.
- Core's ImportDispatcher builds every queued job for the import in that request, for workbooks as for CSV, so the request's memory still grows with the number of rows.
- While columns are mapped, each change re-reads the sheet list and the header rows, not the data rows.
- The plugin sets no file size limit of its own; core's ->fileRules(['max:20480']) still applies.
The example file
The modal's link reads "Download example .xlsx file" and downloads a workbook built from the Importer's columns: a bold heading row with each column's example header, then the columns' example values. It takes core's example file name with an .xlsx extension. ->exampleFormat('csv') switches to core's "Download example CSV file" link and a CSV of the same rows, written by the plugin like core's.
What it replaces in core
XlsxImportAction is a subclass of Filament's ImportAction. It replaces the modal form (the upload field, the new Sheet select and the column mapping), the code that counts and reads the rows when the import is submitted, the file validation rules and the example-file download. The rest is core's, called as core calls it.
- Kept from core: the Import record, your Importer class and its validation, ImportDispatcher and the job batch, chunkSize(), queue and connection settings, the started and completed notifications, and the failed-rows CSV download from core's own route.
- Core's extensions:csv,txt rule is replaced by the plugin's extension check; rules added with fileRules() are kept.
Credits
Workbooks are read and the example file is written with openspout/openspout (MIT). XLSX Import is an independent plugin, not made or endorsed by Filament or the openspout project.
Hire me
Need this built into your app, or something custom?
I wrote XLSX Import. I can wire it into your Filament panel, extend it for your case, or build the feature around it. Tell me what you need and you'll get a plan and a quote, free.
Support
Found a bug or want a feature? Open an issue on GitHub. For help adding it to your own project, use the quote form above.
More Filament plugins
All plugins
RankForge Lite
Adds meta, Open Graph and X fields to any Filament resource, scores the content for SEO and readability as you type, and previews the Google result before you publish.
Change Requests
Holds edits to Filament records as change requests that a reviewer checks field by field, then approves, rejects or rolls back.
Deleted Records
Lists the records spatie/laravel-deleted-models kept when they were deleted, previews their stored attributes, and restores or permanently deletes them.
Stat Cards
Adds progress bars, icon tiles, card and text colours, trends, badges and bar charts to the stats overview widget you already have, by changing one import.
Uptime Monitor
Lists the sites spatie/laravel-uptime-monitor watches with their uptime and certificate status, and lets you add, edit, check, turn on and off and delete them from your panel.