simonhamp/laravel-nova-csv-import
A Laravel Nova package that adds CSV import support to your resources. Upload a CSV, map columns to model fields, validate rows, and import records directly from Nova with progress feedback and customizable import logic.
1. Install (Nova 4/5, PHP 8+)
composer require simonhamp/laravel-nova-csv-import
php artisan vendor:publish --provider="SimonHamp\LaravelNovaCsvImport\CsvImportServiceProvider"
The publish step drops config/csv-import.php. Read that file first — it is the only knob you get for disk/location/delimiter behaviour, and everything else is configured per-action.
2. Register the action on the resource you want to import into
// app/Nova/User.php
use SimonHamp\LaravelNovaCsvImport\CsvImport;
public function actions(NovaRequest $request): array
{
return array_merge(parent::actions($request), [
(new CsvImport())
->canSee(fn ($request) => $request->user()->isAdmin())
->canRun(fn ($request, $model) => $request->user()->isAdmin()),
]);
}
3. First use case
Go to the resource index → tick any row (Nova needs a selection to show the action menu) → Import CSV → upload a file → map CSV headers to Nova fields → run. The first row of the file is treated as the header row, so keep column names human-readable (Email, Company ID) and do the mapping in the modal rather than renaming headers to match DB columns.
Where to look next: the CsvImport action class itself (it is small — fields(), handle(), and the import loop), the compiled Vue components under resources/js, and the config. Because there are effectively zero downstream dependents, reading the source is cheaper than searching for answers. Note that from v0.8.0 the package ships a Workbench harness (PR #77) — if you want to contribute, run the package's own sandbox via composer test/Workbench rather than wiring a throwaway Nova app.
Scope the action, don't ship a global one. Attaching CsvImport to every resource means a support ticket every time someone imports a users.csv into Invoices. Either register it only on the resources that legitimately need bulk creation, or put it behind canSee plus a config flag so it can be turned off in production.
Map to Nova fields, not to DB columns. The package builds its mapping UI from the target resource's fields(). That means computed/onlyOnDetail fields, HasMany, and file fields are noise in the mapping dropdown. Trim the mapping surface by returning a reduced field set for the import action's target resource, or subclass the action and override the field list used for mapping.
Relationships: resolve keys before the import. BelongsTo columns are matched on the related model's key, not its display label. In practice you pre-process the CSV:
// users.csv has "Company" but invoices need company_id
$rows = collect($rows)->map(fn ($row) => [
...$row,
'Company' => Company::where('name', $row['Company'])->value('id'),
])->all();
If you do this a lot, wrap it in a subclassed action so the transformation lives in one place rather than in a spreadsheet macro that someone forgets to run.
Subclass for repeatable imports. The highest-leverage pattern for day-to-day work is a thin subclass per import shape:
class ImportCustomers extends CsvImport
{
public function name() { return 'Import Customers'; }
public function handle(ActionFields $fields, Collection $models)
{
// custom validation, then delegate
return parent::handle($fields, $models);
}
}
This is also where you set defaults the package can't know about — created_by, tenant IDs from $request->user(), a status column, or normalising $1,234.56 → 1234.56, dates from d/m/Y, and Yes/No → 1/0.
Attribute ownership explicitly. Actions run with the authenticated user's context but the importer will not stamp audit columns for you. Anything like team_id, created_by, or source = 'csv' has to come from the action's fields() or a fillUsing hook on the Nova field.
Chunk large files. The default path is synchronous inside the action request. Files over a few thousand rows will hit max_execution_time and leave a half-imported table. Either validate row counts up front and reject oversized files, or move the parsing into a queued job and keep the Nova action as the file-upload/triage step.
Tests belong at the importer layer, not the Nova layer. Keep fixture CSVs in tests/Fixtures, call the import logic directly, and assert on the database. Asserting through the Nova action modal is brittle and slow.
\uFEFFEmail) and the mapping silently fails. Strip it on ingest: ltrim($firstLine, "\xEF\xBB\xBF").;. Check config/csv-import.php for the delimiter setting before debugging "column count mismatch" errors.rules() from your resource fields are enforced row-by-row. Add explicit validation in the action (or a FormRequest-style check per row) and fail fast with a readable line number.QueryException and no rollback. Wrap the import in a transaction, or pre-validate the whole file against unique rules and reject it before writing anything.null. Excel blanks arrive as ''. This breaks nullable unique indexes and date casts. Normalise to null in the action before the model is filled./storage/app/csv-import grows forever with personally identifiable data in it.composer.json ("simonhamp/laravel-nova-csv-import": "0.8.*") and read the changelog on bump — with near-zero dependents, you are the QA.canSee is not canRun. The action menu is client-side; set both. Importing is a write operation on arbitrary volume and deserves the same authorisation strictness as a mass-assignment endpoint.fields() returning something unexpected for the current request — dump the field set in the action before chasing the Vue side.fields() to add import options (skip-first-row, update-existing, dry-run), override handle() to wrap in a transaction and custom validation, and override name() so multiple importers on one resource are distinguishable in the modal.fields().How can I help you explore Laravel packages today?