Table
Table Imports
A CSV read back into the model: headers mapped, cells cast and validated per column, and failures collected rather than fatal.
On this page
Wire Table can import rows from an uploaded CSV file into the table's model — the mirror of Exports. Columns map file headers to model attributes, each cell is cast and validated per column, and per-row validation failures are collected instead of aborting the run.
Declare the Importer
Attach an ImportAction with a TableImport config to the table's header actions:
use NyonCode\WireTable\Import\ImportAction;use NyonCode\WireTable\Import\ImportColumn;use NyonCode\WireTable\Import\TableImport; public function table(Table $table): Table{ return $table ->model(Contact::class) ->columns([...]) ->headerActions([ ImportAction::makeImport() ->importConfig( TableImport::make() ->model(Contact::class) ->columns([ ImportColumn::make('name') ->requiredMapping() ->rules(['required']), ImportColumn::make('email') ->rules(['nullable', 'email']) ->guess(['e-mail', 'mail']), ImportColumn::make('age') ->castStateUsing(fn ($value) => (int) $value), ]) ), ]);}
Run the Import
The host seam is WithTable::importTable(string $filePath): ImportResult. The application wires the file-upload UI to it — typically a Livewire upload whose temp file's real path is passed in:
use Livewire\WithFileUploads; class Contacts extends Component{ use WithTable; use WithFileUploads; public $importFile = null; public function runImport(): void { $this->validate(['importFile' => 'required|file|mimes:csv,txt']); $result = $this->importTable($this->importFile->getRealPath()); // e.g. notify: "{$result->getImported()} imported, {$result->getFailedCount()} skipped" }}
importTable() resolves the ImportAction config from the table's header actions (mirroring exportTable()) and invalidates cached records so the next render shows the new rows.
Header Mapping
A column matches a file header by its label, its attribute name, or any guess() alias — case-insensitive and trimmed. Mapping is resolved once from the header row.
requiredMapping()marks a header the file must contain; a missing one throws aRuntimeExceptionbefore any row is processed.- Unmapped optional columns are simply skipped for every row.
Per-Row Validation
rules() validate each mapped cell. A failing row is skipped and recorded — the run continues:
$result = $this->importTable($path); $result->getImported(); // rows persisted$result->getFailedCount(); // rows skipped by validation$result->hasFailures();$result->getFailures(); // [['row' => 3, 'errors' => ['The Email field must be…']], …]
Update or Create
Match existing records instead of always creating:
TableImport::make() ->model(Contact::class) ->columns([...]) ->updateExisting(['email']) // updateOrCreate keyed by email
Every updateExisting() attribute must be fed by a mapped file column — an unmapped match attribute fails the whole run up front (otherwise an empty match-key set would silently overwrite unrelated records).
Custom Persistence
Take over persistence entirely with createUsing() (no model() needed):
TableImport::make() ->columns([...]) ->createUsing(function (array $data) { Contact::firstOrNew(['email' => $data['email']])->fill($data)->save(); })
CSV Options
TableImport::make() ->delimiter(';') ->enclosure('"')
The importer handles a UTF-8 BOM, blank lines, and rows with fewer/more cells than the header (missing cells become empty strings, extras are dropped). CSV is the only supported format (like Filament's importer).
Methods
| Method | On | Description |
|---|---|---|
model(string) |
TableImport |
Target Eloquent model |
columns(array) |
TableImport |
ImportColumn list |
delimiter(string) / enclosure(string) |
TableImport |
CSV parsing options |
updateExisting(array) |
TableImport |
updateOrCreate match attributes |
createUsing(Closure) |
TableImport |
Custom per-row persistence handler |
label(string|Closure) |
ImportColumn |
Header label (defaults to a headline of the name) |
requiredMapping() |
ImportColumn |
The file must contain this column |
rules(array) |
ImportColumn |
Per-cell validation rules |
castStateUsing(Closure) |
ImportColumn |
Transform the raw cell value |
guess(array) |
ImportColumn |
Alternative header names |
importTable(string) |
host | Run the import now, from a real path |
queueTableImport(string, ?string) |
host | Run it on a worker, from a disk path |
Queue a Large Import
An import was already path-in, result-out, so nothing in the import pipeline changes when it moves to a queue. What a job adds is the three things it cannot borrow from a request: a file that outlives it, a result with somewhere to go, and a failure that is visible.
public function importInBackground(): void{ $path = $this->file->store('imports', 's3'); $this->queueTableImport($path, 's3'); }
Store the upload first and pass what that returns. queueTableImport() takes
a disk path, not the temp upload's real path: the worker is entitled to be a
different machine, and a Livewire temporary file will not be there when it looks.
The result arrives as a notification — the imported and failed counts — because a queued import has no return value. A run that rejected rows notifies as a warning, not a success: an import that silently dropped a row is the kind of success worth being told about.
A missing file fails the job. The CSV reader treats an unreadable path as "no
rows", which is right for a run the user is watching and a lie for a queued one:
"imported 0 row(s), 0 failed" is indistinguishable from an empty file. A worker
that cannot find the upload throws ImportException and retries.
Adjusting an import you do not own
A table shipped by an installed module declares its own
mapping, and an application adds to it through the
import.configuring hook rather than by replacing the
class:
$manager->hook(Hook::ImportConfiguring, function (ImportConfiguringPayload $payload) { $payload->columns = [...$payload->columns, ImportColumn::make('imported_by')]; $payload->import->updateExisting(['email']); return $payload;}, for: 'users');
It runs once per import, whichever way it is delivered — a queued import
re-enters through the same importTable() — and after the ImportAction's
authorization check, so $payload->path is a file the action has already agreed
to open. The path is read-only for that reason.