anaseqal/nova-import
A Laravel Nova package for importing CSV and Excel files into Nova resources. It adds an import action with column mapping, validation, and customizable import logic, making bulk data entry from spreadsheets easier directly inside your Nova admin panel.
Check the version wall before you invest an hour. This package ships its last release in August 2020 and is built for Nova 3 / Laravel 6–8 / PHP 7.2–7.4, sitting on maatwebsite/excel ^3.1. On Nova 4/5 the compiled Vue assets won't mount and the toolchain mismatch will surface only after nova:publish. Treat that as a fork-and-reimplement signal, not a bug to debug.
On a compatible Nova 3 install the setup is quick:
composer require anaseqal/nova-import
php artisan vendor:publish --provider="Anaseqal\NovaImport\ToolServiceProvider"
php artisan nova:publish
Register the tool in app/Providers/NovaServiceProvider.php:
public function tools()
{
return [
new \Anaseqal\NovaImport\NovaImport(),
];
}
Read the source in this order before writing any glue code:
src/ — the tool class plus the upload/import controller. This is the documentation: it shows the exact file → parse → map → validate → Model::create() flow.resources/js/ — the Vue modal that posts the column mapping. Read it to learn the payload contract, because that's what you'll extend or intercept.composer.json — the exact pins it forces on Nova and maatwebsite/excel. Run composer why maatwebsite/excel before installing if you already have export/reporting code.config/ — if a config file is published, that's where disk, queue, and validation defaults live.First task: import one boring reference table. Pick a flat, single-table model (countries, carrier list, product categories) with 40–100 rows of scalar columns, import into a scratch DB, and diff the result against the source row by row. This surfaces header matching, type casting, date handling, and silent row-skips in under an hour. Do not start with a table that matters.
Gate the tool, don't just rely on Nova auth. The upload endpoint is authenticated but not otherwise authorized, so canSee() is your primary lever:
(new NovaImport())->canSee(fn ($request) => $request->user()->hasRole('ops-admin'))
Pair it with a policy on the target model. If ops asks "who can import what?", the answer must live in code, not in a Nova permission you'll forget to audit.
Curate the mapping field list explicitly. Don't let the mapping UI infer from $model->getFillable() or the schema. Enumerate accepted columns in a config array, keep it in git, and version it alongside validation rules. When someone adds a model column, the importer must not silently start accepting it — that's how unvalidated data lands in production.
// config/imports.php
'products' => [
'fields' => ['sku', 'name', 'price', 'category_id'],
'rules' => [
'sku' => 'required|string|max:64',
'name' => 'required|string|max:255',
'price' => 'required|numeric|min:0',
'category_id' => 'required|exists:categories,id',
],
],
Tag every import with a batch ID. The package's per-row validation flashes errors to the browser and discards them; "why didn't row 47 import?" becomes unanswerable. Wrap the import and persist a batch reference on the target table (or a dedicated import_batches table):
DB::transaction(function () use ($rows, $batchId, $rules) {
foreach ($rows as $row) {
$validated = Validator::make($row, $rules)->validate();
Product::create([...$validated, 'import_batch' => $batchId]);
}
});
That single column buys you rollback (DELETE WHERE import_batch = ?), an audit trail, and a way to reason about half-finished runs. On a package with no maintainer, this is the highest-leverage addition you can make.
Choose insert-only vs. upsert up front. The package has no dedupe semantics — re-running a file creates duplicates or trips unique constraints. Decide now:
updateOrCreate in your own post-parse callback.Do transformations in a post-validation callback, not model mutators. Slugs, phone normalisation, "category by name → category_id", default fills for blank columns — put these in the import pipeline. A mutator fires on every write path and you'll spend a Friday debugging why an admin edit changed a slug unexpectedly.
Queue anything over a few hundred rows. The package advertises chunked processing and queue support, but on a five-year-old codebase verify against your driver before trusting it. If the built-in path is shaky, the reliable pattern is:
Test with checked-in fixtures. A 20-row CSV in tests/Fixtures/ plus a feature test asserting row count and two field values is cheap and will catch every Nova/Excel version bump. Add a second fixture of deliberately bad rows and assert the count of rejected rows.
maatwebsite/excel is the conflict magnet. If you already export or report via Excel, expect a negotiation you may lose. Check composer why maatwebsite/excel before and after..xlsx may arrive as 44197 instead of 2021-01-01. Handle in the transformation step or normalise to CSV as an input requirement.$fillable, the row imports "successfully" minus that data. Run your first import against $guarded = [] in a scratch environment to prove the pipeline, then tighten.max_execution_time, memory_limit, and post_max_size stop you in the low thousands, well before the DB notices. Enforce a row-count cap server-side in the handler — a Vue-side check is bypassable.resources/js/ before extending it.How can I help you explore Laravel packages today?