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

Easy Excel Laravel Package

dcat/easy-excel

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm the baseline

composer require dcat/easy-excel

The package is a thin, fluent wrapper around PhpSpreadsheet (pulled in transitively). Requirements you actually care about in a container:

  • PHP 7.2+ (works on 8.x, see Gotchas)
  • ext-zip, ext-xml, ext-gd — a missing ext-zip is the #1 "works locally, explodes in Docker" failure

Laravel auto-discovery registers Dcat\EasyExcel\Laravel\ExcelServiceProvider and aliases the Excel facade to Dcat\EasyExcel\Laravel\Facades\Excel. No config file is required to get going. If you'd rather not depend on the facade, the standalone static Dcat\EasyExcel\Excel class does the same job outside the container.

Where to look first

  1. The GitHub README — it's the only real documentation, and it's short enough to read end-to-end in ten minutes.
  2. src/Excel.php — the entry point; every method returns an Exporter or Importer.
  3. src/Exporters/ and src/Importers/ — this is where headings(), download(), store(), toArray(), toCollection() live. When you're unsure whether a method exists, grep here rather than guessing.
  4. Your composer.lock for the resolved phpoffice/phpspreadsheet version — most surprises trace back to it.

First use case: export a report from a controller

use Dcat\EasyExcel\Laravel\Facades\Excel;

public function export()
{
    $rows = Order::query()
        ->where('created_at', '>=', now()->subMonth())
        ->get()
        ->map(fn ($o) => [
            'order_no' => $o->order_no,
            'customer' => $o->customer_name,
            'total'    => $o->total,
            'date'     => $o->created_at->toDateTimeString(),
        ]);

    return Excel::export($rows)
        ->headings(['Order No', 'Customer', 'Total', 'Date'])
        ->download('orders-'.now()->format('Ymd').'.xlsx');
}

Two things to internalise immediately: headings() is positional (it maps by index, not by array key), and download() returns a StreamedResponse-like object that must be returned from the controller action.

And the mirror image — reading an upload:

$rows = Excel::import($request->file('sheet'))->toArray();
// [ ['Order No', 'Customer', ...], ['SO-1001', 'ACME', ...], ... ]

That's the whole pitch: no writers, no readers, no sheet objects, until you need them.

Implementation Patterns

Export: download vs. store vs. hand off

// Inline download (browser)
return Excel::export($rows)->headings($headings)->download('report.xlsx');

// Persist to a disk, then attach/mail/link it
Excel::export($rows)->headings($headings)->store('exports/report.xlsx', 's3');

store() is what you want for scheduled/queued jobs — never download() there. Keep the file path deterministic (exports/{tenant}/{date}.xlsx) so a failed job can be retried and a stale file overwritten rather than accumulating orphans.

Export: multi-sheet workbooks

Pass an array of sheet definitions instead of a flat array of rows when a report has multiple tabs (Summary / Detail / Errors). Verify the exact array shape against the README for your pinned version before wiring it into a job — the multi-sheet syntax is the least obvious part of the API. For anything beyond a plain grid, drop to the underlying objects:

$exporter = Excel::export($rows)->headings($headings);

// Reach into PhpSpreadsheet for widths, freeze panes, number formats
$exporter->sheet()->getColumnDimension('B')->setWidth(30);

return $exporter->download('report.xlsx');

This escape hatch is the real value of a thin wrapper: you get semantics for the 90% case and full PhpSpreadsheet access for the last 10%.

Import: the standard pipeline

There is no Eloquent importer, no validation layer, and no queued-import machinery. You build a three-stage pipeline and keep each stage testable:

$raw = Excel::import($request->file('sheet'))->toArray();

$headings = array_shift($raw);                       // 1. peel the header row

$records = collect($raw)
    ->filter(fn ($row) => filled($row[0] ?? null))   // 2. drop blank tail rows
    ->map(fn ($row) => [
        'order_no' => (string) $row[0],
        'customer' => trim((string) $row[1]),
        'total'    => (float) $row[2],
    ]);

$validated = $records->map(fn ($r) => validator($r, [
    'order_no' => 'required|string|max:64',
    'total'    => 'required|numeric|min:0',
])->validate());

DB::transaction(fn () => Order::upsert($validated->all(), ['order_no'], ['total']));

Notice the array_shift for the header: the importer gives you raw rows including the header, and mapping is on you. Build a small header→column-index map (array_flip($headings)) instead of hard-coding $row[2] — columns move, and a positional index is how you ship a silent data-corruption bug.

