nova-kit/nova-queued-export-as-csv
Adds queued CSV exports to Laravel Nova resources, so large datasets export asynchronously through background jobs instead of timing out or blocking the UI. Useful for admin panels needing reliable, scalable data exports.
Install and wire it up once, then treat it as a standard Nova action.
composer require nova-kit/nova-queued-export-as-csv
php artisan vendor:publish --tag=nova-queued-export-as-csv-config
The package pulls in maatwebsite/excel transitively and assumes you have a real queue worker (QUEUE_CONNECTION=database|redis, not sync) plus a writable filesystem disk. Publish the config first — it is where disk, queue, and the notification class live, and you will want to touch it on almost every project.
Where to look first, in order:
config/nova-queued-export-as-csv.php — disk, queue name, notification class. This answers 80% of "why didn't it work" questions.src/QueuedExportAsCsv.php — the action you actually put in actions(). Read the public setters (columns(), filename(), etc.) before you invent your own.src/Jobs/* — what runs on the worker. This is where chunking, querying, and file writing happen.src/Notifications/* — the "your export is ready" class, usually with a link or file path.README.md — the action API changes between releases; pin a version and read the README for that tag.First use case — prove the loop on one resource with 10 rows:
// app/Nova/User.php
use NovaKit\NovaQueuedExportAsCsv\QueuedExportAsCsv;
public function actions(NovaRequest $request): array
{
return [
...parent::actions($request),
new QueuedExportAsCsv,
];
}
Then run php artisan queue:work, hit the action from the Nova index, and verify three things in this order: (1) the job appears in jobs and is consumed, (2) the CSV lands on the configured disk, (3) the Nova notification appears in the bell menu. If any of those three is missing, you know which layer to debug.
Gate it like any other action, not with if inside the job.
(new QueuedExportAsCsv)
->canSee(fn (NovaRequest $request) => $request->user()->can('export', User::class))
->canRun(fn (NovaRequest $request, User $user) => $request->user()->can('export', $user));
The queued job runs outside the request, so canRun is your last line of authorization before dispatch. Do not rely on the resource's indexQuery scoping alone.
Explicit columns beat inferred columns. When the package derives columns from the resource's index fields, any non-database field (accessor, relationship, computed column) either fails the query or comes out empty. Map explicitly and keep the list aligned with what the resource actually exposes:
(new QueuedExportAsCsv)
->columns(['id', 'name', 'email', 'created_at'])
->filename('users-'.now()->format('Ymd-His').'.csv');
(Setter names follow the current release; confirm against src/QueuedExportAsCsv.php for your pinned version.)
Reuse via a trait or a shared base action. In a real app you'll attach this to 5–10 resources. Put the policy check, filename convention, and column mapping in one place:
trait QueuedCsvExportAction
{
protected function csvExportAction(): QueuedExportAsCsv
{
return (new QueuedExportAsCsv)
->canSee(fn ($request) => $request->user()->can('exportCsv', static::class))
->filename(class_basename(static::class).'-'.now()->format('Ymd-His').'.csv');
}
}
Push exports to a dedicated queue. CSV generation for 100k rows will occupy a worker for minutes. Set the queue in config to exports and run a separate supervisor pool so an export never blocks password-reset emails.
Respect Nova filters, not just the visible page. Nova passes the current filters on the request; if the action only serializes $models, you export the current page rather than the filtered set. For "export everything matching the filter" behaviour, rebuild the query from the request payload inside the job (resource class + filters + search) rather than passing an Eloquent collection through the queue.
Test the job in isolation. Faking the queue only proves dispatch happened; it does not prove the CSV is correct.
Queue::fake();
// hit the action / call it directly
Queue::assertPushed(ExportAsCsvJob::class);
// separately, with Storage::fake('local') and a small factory set:
(new ExportAsCsvJob(...))->handle();
Storage::disk('local')->assertExists($expectedPath);
If the package dispatches a Laravel Excel export class, you can also subclass that export and bind your version in the container — that is the cleanest extension point when you need custom formatting, date handling, or chunking.
Notification as the UX. The action's value is "fire and forget." Make sure the notification's via() includes database so it lands in Nova's notification centre, and that any download link is either a temporaryUrl() (S3) or served through a signed route (local disk). Never expose storage/app directly.
QUEUE_CONNECTION=sync silently defeats the entire package. The export runs in the request, times out on large datasets, and the "queued" notification fires immediately. This is the #1 local-vs-prod surprise.jobs table / Horizon before blaming the package.null rows.FromCollection or a full get(), PhpSpreadsheet-style memory behaviour still applies. Use FromQuery with chunk()/cursor(), and sanity-check with 50k rows before shipping.indexQuery, which means trashed records are excluded unless the user selected a trashed filter. Verify this is what finance actually wants before the first "missing rows" ticket.=, +, -, or @ can execute in Excel. Prefix with ' or a tab in a custom export class if users upload free-text fields.\xEF\xBB\xBF) when the audience opens CSVs in Excel on Windows. Non-issue for Google Sheets / pandas.local disk files aren't web-accessible; public disk needs php artisan storage:link. S3 needs temporaryUrl support and correct bucket policy.disk or queue in the config, php artisan config:cache (or clear it) — stale cached config is a common "I changed it and nothing happened" moment.$timeout below your worker's --timeout, and retry_after above it. A failed export with no notification is indistinguishable from a slow one to the end user; add a failure notification if the package doesn't ship one.php artisan queue:failed, storage/logs/laravel.log, Horizon's failed tab, and drop the queue connection to sync locally only to get a stack trace in the Nova UI.composer.json, review the diff before each Nova major upgrade, and be prepared to vendor the ~5 files into your app if it stalls. The surface area is small enough that forking is a realistic escape hatch.How can I help you explore Laravel packages today?