asgoodasnu/spreadsheet-excel-writer
A Laravel/PHP package for generating Excel spreadsheets through a simple, expressive API. Create .xlsx exports from arrays or application data for reports and downloads, with less boilerplate and readable, maintainable export code.
Install
composer require asgoodasnu/spreadsheet-excel-writer
This wraps the legacy PEAR Spreadsheet_Excel_Writer library and emits .xls (BIFF8 / Excel 97‑2003), not .xlsx. Before writing code, confirm the underlying PEAR classes load on your PHP version — expect reliable behavior on PHP ≤ 7.4 and deprecation/warning noise on PHP 8.x.
Register it
The package ships a Symfony bundle. Register it in config/bundles.php:
return [
// ...
Asgoodasnu\SpreadsheetExcelWriterBundle\AsgoodasnuSpreadsheetExcelWriterBundle::class => ['all' => true],
];
It is not a Laravel-native package: there is no package discovery, no config/excel.php, and no facade. In Laravel, manually bind the workbook/factory service in a service provider (or instantiate the PEAR classes behind your own adapter) rather than expecting php artisan vendor:publish to do anything useful.
Where to look first
vendor/asgoodasnu/Spreadsheet_Excel_Writer/ — read DependencyInjection/Configuration.php and the bundle extension to see what is actually exposed. Docs are sparse; the vendor source is the source of truth.Spreadsheet_Excel_Writer_Workbook and Spreadsheet_Excel_Writer_Worksheet. The bundle mostly instantiates these.bin/console debug:container | grep -i excel
Class names and the config root key (often asgoodasnu_spreadsheet_excel_writer) can vary by version — verify against the installed code.
First use case — a downloadable report
use Symfony\Component\HttpFoundation\StreamedResponse;
public function download(): StreamedResponse
{
return new StreamedResponse(function () {
$workbook = new \Spreadsheet_Excel_Writer_Workbook('php://output');
$worksheet =& $workbook->addWorksheet('Report');
$worksheet->write(0, 0, 'Name');
$worksheet->write(0, 1, 'Amount');
$worksheet->write(1, 0, 'Alice');
$worksheet->writeNumber(1, 1, 42);
$workbook->close();
}, 200, [
'Content-Type' => 'application/vnd.ms-excel',
'Content-Disposition' => 'attachment; filename="report.xls"',
]);
}
For Laravel, the equivalent is a streamed response:
return response()->streamDownload(function () {
$workbook = new \Spreadsheet_Excel_Writer_Workbook('php://output');
// ... write rows ...
$workbook->close();
}, 'report.xls', ['Content-Type' => 'application/vnd.ms-excel']);
Put an internal exporter between the app and the package. This is the single most valuable day-to-day pattern: controllers, jobs, and commands should depend on your own SpreadsheetExporter contract, not on PEAR classes. One implementation uses this package; a later implementation can use PhpSpreadsheet without touching call sites.
interface SpreadsheetExporter
{
/** @return string path to generated .xls */
public function export(iterable $rows, array $columns): string;
}
Inject the factory/service; do not new workbooks in controllers. Use the bundle's registered service or your own factory so exports are mockable and the migration point is in one place.
final class UserExport
{
public function __construct(private ReportWriterFactory $writers) {}
public function toXls(array $users): string
{
$workbook = $this->writers->create('php://memory');
$worksheet =& $workbook->addWorksheet('Users');
foreach ($users as $i => $u) {
$worksheet->writeNumber($i, 0, $u->getId());
$worksheet->writeString($i, 1, $u->getName());
$worksheet->writeNumber($i, 2, $u->getBalance());
}
$workbook->close();
return stream_get_contents($workbook->getStream()); // or write to a temp path
}
}
Stream for HTTP, write to disk for jobs. In an HTTP download, write to php://output inside a StreamedResponse to avoid buffering the entire file. In queue workers or CLI commands, write to a real temporary path and store it (Flysystem, S3, local disk). Never write a background export to php://output — it goes to stdout, not the browser.
Create formats once and reuse them. Format objects are stateful and designed to be applied many times. Creating a format per cell is a common cause of bloated, slow exports.
$bold =& $workbook->addFormat();
$bold->setBold();
$bold->setFontFamily('Arial');
$bold->setAlign('center');
$bold->setBorder(1);
$worksheet->write(0, 0, 'Header', $bold);
Useful setters: setBold(), setItalic(), setFontColor('blue'), setBgColor('#EEEEEE') (some builds expect names like 'silver'), setAlign('center'), setNumFormat('#,##0.00'), setBorder(1), setTextWrap(), setColumn()/setColumnWidth() for widths.
Wrap the procedural API with a small sheet helper. PEAR-style calls are &-reference heavy and column/row oriented. A thin wrapper lets the rest of the app speak arrays or iterators.
final class Sheet
{
public function __construct(private \Spreadsheet_Excel_Writer_Worksheet $ws) {}
public function row(int $r, array $cells, $format = null): void
{
foreach (array_values($cells) as $c => $value) {
is_numeric($value)
? $this->ws->writeNumber($r, $c, $value, $format)
: $this->ws->writeString($r, $c, (string) $value, $format);
}
}
}
Use the right write method. write() guesses types; writeString() and writeNumber() are predictable. For dates, prefer writing a formatted serial/date value with an explicit setNumFormat() format rather than relying on locale inference.
Multiple worksheets. Create a summary sheet first and detail sheets after; Excel selects the first sheet by default. Keep worksheet handles and reuse them — do not recreate them per row.
Large exports. This library is not a streaming writer. For big datasets, process rows in chunks, write to a temp file, and consider queueing the export. Use generators to avoid loading the whole dataset into memory:
foreach ($this->repository->iterateUsers() as $i => $u) { /* write row */ }
Laravel integration tips. Bind an exporter service in a provider, use response()->streamDownload() for HTTP, and dispatch an export job for large reports. Do not rely on auto-discovery.
Testing strategy. Assert the generated file has the OLE/BIFF signature (D0 CF 11 E0 A1 B1 1A E1) and open it once manually in LibreOffice or Excel. For regressions, compare generated output against a golden fixture or parse it with PhpSpreadsheet in tests.
Migration pattern. Feature-flag the legacy exporter, implement a parallel PhpSpreadsheet path, compare outputs on real templates/consumers, then decommission the legacy adapter. Treat this package as a stopgap, not a destination.
.xls only. No .xlsx, no .ods, no streaming writer, no modern charts. If any consumer can accept .xlsx, choose PhpSpreadsheet or Laravel Excel instead.addFormat() objects once and reuse. Per-cell format creation is a frequent performance and file-size problem.& references ($worksheet =& $workbook->addWorksheet(...)). Dropping the & can produce notices or unexpected behavior depending on PHP version.php://output in CLI is stdout. For queue jobs and commands, always write to a file path, then move/store the file..xls generation is memory- and CPU-intensive. Large or concurrent exports can exhaust PHP workers. Queue them, cap row counts per file, add timeouts, and monitor failures.tmp or a storage path requires correct permissions in each environment (local, CI, containers). Validate early; a failed temp write often surfaces as a corrupt or empty download.setNumFormat('#,##0.00') is not guaranteed to render identically across viewers. Test date cells explicitly; prefer unambiguous formats.write() type guessing. For IDs, phone numbers, and currency, use writeString()/writeNumber() deliberately. Numeric-looking strings may become numbers and lose leading zeros.'silver') rather than hex values ('#EEEEEE'). If a fill color silently disappears, check the expected argument format in the vendor source.setColumn()/setColumnWidth() on the worksheet and set widths once, not per row.php://output, any accidental echo, warning output, or BOM before workbook output corrupts the file. Enable output buffering during generation and suppress notices, or write to a temp file and send it after.close() means a valid workbook. Add output validation around export failures.$workbook->close() runs, and log context (row count, template, user, environment). Failures that emit partial files are worse than clean errors.file (BIFF/OLE), open in LibreOffice, check debug:container for service/config mismatches, and read the vendor DependencyInjection and PEAR classes directly.=, +, -, or @, sanitize them or ensure they are written as strings so spreadsheet consumers do not execute them..xls output that downstream consumers require, with a bounded modernization plan.How can I help you explore Laravel packages today?