K

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 a RuntimeException before 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.