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 Laravel Package

rap2hpoutre/fast-excel

Fast Excel import/export for Laravel, powered by Spout. Export Eloquent models, queries, or collections to xlsx, ods, or csv with a simple API, customize columns via callbacks, and handle large files efficiently. Benchmark-tested for speed.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and use, no setup ceremony:

composer require rap2hpoutre/fast-excel

There is no service provider to publish, no config file, no migration. The package is a thin, opinionated wrapper around OpenSpout (formerly Box/Spout), so the fastest path to value is: install it, then export one query and import one file. If you want the FastExcel facade, alias it manually in config/app.php ('FastExcel' => Rap2hpoutre\FastExcel\Facades\FastExcel::class); otherwise just use Rap2hpoutre\FastExcel\FastExcel;. The fastexcel() global helper is available without any registration and is the shortest way to get going in a controller or artisan command.

First use case — a controller that downloads a CSV:

public function export()
{
    return (new FastExcel(User::query()->orderBy('id')))->download('users.csv');
}

Note you can pass a Builder, not just a collection — this is the single most important thing to learn early, because it is what keeps memory flat. export() returns the absolute path of the written file when you write to disk rather than download, which is what you want for scheduled reports, mail attachments, or pushing to S3 afterwards.

Read these first, in order: the README's Quick start → Export large collections → Import large files → Advanced usage. The memory section is the real differentiator versus Laravel Excel; the feature list is deliberately short (the README says "use it only for simple tasks"), so do not expect formulas, charts, cell merging, or multi-format styling beyond OpenSpout's Style.

Positioning decision to make on day one: if your requirement is "let users download a filtered table" or "ingest a partner's CSV into the DB", FastExcel is the right tool. If the requirement mentions templates with formatting, pivot tables, or formula injection, use Laravel Excel instead. Mixing both in one codebase is fine and common.

Implementation Patterns

Large exports: feed a cursor or generator, never ::all().

(new FastExcel(User::cursor()))->export('users.xlsx');
(new FastExcel(User::query()->where('active', 1)->lazy()))->download('active.xlsx');

The README's benchmark table is the argument: ~4 MB peak versus >512 MB for a materialized collection. Any custom source can be wrapped in a generator with yield.

Large imports: return null from the callback, or use importLazy().

// Streaming side-effect import
(new FastExcel)->import($path, function ($line) {
    User::create(['name' => $line['Name'], 'email' => $line['Email']]);
    return null; // critical: do not accumulate
});

// Or keep the Collection API while staying lazy
(new FastExcel)->importLazy($path)
    ->chunk(1000)
    ->each(fn ($chunk) => User::insert($chunk->all()));

importLazy()->chunk() is the pattern I'd standardize on for anything that touches the database, because per-row create() is an order of magnitude slower than batched insert()/upsert(). Wrap the whole thing in DB::transaction() or process in an idempotent job.

Column shaping with closures. The export callback is where you decouple storage schema from spreadsheet headers, compute derived values, and normalize formatting:

(new FastExcel(User::cursor()))->export('users.xlsx', fn ($u) => [
    'Email'      => $u->email,
    'First Name' => $u->firstname,
    'Phone'      => $u->phone,
]);

When the callback needs helper values that shouldn't appear as columns, use hideColumnsPrefixedWith() — prefixed keys stay available inside the closure but are stripped from headers and rows. It is opt-in, so existing underscore columns are unaffected.

Multi-sheet work. Export with SheetCollection, using string keys for named sheets:

$sheets = new SheetCollection(['Users' => User::cursor(), 'Projects' => Project::cursor()]);
(new FastExcel($sheets))->export('report.xlsx');

On the import side, importSheets() returns all sheets, withSheetsNames() keeps the sheet names as keys, withSheetContext() hands the sheet name as the callback's first argument (useful for per-sheet validation rules), and sheet(3) / sheet('Users') selects one sheet — the name variant removes the fragile positional coupling from your code.

