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

avadim/fast-excel-reader

Fast, memory-efficient Excel reader for PHP and Laravel. Streams XLSX worksheets with XMLReader to read rows and cells without loading whole files, making it suitable for large spreadsheets and background jobs. Simple API with minimal dependencies.

View on GitHub
Deep Wiki
Context7

Getting Started

avadim/fast-excel-reader is a pure-PHP, dependency-free reader for .xlsx and .csv. It does not use PhpSpreadsheet — it unzips the XLSX and parses the XML directly with SimpleXML, which is why it is roughly an order of magnitude faster and far lighter on memory. In a Laravel app that means it is a natural fit for read-only import pipelines, especially large files that would otherwise time out or exhaust memory_limit.

composer require avadim/fast-excel-reader

There is no service provider, no config file, and no facade. It is a plain library — you use it directly. Requirements are just ext-zip, ext-simplexml/ext-xml, and PHP 8.x. Nothing to publish.

Where to look first: the README's Usage section for Excel::open() and the sheet-level readRows() config array. That config array (range, row window, header handling, empty-row behaviour) is where 90% of day-to-day work happens, and its exact keys are the thing most worth verifying against the version you pin.

First use case — a minimal controller import:

use avadim\FastExcelReader\Excel;

$excel = Excel::open($request->file('import')->getRealPath());

foreach ($excel->getSheetNames() as $name) {
    $sheet = $excel->getSheet($name);
    $rows  = $sheet->readRows();          // numeric-keyed array of row arrays
    // $rows[1] = ['A' => 'id', 'B' => 'name', ...]
}

That is the whole onboarding. The next step up is readRowsWithHeader(), which turns the first row into associative keys so you can hand rows straight to a validator.


Implementation Patterns

1. Upload → validate → queue → import

The single most common shape in a Laravel app. Keep the controller thin: validate, move the file to a durable location, dispatch a job, and do all reading in the job.

public function store(ImportRequest $request): RedirectResponse
{
    $path = $request->file('file')->store('imports');   // must be durable — tmp files vanish
    ImportContacts::dispatch($path, auth()->id());

    return back()->with('status', 'Import queued');
}
class ImportContacts implements ShouldQueue
{
    use Queueable, Batchable;

    public function __construct(public string $path, public int $userId) {}

    public function handle(): void
    {
        $excel = Excel::open(Storage::path($this->path));
        $sheet = $excel->getSheet($excel->getSheetNames()[0]);

        foreach ($sheet->readRowsWithHeader() as $row) {
            // $row is keyed by the header cells: ['email' => ..., 'name' => ...]
            ContactImporter::dispatch($row, $this->userId)->onQueue('imports');
        }
    }
}

2. Header-driven mapping with normalisation

Spreadsheets arrive with " Email ", E-Mail, email_address. Normalise once, at the boundary, then use your own DTOs inside the app:

$rows = $sheet->readRowsWithHeader();          // first row = keys

$normalise = fn (string $k) => Str::slug(Str::lower(trim($k)), '_');

$records = collect($rows)->map(
    fn (array $row) => collect($row)->mapWithKeys(fn ($v, $k) => [$normalise((string) $k) => $v])->all()
);

Mapping to a ImportRowData DTO here also means a schema change in the customer's file only breaks one class.

3. Bounded reads for big files

Never call readRows() with no bounds on user-supplied files. Read a header, then walk the file in windows so a 200k-row sheet stays at a flat memory profile:

$sheet->readRowsWithHeader(...);  // or read the header row alone
$chunk = 1000;
$min   = 2;                        // row 1 is the header

do {
    $rows = $sheet->readRows(0, ['minRow' => $min, 'maxRow' => $min + $chunk - 1]);
    if (!$rows) break;

    DB::transaction(fn () => Contact::insert($this->mapRows($rows)));

    $min += $chunk;
} while (count($rows) === $chunk);

