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

Fast Excel Writer Laravel Package

avadim/fast-excel-writer

Fast, memory-efficient PHP library for writing Excel XLSX files. Streams rows to keep memory low and supports multiple sheets, styling, formulas, merged cells, images, and large datasets. A lightweight alternative to PhpSpreadsheet for export-heavy Laravel/PHP apps.

View on GitHub
Deep Wiki
Context7

Getting Started

avadim/fast-excel-writer is framework-agnostic — there is no service provider, facade, or config file to publish. Installation is one line and everything else is plain PHP:

composer require avadim/fast-excel-writer

Where to look first

  1. README.md — the examples are the real documentation; the API is fluent and short.
  2. src/Excel.php — workbook factory, save() / download() / output().
  3. src/Sheet.php — the bulk of the API you will use: writeRow(), writeCell(), setStyle(), setColWidths(), mergeCells(), addChart().
  4. src/Style.php and the demo scripts for the exact style-array dialect (it is not PhpSpreadsheet's).

First use case — an admin "Export to Excel" button

use avadim\FastExcelWriter\Excel;

Route::get('/orders/export', function () {
    $excel = Excel::create(['Orders']);
    $sheet = $excel->getSheet();

    $sheet->writeRow(['ID', 'Customer', 'Total', 'Created'], [
        'font' => ['bold' => true],
        'fill' => '#EEEEEE',
        'text-align' => 'center',
    ]);

    foreach (Order::with('customer')->cursor() as $order) {
        $sheet->writeRow([
            $order->id,
            $order->customer->name,
            $order->total,
            $order->created_at->format('Y-m-d H:i'),
        ]);
    }

    $sheet->setColWidths([8, 32, 12, 18]);
    $sheet->setFreeze('A2'); // keep the header visible

    $path = $excel->save(storage_path('app/exports/orders.xlsx'));

    return response()->download($path)->deleteFileAfterSend(true);
});

That is the whole loop: create workbook → get sheet → write rows → style ranges → save → hand the file to Laravel. The first thing most teams use it for is replacing a PhpSpreadsheet or CSV export that is blowing the memory limit.

Implementation Patterns

1. Encapsulate each export in a class, not a controller

Keep formatting out of the route closure. A small app/Exports/OrdersExport.php with a build(): Excel method (or handle(string $path): string) is testable, reusable from a command, and keeps controllers thin. Resist the temptation to build a generic "Laravel Excel"-style abstraction on top — you get more value from writeRow() loops than from a reinvented DSL.

2. Stream from the query builder, never ->get()

The package's speed advantage disappears if you hydrate 200k Eloquent models first. Use cursor() or chunkById() and write inside the loop. This is the single biggest reason to pick this package over PhpSpreadsheet.

Order::query()->select('id', 'customer_id', 'total')
    ->orderBy('id')
    ->chunkById(5000, function ($orders) use ($sheet) {
        foreach ($orders as $order) {
            $sheet->writeRow([$order->id, $order->total]);
        }
    });

3. Centralise style presets

Style arrays are verbose and repeated everywhere. Put them in config/exports.php or class constants and apply them by range rather than per cell — setStyle('A1:D1', $headerStyle) is one XML node, whereas styling 50k cells individually adds rows to the sheet XML and time to the request.

4. Large exports belong in a queued job

Anything measured in minutes should not hold an HTTP connection. Write to storage/app/exports/{uuid}.xlsx in a job, record the path on a model, then generate a URL::temporarySignedRoute() for the download. Wrap the job in withoutOverlapping() — two concurrent jobs writing the same filename is a classic corruption source.

5. Multi-sheet workbooks

Excel::create(['Summary', 'Detail']) gives you named tabs; getSheet('Detail') switches between them. Use this instead of 40-column "everything" sheets — the summary sheet can hold totals, charts, and an autofilter while the detail sheet stays plain and cheap to write.

6. Formulas, charts, and autofilter for dashboards

$sheet->writeRow([], []);                       // spacer
$sheet->writeRow(['Total', '=SUM(C2:C'.$last.')']);
$sheet->setAutofilter('A1:D1');
$sheet->addChart('A1:B10', ['type' => 'column', 'title' => 'Revenue']);

Charts and ranges must be written after the data they reference — the chart series is resolved against what is already in the sheet buffer.

7. Verify output in tests

There is no Laravel assertion helper. In a feature test, save to a temp path and re-open it with PhpSpreadsheet (dev dependency only) or check the unzipped xl/worksheets/sheet1.xml for the expected <row>/<c> count. Asserting "file exists and is > 0 bytes" catches almost nothing.

Gotchas and Tips

  • download() vs response()->download(). The package's built-in download() writes headers and exits-style output itself, which bypasses Laravel middleware and can trip output buffering. Prefer save() to a path and return a standard Laravel download response; you keep the Content-Disposition filename handling (including non-ASCII names) and deleteFileAfterSend().

  • Style dialect is not PhpSpreadsheet's. Expect ['font' => ['bold' => true], 'text-align' => 'center', 'format' => '#,##0.00'], not font.bold. Style keys are the ones in the package docs; copy-pasting PhpSpreadsheet style arrays silently produces unstyled cells rather than an error. Check the produced XML or open the file in Excel when a style "doesn't work" — there is no validation layer to help you.

  • Formulas have no cached value. writeCell('=SUM(A1:A5)') stores the formula only. Excel and LibreOffice recalculate on open, but PhpSpreadsheet-based importers and headless CSV converters read a blank/zero cell. If another system consumes the generated file, write the computed value instead of the formula, or accept that some readers will show empty cells.

  • Formulas use English names and comma separators. =SUM(...), =IF(A1>0,"yes","no") — regardless of the user's locale. Never pass a locale-formatted number string (1.234,56); Excel stores strings as text and SUM ignores them. Write floats and let a number format handle display.

  • Dates need a number format. Pass a real value/date plus 'format' => 'YYYY-MM-DD HH:MM'. A raw string date is just text, sorts wrong, and breaks SUMIFS/pivot grouping. Store timestamps in UTC and convert at write time so the export matches what the app shows.

  • Memory is low, not zero. The writer streams XML, but a single sheet's rows are buffered in a sheet object until save. For truly huge datasets, split into multiple sheets or multiple files rather than assuming constant memory.

  • getSheet() is idempotent by name. Calling it repeatedly returns the same sheet — this is usually what you want, but if you intend a new tab, use makeSheet('Name') (or create all sheets up front). Accidentally writing two logical datasets into "Sheet1" is a common first-day bug.

  • Sanitise user strings. Control characters from scraped/user-supplied text (e.g. \x00–\x08) produce invalid XML inside the zip; Excel reports the file as corrupt with no useful message. A preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/u', '', $value) on free-text columns has saved many support tickets.

  • Set explicit column widths. Autofit is not reliable or is absent in many paths; setColWidths([...]) is cheap and prevents the "everything is ####" complaint.

  • Temp/disk considerations. Check open_basedir, disk free space, and directory permissions on the export folder in production. A failing save() mid-stream may leave a truncated .xlsx behind — write to *.tmp and rename() on success.

  • Merged cells only accept a value in the top-left. Writing to a covered cell stores an orphaned value that Excel drops silently.

  • Extension points are thin on purpose. There is no WithMapping-style hook; customisation is "write your own loop". If you need cell-by-cell conditional logic, compute the style in your loop and pass it to writeRow(). For truly exotic needs (pivot tables, macro-enabled .xlsm), this is the wrong tool.

  • Pin the version. Releases are frequent (the package is actively maintained). Lock a minor version and re-run your export tests before bumping — style-key and option names have shifted across major versions.

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.
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log