rich-id/excel-generator-bundle
A Laravel/PHP bundle for generating Excel spreadsheets, offering a simple API to turn application data into downloadable .xlsx reports and exports. Composer-installable and suited to lightweight reporting workflows.
The v1.0.2 release adds ExcelResponseFactory, the piece that turns a generated spreadsheet into an HTTP response. Previously you likely had to hand-roll response()->download(...) calls or rely on whatever Excel::download() exposed; the factory centralizes filename generation, content-type headers, and download-vs-inline behaviour.
Start here, in order:
$this->app->singleton(ExcelResponseFactory::class, ...) or an alias. If it is bound, you never new it up.download($export, string $filename, array $options = []), store(...), and possibly inline(...). The exact signatures matter more than the naming — check whether the first argument is an export class, a Spreadsheet instance, or a closure.Excel::download() calls are deprecated or merely supplemented.First use case — the smallest thing that proves it works end to end:
// app/Http/Controllers/InvoicesExportController.php
public function __invoke(ExcelResponseFactory $excel)
{
return $excel->download(new InvoicesExport($this->currentPeriod()), 'invoices-2024-06.xlsx');
}
Hit that route, confirm the browser receives an .xlsx with the right Content-Type and a sane Content-Disposition, and open the file in both Excel and LibreOffice before you build anything on top of it. A package that produces a file Excel repairs on open is worse than no package.
Constructor injection in invokable controllers. The factory is stateless by design; inject it rather than resolving from the container inline. This keeps the controller testable and makes the dependency visible.
Factory as a formatting boundary. The best use of a response factory is to keep presentation decisions out of your export classes. Let the export class describe what the data is; let the factory own how it is delivered — filename, extension, headers, inline vs attachment. Put filename conventions (slugs, period suffixes, user prefixes) in one place, e.g. a small ExportName helper the factory consumes, instead of scattering sprintf() through controllers.
Wrap it in a response macro or your own thin service. If you have more than a handful of export endpoints, wrap the factory:
final class Exports
{
public function __construct(private ExcelResponseFactory $factory) {}
public function download(Exportable $export, string $basename, ?User $user = null): Response
{
$name = Str::slug($basename).'-'.now()->format('Y-m-d').'.xlsx';
return $this->factory->download($export, $name, [
'author' => $user?->name,
'company' => config('app.name'),
]);
}
}
This gives you one choke point for auth checks, audit logging (Log::info('export', [...])), and future signature changes.
Pair downloads with queued generation for anything large. A response factory streams a file you already built; it does not make generation cheap. For exports over roughly 10–20k rows, generate to storage in a queued job and let the endpoint hand back a signed URL. Use the factory for the small, synchronous exports and a TemporaryUrl / Storage::download() path for the big ones. Keeping both paths behind the same wrapper above stops the two from drifting.
Testing pattern. Assert on the response envelope rather than on bytes:
$response = $this->actingAs($user)->get('/exports/invoices');
$response->assertOk()
->assertHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
->assertDownload('invoices-2024-06.xlsx');
For content assertions, write the response to a temp path and re-open it with the underlying reader — this catches column-order and date-format regressions that header assertions never will.
Integration with policies. Authorize before calling the factory, not inside it. A response factory should not be making authorization decisions; it has no request context and no way to fail cleanly.
echo, dump(), dd(), or stray whitespace before the response returns will corrupt the download. Laravel's debug output and ray() calls are the usual culprits. Test on a route without middleware that prints anything.Content-Disposition. Check whether the factory does filename*=UTF-8''... or just quotes the raw string. If it does not, normalise to ASCII slugs in your wrapper rather than patching vendor code.assertDownload() is loose. It compares the filename, not the contents. A factory that returns an empty or HTML-error body will still pass. Add one content-level test per export type.Allowed memory size exhausted when an export class uses FromCollection with eager-loaded relations over a large table. FromQuery plus chunking is the fix; the factory is irrelevant to it.=, +, -, or @, prefix with a single quote or force the cell to string type. This is a security issue, not a cosmetic one, and a response factory will happily ship the payload.Carbon instance and set the format code on the cell.deleteFileAfterSend(true) (or the Symfony BinaryFileResponse equivalent); otherwise /tmp grows until the disk fills. This is the single most common defect in hand-rolled download responses.php artisan vendor:publish --tag=<package>-config and read the published file before tuning anything — particularly driver selection, temp path, and any cache settings. Defaults are usually fine for local but wrong for multi-server setups.final, subclassing for a project-specific filename convention is tempting but brittle across patch releases. Prefer composition (the Exports wrapper above) and treat the factory as a dependency you do not modify.^1.0.2 is fine; a bare ^1.0 will pull in future minors that may alter method signatures on a factory you now call from every export endpoint.How can I help you explore Laravel packages today?