joy/voyager-export
Export Voyager admin data to Excel or CSV in one click. joy/voyager-export adds BREAD-aware export actions to Laravel Voyager, letting you download any table, filtered list, or dataset straight from the admin panel.
Prerequisites and install
composer require joy/voyager-export
This is a Voyager module, so you need a working Voyager install first (tcg/voyager or the joy-voyager ecosystem it belongs to). Confirm the constraint in composer.json before running the command — the 2024-02 release targets modern Laravel, but the Voyager major version it pins against is the thing that will bite you.
Then publish the module assets/config and clear caches:
php artisan vendor:publish --provider="Joy\VoyagerExport\VoyagerExportServiceProvider"
php artisan optimize:clear
If the provider name differs in your version, just run the interactive publisher and look for the joy/voyager-export tag. If a migration ships with it (some Joy modules store export jobs/logs), run php artisan migrate as well.
Where to look first
config/voyager-export.php (or whatever the published file is named) — exporter driver, chunk size, disk, and which BREADs get an export button.vendor/joy/voyager-export/src/ — the service provider and the exporter class it registers. This package is small and thinly documented; reading the ~5 files is faster than searching for docs.voyager::bread.browse.First use case
Log into /admin, open any BREAD (e.g. Users or Posts), and look for an Export action on the browse screen. Clicking it should stream the table out as CSV (or XLSX, depending on the driver configured). That end-to-end click is your smoke test — it exercises provider boot, view injection, permission check, and the exporter in one shot.
Admin-facing export on a BREAD
The primary pattern is declarative: enable it per-BREAD via config, and the browse view renders the button without you touching controllers.
// config/voyager-export.php
return [
'enabled' => true,
'driver' => 'csv', // or 'xlsx' if a Laravel Excel adapter ships
'chunk_size' => 1000,
'breads' => [
'users' => ['columns' => ['id', 'name', 'email', 'created_at']],
'posts' => ['columns' => ['id', 'title', 'status', 'created_at']],
],
];
Anything not listed either exports everything or is hidden — check the source, the default matters more than the docs suggest.
Reusing the exporter outside Voyager
The real day-to-day leverage is calling the exporter from your own controller or Artisan command rather than from the admin UI:
use Joy\VoyagerExport\Facades\VoyagerExport;
Route::get('/reports/posts.csv', function () {
return VoyagerExport::fromModel(Post::class)
->columns(['id', 'title', 'status', 'created_at'])
->download('posts-' . now()->format('Y-m-d') . '.csv');
});
You get consistent column handling and streaming for free, and your scheduled reports look the same as the admin's ad-hoc downloads.
Scoping to the current filter
If the browse view supports search/filter, the button should carry the query string through — otherwise users get "export" for the whole table and assume it exported their filtered view. Either pass the request query into the exporter (->applyFilters($request->query())) or generate the button URL with request()->getQueryString() appended. This is the single most common "bug report" for export modules.
Large exports
For anything over ~10–50k rows, don't do it in the request:
ExportPostsJob::dispatch($filters)->onQueue('exports');
Have the job write to Storage::disk('exports') and email/notify the user with a signed URL. The package's chunking helps memory, but it won't help a load balancer's 30-second timeout.
Permissions
Voyager's role system is the natural gate. Wrap the button render in a permission check (Auth::user()->hasPermission('browse_posts') or a dedicated export_posts if the package registers one), and keep the route behind the same admin.user middleware group. Never expose the export route publicly just because it's read-only — a full table dump is a data exfiltration vector.
Excel and UTF-8. Raw UTF-8 CSV opened in Excel mangles accented characters. Prepend a BOM (\xEF\xBB\xBF) on the first row, or your first support ticket will be about Müller showing as Müller.
BREAD columns ≠ database columns. The exporter typically reads the BREAD definition, so relation columns (belongsTo) come out as raw IDs, and image/file columns as paths. If you need labels, add an accessor on the model and export that instead — don't try to make the exporter join.
Dates and timezones. Timestamps export in whatever the app timezone is at query time, which may not be the admin's. Either document it or format explicitly per-column. Mixed date formats across exports are a silent source of reporting errors.
Empty exports. If a BREAD has zero rows, most exporters happily emit a header-only file or an empty body — verify which, because "download succeeded but the file is blank" is ambiguous to users. Prefer a header-only file.
Version drift is your biggest risk. Seven stars and a single 2024 release means no active maintainer to catch a Voyager or Laravel breaking change. Pin the version in composer.json, and if the module is load-bearing, fork it into your packages/ directory with a path repository so you can patch it during a framework upgrade without waiting on upstream.
Extension points, in order of preference:
Debugging a missing button. Work outward: (a) is the BREAD browse permission granted to the role? (b) is the BREAD listed in the package config? (c) did the view publish actually overwrite the cached compiled views (php artisan view:clear)? (d) is the service provider auto-discovered — check bootstrap/cache/packages.php.
Test it in the pipeline. A one-line feature test that hits the export endpoint and asserts Content-Type and a non-zero row count will catch the "an upgrade silently broke the exporter" case that nobody notices until month-end reporting.
How can I help you explore Laravel packages today?