Import: choosing a return type

  • ->toArray() — plain arrays, cheapest to reason about, fine for anything under a few thousand rows.
  • ->toCollection() — same data, but you get map/filter/chunk for free and it reads better in Laravel.
  • Multi-sheet files: select the sheet first (->first() for the active one, or ->sheetByName('Detail')), then convert. Calling toArray() on a workbook without selecting a sheet gives you the first sheet, which is often not what you want.

Reading from Laravel disks

The importer wants a real filesystem path, not a Storage URL. For the local driver that's Storage::path($key); for s3 you must copy to a temp file first (Storage::readStream → tmpfile/tempnam). Uploaded files (UploadedFile, an SplFileInfo) are accepted directly.

CSV and other formats

The writer/reader format is inferred from the file extension. .csv export/import works, but CSV means encoding decisions — state UTF-8 with BOM explicitly if the consumer is Excel on Windows, or you'll get mojibake in customer-facing files.

Testing

Fixtures in tests/Fixtures/*.xlsx, Storage::fake('local'), and assert on the parsed output rather than byte-comparing files (PhpSpreadsheet output isn't stable across patch versions). For export tests, store() to the fake disk and re-import the file to round-trip it — that single test catches most column-mapping regressions.

Gotchas and Tips

The maintenance question is the headline risk. The last tagged release is 2022-03-03. That's not a dead package (it's a small wrapper over a stable API), but it means the constraint on phpoffice/phpspreadsheet was written against a 2022-era version. Before adopting it greenfield, run composer why phpoffice/phpspreadsheet and check for conflicts with anything else in your tree that wants a newer PhpSpreadsheet. If you're already in the Dcat Admin ecosystem, this is the natural choice and the constraint is already baked in; if you're starting a fresh Laravel 11/12 app with no Dcat dependency, weigh maatwebsite/excel or using PhpSpreadsheet directly — you're buying convenience, not longevity.

Memory blows up at exactly the wrong size. PhpSpreadsheet loads the whole worksheet into memory, and toArray() then materialises a second copy as PHP arrays. A 50 MB xlsx can easily need 500 MB+. On import, keep memory_limit generous and consider raising it per-job (ini_set at the top of a queued job) rather than globally. For genuinely large files, the honest answer is that this package is not the tool — you need streaming readers or pre-split files.

Numbers lose their identity. Order numbers, phone numbers, ID card numbers, and anything with a leading zero arrive as floats/scientific notation. Symptom: 13800138000 becomes 1.38E+10, 007 becomes 7. On import, cast aggressively and treat identifier columns as strings at the format level. On export, format those columns as text before writing, or the recipient's Excel does the mangling for you.

Dates are not dates. A date cell may come back as an Excel serial integer, a formatted string, or a DateTime depending on the cell's format and how it was written. Never strtotime() blindly. Pin down what your input files actually contain with one debug dump:

dd(Excel::import($path)->first()->toArray());

headings() maps by position, not key. If you pass headings(['A','B']) with a three-column dataset, the third column is written with no header. If you pass more headings than columns, you get blank columns. Keep the headings array and the row-shaping closure physically next to each other in the code so they can't drift.

Empty rows and phantom ranges. Excel files frequently have a trailing block of "empty" rows that PhpSpreadsheet still reports (a stray space, a leftover format). Always filter imported rows for at least one non-blank cell before validating — otherwise a validation error fires on row 5000 and nobody can find the problem.

Formula cells. A cell containing a formula may yield the formula string or the cached computed value depending on how the file was saved. If you're importing files produced by another system, assume cached values exist; if you're importing files freshly generated and never opened in Excel, they may not. Test with a real-world file, not one you generated yourself.

Concurrency on store(). Two queued jobs writing the same path will corrupt each other. Include the job/tenant/user ID in the filename and clean up with a scheduled prune.

Non-ASCII filenames in download() need care in the response headers (RFC 5987 filename*). If users want Chinese filenames, test in the actual target browsers, not just curl.

Debugging tip: when output looks wrong, bypass the wrapper. Grab the underlying writer/reader (->writer(), or the exporter's sheet) and dd() the raw cell values. Almost every "the package is broken" moment is either a stray header row, a numeric format, or a merged cell — all visible in three lines of raw inspection.

Extension point to remember: the wrapper deliberately stops short of styling, charts, images, and conditional formatting. That's not a gap to work around — it's the design. Reach for ->sheet() and the PhpSpreadsheet API the moment you need it, and don't fight the abstraction.

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.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource