onurb/excel-bundle
Symfony bundle that integrates PHPExcel to read and write Excel spreadsheets, exposing the library as a service with helpers for controllers and templates. Simplifies importing data from and exporting data to XLS/XLSX files in Symfony applications.
Install the wrapper plus its transitive PhpSpreadsheet dependency:
composer require onurb/excel-bundle
Check the resolved phpoffice/phpspreadsheet version against your PHP version — recent PhpSpreadsheet releases require PHP 8.1+, while older releases of this bundle were built for PHP 7.x. Pin versions defensively.
Register the bundle (Symfony Flex will not do this for you, since there is no recipe):
// config/bundles.php
Onurb\Bundle\ExcelBundle\OnurbExcelBundle::class => ['all' => true],
Find the factory service. The bundle's only real value is exposing PhpSpreadsheet behind a DI-managed factory:
bin/console debug:container | grep -i excel
Expect Onurb\Bundle\ExcelBundle\Service\Factory; older releases exposed it as excel.factory / PhpExcelBundle\Service\Factory. Autowire the class, not a string id, to be version-safe.
First use case — a streamed export action:
use Onurb\Bundle\ExcelBundle\Service\Factory;
public function export(Factory $excel): Response
{
$spreadsheet = $excel->createSpreadsheet();
$spreadsheet->getActiveSheet()
->setCellValue('A1', 'Name')
->setCellValue('B1', 'Total');
$writer = $excel->createWriter($spreadsheet, 'Xlsx');
return $excel->createStreamedResponse($writer); // sets headers + php://output
}
The package is a thin adapter, so your first hour is better spent reading the PhpSpreadsheet docs than the bundle's README. The bundle adds: Symfony service wiring, a writer/reader factory, and a streamed-response helper. That's the entire leverage surface.
Wrap the factory in your own exporter/importer service. Do not inject the bundle factory throughout your codebase; it couples controllers to a 13-star package. Create App\Excel\SpreadsheetExporter and App\Excel\SpreadsheetImporter, inject Factory once, and expose domain methods (exportInvoices(array $rows): StreamedResponse, importProducts(UploadedFile $file): array). If you later drop the bundle, only one class changes.
Export: build in memory, then choose sink.
$spreadsheet = $factory->createSpreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->fromArray($headers, null, 'A1');
$sheet->fromArray($rows, null, 'A2');
// Sink A: browser download
return $factory->createStreamedResponse($factory->createWriter($spreadsheet, 'Xlsx'));
// Sink B: persist to disk (e.g., for Mailer attachment)
$factory->createWriter($spreadsheet, 'Xlsx')->save($tmpPath);
Use sink B for CLI commands and Messenger handlers; createStreamedResponse is only meaningful in an HTTP context.
Import: upload → reader → normalise.
$reader = $factory->createReader('Xlsx');
$reader->setReadDataOnly(true);
$spreadsheet = $reader->load($file->getPathname());
$rows = $spreadsheet->getActiveSheet()->toArray(null, true, true, true);
Always setReadDataOnly(true) for imports unless you need styling. Convert dates yourself — PhpSpreadsheet hands back Excel serial numbers:
$date = \PhpOffice\PhpSpreadsheet\Shared\Date::excelToDateTimeObject($value);
Unknown formats: use IOFactory::identify($path) then IOFactory::createReaderForFile($path) rather than trusting the file extension; this bypasses the factory but is far more robust for user uploads.
Format selection: the second argument to createWriter/createReader is the PhpSpreadsheet writer/reader short name — Xlsx, Xls, Csv, Ods, Html, Pdf. Case-insensitive, but standardise in a constant map in your wrapper. Pdf is not self-contained; see gotchas.
Batch/large files: use a IReadFilter (setReadFilter) to stream rows by range instead of toArray(), and for writes that exceed memory, write row-by-row with $writer->save('php://output') inside a StreamedResponse callback rather than building the full sheet.
Integration with Symfony Form uploads: accept UploadedFile, validate MIME/extension in a form constraint, then hand getPathname() to your importer. Never load a file the user hasn't passed through validation — PhpSpreadsheet parsing is a real attack surface (XXE, zip bombs in OOXML).
PDF writer needs a third-party renderer. createWriter($ss, 'Pdf') only works if one of dompdf/dompdf, mpdf/mpdf, or tecnickcom/tcpdf is installed and you pass the correct class. The bundle does not pull these in; expect \PhpOffice\PhpSpreadsheet\Writer\Pdf\... not-found errors otherwise. In practice most projects shell out to a headless browser or dedicated PDF lib instead.
Streamed response buffering. If you see "headers already sent" or a corrupted xlsx download, a stray echo, BOM, or active output buffer in your app is leaking into the stream. Nothing should output before the response; if you use a base controller with debugging, disable it for export routes.
Memory is the usual production failure. A 50k-row xlsx can exceed 256 MB. Mitigations, in order of impact: setReadDataOnly(true) on reads; setCache() on Settings with a PSR-16 cache backend; IReadFilter chunking; Writer::setPreCalculateFormulas(false) on writes; raise memory_limit in the export command only.
Dates, formulas, and nulls. toArray() returns null for empty cells and raw floats for dates. getCalculatedValue() returns the cached value unless the file was saved by Excel; if you generate a file and immediately re-read it, formulas may resolve to 0/null. Call $spreadsheet->getCalculationEngine()->calculate() or setPreCalculateFormulas(true) before saving.
CSV quirks. Default delimiter is , and encoding detection varies by locale. Set explicitly:
$writer = $factory->createWriter($spreadsheet, 'Csv');
$writer->setDelimiter(';')->setEnclosure('"')->setSheetIndex(0);
A single-sheet CSV is fine; multi-sheet exports silently write only the active sheet.
Service-id churn across versions. Documentation online mixes excel.factory, PhpExcelBundle\Service\Factory, and Onurb\Bundle\ExcelBundle\Service\Factory. If autowiring fails, debug:container --show-arguments to confirm the real id — do not copy a service id from a blog post.
Extension points are sparse. There is no plugin system. If you need extra behaviour (array→sheet helpers, header styling, autosize), decorate the factory:
final class SpreadsheetFactory
{
public function __construct(private Factory $inner) {}
public function styledExport(string $title, array $rows): StreamedResponse { /* ... */ }
}
Register this as your own service and inject it everywhere; never subclass the bundle's factory (it is not designed for extension and internal signatures shift between releases).
Maintenance risk. 13 stars, zero known dependents, no Symfony recipe, and a wrapper that mostly forwards calls. Treat the bundle as optional sugar: PhpSpreadsheet's own API is stable and well-documented, and composer require phpoffice/phpspreadsheet with a small hand-written factory gives you the same result with fewer upgrade surprises. If you keep the bundle, isolate it behind your own exporter/importer services so swapping it out is a one-class change.
How can I help you explore Laravel packages today?