maatwebsite/laravel-nova-excel
Export Laravel Nova resources, lenses, and filtered selections directly to Excel or CSV with automatic chunking, queued exports, and filter-aware queries for fast, scalable exports.
1. Pick the branch that matches your stack before anything else. This package is a thin Nova action wrapper around maatwebsite/excel, so its constraints are really the constraints of two other packages. Rough matrix:
| Package branch | Nova | laravel-excel | Notes |
|---|---|---|---|
^1.3 |
Nova 4.x / 5.x | ^3.1 |
Current maintenance line for Nova 4/5; 1.3.11+ added Nova 5 support |
^2.0 |
Nova 5.x | ^4.0 |
Tracks laravel-excel v4; 2.0.2 fixed Nova 5 field-name casting |
composer require maatwebsite/laravel-nova-excel:^1.3
# or, if you are already on maatwebsite/excel v4
composer require maatwebsite/laravel-nova-excel:^2.0
Run composer why maatwebsite/excel first if you have other export code in the app — a v3/v4 mismatch is the single most common install failure.
2. Publish the laravel-excel config (only if you don't have it yet, which is likely in a Nova-only app):
php artisan vendor:publish --provider="Maatwebsite\Excel\ExcelServiceProvider" --tag=config
That gives you config/excel.php, which is where chunk size, temporary files and default disk live. The Nova package itself ships no config file — everything is configured per action instance.
3. First use case — the 30-second win. Add the action to one resource and ship it:
// app/Nova/User.php
use Maatwebsite\LaravelNovaExcel\Actions\DownloadExcel;
public function actions(Request $request)
{
return [
(new DownloadExcel)
->withFilename('users-' . now()->format('Y-m-d')),
];
}
Select rows in the index, click the action, get users-2026-08-20.xlsx. No new files, no registration, no assets to rebuild. If that doesn't work, the problem is version compatibility, not your code.
4. Where to look first when you need more than the README shows. The README is marketing; the real API is fluent methods on the action class. Read the installed source directly — vendor/maatwebsite/laravel-nova-excel/src/Actions/DownloadExcel.php and its traits — rather than blog posts, because the method set has drifted between 1.2/1.3/2.0. Then read docs.laravel-excel.com/nova/1.1 for the export-side concerns (mapping, headings, formatting), which the action simply delegates to.
Licensing note for your procurement checklist: MIT, but licensed as Postcardware. It's commercially safe; they just ask for a postcard if it reaches production.
Pattern 1 — Bare action on the resource (most common). The action exports the selection if there is one, otherwise the filtered query. That single behaviour covers "export these 12 rows" and "export everything matching my filters" without extra code. This is the pattern to reach for 80% of the time.
Pattern 2 — A named, preconfigured action class.
Once you start repeating ->withFilename(...)->only([...]), extract it. Subclassing keeps the resource file readable and gives you one place to change column sets:
namespace App\Nova\Actions;
use Maatwebsite\LaravelNovaExcel\Actions\DownloadExcel;
class ExportUsers extends DownloadExcel
{
public function __construct()
{
parent::__construct();
$this->withFilename('users-' . now()->format('Ymd-His'))
->withName(__('Export users')) // translatable label, 1.1.8+
->only(['id', 'name', 'email', 'created_at']);
}
}
only() also reorders the columns (added in 1.1.8), so this doubles as your column-order control. except() is the inverse; allFields() pulls in fields that aren't on the index view.
Pattern 3 — Guard it. Nova actions are visible to anyone who can view the resource, and an export can leak columns that the index deliberately hides. Gate everything non-trivial:
(new ExportUsers)
->canSee(fn ($request) => $request->user()->hasRole('admin'))
->canRun(fn ($request, $model) => $request->user()->can('export', $model))
->onlyOnIndex(),
onlyOnIndex() matters: exporting a single record from the detail view is almost never what the user meant, and it hides a confusing button.
Pattern 4 — Lens exports. Add the same action to the lens's actions() array; the package uses the lens query, so filters and aggregates defined on the lens carry through (aggregate/grouped lenses were fixed in 1.2.9, heading lookup for lenses in 1.2.11 / 1.3.6). This is the cleanest way to offer "the exact report I'm looking at, as a spreadsheet" — build the Lens, not a bespoke query.
Pattern 5 — Large exports: ExportToExcel, not DownloadExcel. A synchronous download has nowhere to put a 200k-row file except the HTTP response, which is exactly where timeouts and memory limits live. Use the disk-writing action instead:
use Maatwebsite\LaravelNovaExcel\Actions\ExportToExcel;
(new ExportToExcel)
->withFilename('users-' . now()->format('Ymd'))
->withDisk('s3')
->onQueue('exports')
->only(['id', 'name', 'email']);
This queues the job, writes to the configured disk, and notifies the user with a signed download link. Requirements: a running worker, and a disk every app server can read (S3, not local) — the 1.2.7 release was specifically about Vapor/multi-server setups.
Pattern 6 — Shape the output with laravel-excel concerns. The action delegates to a laravel-excel export, so the standard concerns are your formatting toolkit: WithMapping for computed/derived columns and relation labels, WithColumnFormatting for dates and numeric-as-text, WithHeadings/WithStyles/ShouldAutoSize for presentation. Anything you need that the fluent API doesn't expose, you override in the subclass.
Pattern 7 — Separate the data from the trigger. For exports that matter (finance, compliance, anything with an agreed column contract), don't bury the logic in a Nova action. Define App\Exports\UsersExport implements FromQuery, WithMapping, WithHeadings and let the Nova action be a thin trigger. You get a testable class, CLI reuse via php artisan/scheduler, and a sheet you can diff in CI. The Nova action is a UI affordance; the export is domain logic.
Pattern 8 — Kill N+1 before you ship. Exporting a BelongsTo gives you the foreign key, so the instinct is ->relation->name in a mapping — which fires a query per row. Override indexQuery() on the resource to with() the relations you export (the action builds on the resource/lens query, so those eager loads apply), or denormalise the label into a real column. A 5,000-row export that takes 90 seconds is almost always this.
Pin the version and re-test on every Nova upgrade. This is the defining operational risk of the package. Nova patch releases have repeatedly broken the action contract — 1.3.8 ("fix for Nova 4.35.0"), 1.3.12 ("Fix incompatibility with Nova 5.6.2"), 1.3.13 ("fix DownloadExcel action"). Symptoms are a broken action button, a handle() return-type error, or an "Action class mismatch" exception. Keep it out of the ^ float or at least add a smoke test that clicks the action, and check the releases feed before bumping Nova.
withFilename() appends the extension for you (since 1.2.3). Passing users.xlsx gets you users.xlsx.xlsx. Pass the basename only.
Queued exports and closures don't mix. Queued queries are serialised with laravie/serialize-queries; a lens filter or resource query containing a closure (->where(fn ($q) => ...)) can fail to serialise and the job dies with an opaque error. Keep queued-export queries to plain column/operator/value clauses, or resolve the IDs before dispatch.
"Select all" is not free. Nova sends the selected IDs in the request. Selecting 20,000 rows can blow past post_max_size/max_input_vars and fail with a 413/414 before PHP ever reaches your action. The workaround is a UX one: teach users to set a filter and click the action with nothing selected, which exports the filtered query via SQL rather than an ID list.
Watch the data types, not the code.
1.23457E+11 in XLSX. Format the column as text, or use withWriterType(Excel::CSV) for machine-consumed feeds.DateTime/Carbon and get Excel-localised. For CSV/hand-off files, format explicitly.withMapping, not in the model, if the model is used elsewhere.->asHtml(), fields with no underlying attribute) may export blank or throw, since there's no attribute key to resolve. The 2.0.2 fix (casting Nova 5 field names to string before using them as array keys) is a symptom of how fragile that name-based mapping is.Signed URLs are a config trap. ExportToExcel notifications link to a signed route. If APP_URL, proxy headers, or TrustProxies are wrong — common in Docker/Vapor/staging — the notification link 403s or points at localhost. Same class of bug as password-reset links; check it in staging, not in production.
Visibility ≠ exportability. only()/except()/allFields() operate on field attribute names, and allFields() deliberately includes fields not shown on the index. Combining that with an ungated action is how PII ends up in a download. Prefer an explicit only() list over allFields() for anything sensitive.
Chunking solves memory, not wall-clock time. Automatic chunking keeps peak memory flat, but a synchronous export can still hit max_execution_time on a large resource. There is no config knob that fixes that — the fix is ExportToExcel on a queue. Also size the job's $timeout and $tries deliberately; a retried export can leave duplicate files on the disk.
Order isn't guaranteed to survive. Selection-based exports rebuild a query from IDs, so the visual sort order in Nova may not carry through. If column order matters downstream, sort in the export query or a mapping.
Debugging recipe. Test the query first with ->withWriterType(Excel::CSV) — CSV is readable in the terminal and isolates "wrong data" from "wrong formatting". Then check the queue (--once), the configured disk path, and storage/logs/laravel.log for the underlying laravel-excel exception; the Nova action swallows most of it into a generic toast.
Extension points, in order of preference. 1) Fluent setters on the action. 2) Subclass DownloadExcel/ExportToExcel and configure in the constructor. 3) Implement laravel-excel concerns on the action class. 4) Replace the action with your own invoking a dedicated App\Exports class — the right move once the export has a contract anyone depends on. Don't patch the vendor package; the Nova upgrade churn will make you regret it.
How can I help you explore Laravel packages today?