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

avadim/fast-excel-laravel

Fast Excel for Laravel wraps the avadim/fast-excel library to provide simple, memory-efficient spreadsheet import and export. Read and write XLSX/CSV files with a clean Laravel facade and minimal setup.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm discovery

composer require avadim/fast-excel-laravel

Laravel's package auto-discovery registers the service provider; there is nothing mandatory to configure. Verify before writing code:

php artisan about | grep -i excel        # or check bootstrap/cache/packages.php
ls vendor/avadim/fast-excel-laravel

Look first at vendor/avadim/fast-excel-laravel/README.md and the facade class itself — the wrapper is thin, so the real API surface lives in the underlying avadim/fast-excel-writer / avadim/fast-excel-reader packages. Reading those two READMEs is usually faster than guessing at wrapper method names.

First use case: stream a query result to XLSX without blowing memory

The whole point of this package versus maatwebsite/excel + PhpSpreadsheet is that rows are written incrementally, so you can export 100k rows with a flat memory profile. Start there:

use Avadim\FastExcelLaravel\Facades\FastExcel; // confirm alias in the README

Route::get('/exports/users', function () {
    $path = storage_path('app/private/users-'.now()->timestamp.'.xlsx');

    $excel = FastExcel::make($path);           // or FastExcel::create(...)
    $sheet = $excel->sheet('Users');

    $sheet->writeHeader(['ID', 'Name', 'Email', 'Created']);

    App\Models\User::query()->lazy(1000)->each(function ($user) use ($sheet) {
        $sheet->writeRow([
            $user->id,
            $user->name,
            $user->email,
            $user->created_at->toDateTimeString(),
        ]);
    });

    $excel->save();                             // or ->output() / ->download()

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

If the facade name collides with something in your app, resolve the class out of the container instead of aliasing it.

Implementation Patterns

Wrap it in a dedicated export class, not in controllers. The package is a low-level writer; your app's column mapping, formatting, and file naming belong in a class you own:

final class UserExport
{
    public function __construct(private array $columns, private string $disk = 'local') {}

    public function build(string $path): string
    {
        $excel = FastExcel::make(Storage::disk($this->disk)->path($path));
        $sheet = $excel->sheet('Users');
        $sheet->writeHeader(array_values($this->columns));

        User::query()->lazyById(1000)->each(fn ($u) => $sheet->writeRow(
            collect($this->columns)->keys()->map(fn ($k) => data_get($u, $k))->all()
        ));

        $excel->save();
        return $path;
    }
}

This indirection is your escape hatch: if the package stalls (41 stars, no dependents), you swap the body for PhpSpreadsheet and nothing else changes.

Long exports belong in a queued job. Write to storage/app/private/exports/{uuid}.xlsx, then notify the user with a temporary URL:

class BuildUserExport implements ShouldQueue
{
    public function __construct(public int $userId, public string $path) {}

    public function handle(): void
    {
        (new UserExport(config('exports.users')))->build($this->path);

        Notification::send(User::find($this->userId),
            new ExportReady(Storage::disk('local')->temporaryUrl($this->path, now()->addHour())));
    }
}

Keep the job's timeout proportional to row count and give it its own queue — a 200k-row export will hold a worker for a while.

Import: iterate, don't materialize. The reader exposes row iteration; use it and insert in chunks rather than building an array of all rows:

use Avadim\FastExcelLaravel\Facades\FastExcel; // reader facade

$excel = FastExcel::open($request->file('sheet')->getRealPath());
$sheet = $excel->sheet();

$headers = null;
$buffer  = [];

foreach ($sheet->rows() as $row) {          // confirm iterator method name
    if ($headers === null) { $headers = $row; continue; }

    $buffer[] = array_combine($headers, $row);
    if (count($buffer) >= 500) {
        User::insert($buffer);              // or upsert() for re-imports
        $buffer = [];
    }
}
if ($buffer) { User::insert($buffer); }

Header-driven mapping is the pattern that survives spreadsheet edits. Users reorder columns constantly. Resolve field names from row 1 into a key→index map, then read by index on every subsequent row; reject the file outright if a required header is missing.

Formatting and multi-sheet exports. Styling is passed as arrays to the sheet, which is where the underlying writer shines compared to writing raw XML:

$sheet->writeTo('A1', 'Report');
$sheet->applyStyle('A1:D1', ['font-style' => 'bold', 'fill' => 'FFEEDD']);
$sheet->setColWidths(['A' => 8, 'B' => 30, 'C' => 35]);
$sheet->freezeHeader();                      // or freeze panes at 'A2'
$sheet->writeCell('=SUM(D2:D100)');

Multiple sheets come from calling ->sheet('Name') repeatedly on the same $excel instance — good for "summary + detail" deliverables in one file.

Validate uploads before touching the reader. Wrap import in a FormRequest: mimes:xlsx, max:10240, and a whitelist of expected header names. Excel is not a trusted input format; a malformed file should produce a 422, not a stack trace from deep in the XML parser.

Gotchas and Tips

The Excel facade alias is the number one conflict. If the app also has maatwebsite/excel (or any package registering Excel), you will get "A facade root has not been set" or, worse, calls silently hitting the wrong class. Register this package's facade under a custom alias in config/app.php ('FastXlsx' => Avadim\...\Facades\...::class) or type-hint the concrete class.

Excel serial dates. Date cells come back as integers, not Carbon. Convert with the reader's formatter if it exposes one, otherwise \PhpOffice\PhpSpreadsheet\Shared\Date::excelToDateTimeObject() — or just pull in Carbon and do Carbon::create(1899, 12, 30)->addDays($serial). Watch the 1900 leap-year bug and timezone drift; store UTC.

Formula injection is a real security issue on export. Any cell starting with =, +, -, or @ that came from user input will execute when opened. Sanitize on write:

$safe = fn ($v) => is_string($v) && preg_match('/^[=+\-@]/', $v) ? "'".$v : $v;

Everything is a string unless you say otherwise. Excel will happily turn "007" into 7 and "2024-01-01" into a date on the user's machine. For ID columns, write as explicit string cells (or prefix with ') so round-trips don't corrupt data.

Memory: the writer streams, the reader mostly does not. lazy()/cursor() in Eloquent plus writeRow() in a loop keeps the writer flat, but the reader caches dimension and shared-string data. Huge import files still need memory_limit headroom — test with a 50MB file, not a 5-row fixture.

Temp file hygiene. These libraries write to sys_get_temp_dir() or to the path you gave. For queued exports, always deleteFileAfterSend() on downloads and prune abandoned export files with a scheduled command; otherwise storage/app/private/exports grows forever.

Config publishing may not exist. Try php artisan vendor:publish --tag=fast-excel then --tag=fast-excel-config; if both return nothing, fall back to --provider="Avadim\FastExcelLaravel\FastExcelServiceProvider" and read the source. Don't build a config file by hand from blog posts — the keys change between the wrapper and the underlying packages.

Pin the version and check PHP/ext requirements. ext-zip and ext-xml (or ext-simplexml) are hard requirements, and the package tracks the underlying reader/writer majors closely. In composer.json, use a caret constraint on a specific minor ("avadim/fast-excel-laravel": "^1.2") rather than *; with a single maintainer and a fast release cadence (a 2026-09 release on a 41-star repo), a silent breaking change in a patch is plausible. Commit composer.lock and run the export/import test suite on every composer update.

Testing. Use Storage::fake('local') and assert the file exists plus a spot-check of contents:

Storage::fake('local');
(new UserExport(['id' => 'ID', 'email' => 'Email']))->build('exports/test.xlsx');
Storage::disk('local')->assertExists('exports/test.xlsx');

Read the file back with the same package to assert row count and header order — cheaper and more robust than asserting binary contents. If the facade is hard to fake, resolve the underlying class through the container and ->instance() a stub.

Column letters break after Z. Hand-rolled chr(65 + $i) produces [ at index 26. Use writeRow() (positional, handled for you) or the writer's column-letter helper rather than computing AA yourself.

Debugging a failed write. Exceptions from the underlying writer are often generic ("Cannot write to file") — nine times out of ten it's a directory permission, a missing parent directory, or a filename with characters the ZIP layer rejects. storage_path() on a fresh container without storage/app/private created is the classic first-run failure; php artisan storage:link does not create it.

Extension points worth knowing. Styling arrays, per-cell formatters/callbacks, and direct cell addressing (writeTo('C3', ...)) are the levers the package actually gives you. Anything more exotic — conditional formatting, charts, pivot caches — is out of scope; for those, go straight to PhpSpreadsheet rather than fighting the wrapper.

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/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
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route