Formatting and type fidelity. Excel silently eats leading zeros. Use stringValues() for phone numbers, IBANs, and long numeric IDs, then relax it per column with setColumnFormat(['id' => 'number', 'phone' => 'string']), which takes precedence. For presentation, headerStyle(), rowsStyle(), and setHeaderColumnStyles([0 => ...]) accept OpenSpout\Common\Entity\Style\Style instances.

CSV quirks. For non-standard files, configureCsv(';', '#', 'gbk') before import() handles delimiter, enclosure, and encoding in one call. Pair with withoutHeaders() for headerless files and limitRows(100) for "dry run / preview the first N rows" UI features.

Uploaded files. Pass the uploaded file path directly — since v5.9.0, extension-less uploads from $request->file('file') are detected correctly. Do not invent the extension from the client-supplied filename.

Facade for terse call sites: FastExcel::data($list)->export('file.xlsx') — remember the facade has no constructor, so data() is mandatory there.

Gotchas and Tips

  • import() without a callback is a memory trap. It returns a Collection containing every row; on a file beyond memory_limit it fails outright. The fix is a callback returning null, or importLazy().
  • A callback that returns the created model re-accumulates everything. This is the most common self-inflicted OOM: return User::create([...]); looks harmless and defeats streaming. Return null when you only need the side effect.
  • cursor() alone is not enough for multi-sheet exports. transpose() and SheetCollection need buffering; transpose() cannot stream at all, and transpose() is also unsupported with importLazy().
  • Legacy .xls is not supported. Only xlsx, ods, csv. Reject .xls at validation time with a clear message rather than letting the reader throw.
  • Pin versions for the two fixes that bite in production: v5.9.0 (extension-less uploaded CSV/ODS detection) and v5.14.0 (reader is now closed when an import throws — before that, long-running workers leaked file handles across failed imports). Anything below those two is a latent bug, not just a missing feature.
  • Datetime cells come back as objects, not strings. Normalize with Carbon::parse() or format inside your import callback; do not assume the spreadsheet's display format survives the round trip.
  • Header matching is by exact literal key. Whitespace, casing, and BOMs in the header row will produce null from $line['Email']. When debugging, dump(array_keys($firstRow)) before anything else. Header de-duplication exists, but silently renamed duplicates are worse than a hard failure — validate the header row explicitly for user-uploaded files and reject files with missing required columns.
  • Column hiding is opt-in. If you adopt hideColumnsPrefixedWith(), be aware it changes nothing until called; teams mixing code that assumes it is on will produce files with stray _role columns.
  • Batch your inserts. Per-row Eloquent writes in an import callback dominate the runtime; the parser is fast, your ORM is not. Chunk to 500–1000 and use insert()/upsert().
  • Queue anything over a few thousand rows. max_execution_time and web server timeouts will kill a synchronous 10-minute export. The return-path idiom works well here: export to storage/app/exports/..., then attach or stream it.
  • export() gives you the path — use it. It's the cleanest hook for "write file, then Storage::disk('s3')->putFile(...)" or "write file, attach to Mailable", and it composes far better than download().
  • Testing: use Storage::fake(), assert file_exists on the returned path, then re-import the generated file and compare — round-tripping is the cheapest end-to-end assertion for export/import code, and avoids fixture drift.
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.
muka/shape-reader
tdt/json
refinery29/php-cs-fixer-config
hotmeteor/eco-env
mvaliolahi/hydrate
wearede/tbcpay-php
parsnick/steak
thibaud-dauce/mattermost-php
multilinguals/apollo-client
antaresbr/collection
kr0lik/phpstan-rules
loco/loco
starfederation/datastar-php
yupmin/modern-phystrix
stephenjude/extended-artisan-commands
lumphp/laravel-macroable
lumphp/laravel-collections
lumphp/laravel-contracts
sermepa/sermepa
arkecosystem/fortify