The exact window keys (minRow / maxRow / range) are worth confirming in the README for your pinned tag — but the pattern is stable and is the reason this library exists.

4. Template / fixed-layout sheets

For vendor exports and form templates, an explicit read area is more robust than positional array access:

$sheet->setReadArea('B4:F200');   // or pass 'range' => ... in the read call
$rows = $sheet->readRows();

This also gives you a cheap "did the file change shape?" guard: if readRows() returns a row count of zero, the vendor moved their columns.

5. Pairing with the write side

For round-trip features (export → user edits → re-import) use the companion avadim/fast-excel-writer rather than pulling PhpSpreadsheet in for one feature. The cell-addressing model and date helpers match.

6. A thin wrapper service

Because there's no facade, bind a small reader in the container so tests can fake it and so option defaults live in one place:

final class SpreadsheetReader
{
    public function rows(string $path, ?string $area = null): array
    {
        $sheet = Excel::open($path)->getSheet(0);
        if ($area) { $sheet->setReadArea($area); }

        return $sheet->readRowsWithHeader();
    }
}

Gotchas and Tips

Read-only, and only .xlsx / .csv. Legacy .xls (BIFF) is not supported and will fail — validate the extension and the mime type up front (Excel::isValidFile($path) does a cheap structural check). If customers still send .xls, reject with a clear message rather than letting the parser throw something cryptic.

Formulas are not evaluated. You get the cached value stored in the file, or possibly the formula string, depending on how the file was last saved. A workbook written by a script that never recalculated can therefore read as empty. If users depend on computed columns, that is a real failure mode.

Dates and numbers are raw. Excel stores dates as serial floats with a 1900 leap-year quirk. Expect 45123 rather than 2023-07-15. Convert at the boundary with the library's date helper (Excel::dateToDateTime() / Excel::dateToString() — check the exact name in your version) rather than Carbon::parse(), which will happily mis-parse small floats.

Don't trust a file because it has the right extension. XLSX is a zip; a .xlsx that doesn't unzip should be caught before you Excel::open(). Wrap the whole read in a try/catch and surface a domain-level "the file could not be read" error, because the underlying SimpleXML/zip errors are not user-facing.

Uploaded temp files die with the request. $file->getRealPath() is valid only inside the current request. Any queued import must operate on a stored path. This bites nearly everyone the first time.

Merged cells only carry a value in the top-left corner — the rest read as empty. If a customer's template merges label cells down a column, forward-fill in your mapping step.

Empty rows and trailing whitespace. Blank rows can appear in the middle of real data (users press Enter, then delete), and header cells frequently carry \u{00A0} or a BOM. trim() plus an explicit Str::of($v)->trim()->replace("\u{00A0}", ' ') on header keys avoids a whole class of "column not found" bugs. There is generally a flag to include or skip empty rows — pick deliberately rather than relying on the default.

CSV auto-detection is a guess. Delimiters, quoting and encodings (Windows-1251, latin1) are detected heuristically. If users routinely upload semicolon-CSV exported from European Excel, detect the delimiter yourself from the first line and pass it in, and convert to UTF-8 before reading. Also check whether the file starts with a BOM.

Memory is bounded by your call, not by the library. readRows() with no arguments materialises every row as a PHP array. On a 50 MB sheet that array is hundreds of MB. Always window large reads, and keep max_execution_time in mind for queued workers.

Zip handles. If you process many files in a long-running worker, let the Excel object go out of scope (or call its close/destruct method where available) between files, otherwise file descriptors can accumulate.

Testing. Fixtures are just .xlsx files — put a handful in tests/Fixtures covering: header-only, merged cells, dates, empty interior rows, a semicolon CSV, and a corrupt file. Those six cover almost every production incident this package produces.

When not to use it: anything needing cell styling, formula evaluation, charts, or writing. That's the point where maatwebsite/excel / PhpSpreadsheet earns its weight — and the migration path is easy because your rows are already plain arrays behind the wrapper service.

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.
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log