Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Spreadsheet Excel Writer Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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

  1. 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.
  2. The underlying PEAR API is what you will use day-to-day: Spreadsheet_Excel_Writer_Workbook and Spreadsheet_Excel_Writer_Worksheet. The bundle mostly instantiates these.
  3. Inspect the container to see real service ids and config keys:
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']);

Implementation Patterns

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.

Gotchas and Tips

  • .xls only. No .xlsx, no .ods, no streaming writer, no modern charts. If any consumer can accept .xlsx, choose PhpSpreadsheet or Laravel Excel instead.
  • PHP compatibility is the real gate. The PEAR lineage is old; PHP 8.x may produce warnings, deprecations, or fatal errors. Run a time-boxed spike on your exact PHP/Laravel/Symfony version matrix before committing.
  • No Laravel auto-discovery. Expect manual service binding. The Symfony bundle registration does not help Laravel. Grep vendor code for actual class names and config keys.
  • Do not create formats per cell. Create addFormat() objects once and reuse. Per-cell format creation is a frequent performance and file-size problem.
  • Reference semantics. Many PEAR methods expect & 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.
  • Memory and concurrency. In-process .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.
  • Temp directory permissions. Writing to 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.
  • Number and date formatting quirks. Locale-specific decimal separators can misformat numbers. 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.
  • Background colors vary by build. Some versions expect names ('silver') rather than hex values ('#EEEEEE'). If a fill color silently disappears, check the expected argument format in the vendor source.
  • Column widths are in character units. Use setColumn()/setColumnWidth() on the worksheet and set widths once, not per row.
  • Headers already sent. With 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.
  • Silent corruption. Excel may open a generated file with a "repair" prompt. Open outputs in LibreOffice and Excel, check the BIFF signature, and never assume a successful close() means a valid workbook. Add output validation around export failures.
  • Error handling. Wrap generation in try/catch, ensure $workbook->close() runs, and log context (row count, template, user, environment). Failures that emit partial files are worse than clean errors.
  • Debugging workflow. Reproduce exports to a local file, inspect with file (BIFF/OLE), open in LibreOffice, check debug:container for service/config mismatches, and read the vendor DependencyInjection and PEAR classes directly.
  • Security and license posture. Verify the license and dependency chain before production use. Treat abandoned dependencies as a risk item; vendor if necessary.
  • Formula injection. If you write user-supplied strings that begin with =, +, -, or @, sanitize them or ensure they are written as strings so spreadsheet consumers do not execute them.
  • Extension points. The useful extension points are yours, not the package's: an exporter interface, a sheet/format helper, a configurable temp path, and a feature flag. Keep package-specific code behind them so the eventual swap to PhpSpreadsheet is an implementation change, not an application-wide rewrite.
  • Adoption signal. Zero stars, zero dependents, and Score 0 are a red flag. Use this only to maintain existing .xls output that downstream consumers require, with a bounded modernization plan.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin