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

Simple Excel Laravel Package

spatie/simple-excel

Spatie Simple Excel lets Laravel/PHP apps read and write simple Excel and CSV files with low memory usage via generators. It streams rows as LazyCollections, making large imports and exports easy and efficient.

View on GitHub
Deep Wiki
Context7

Getting Started

Install with Composer — there is no service provider, no config file and nothing to publish:

composer require spatie/simple-excel

Where to look first. The whole package is essentially two classes: Spatie\SimpleExcel\SimpleExcelReader and Spatie\SimpleExcel\SimpleExcelWriter. Skim the README once, then read the two source files (~400 lines each) — that's faster than hunting for API docs, and it makes the fluent methods' interaction (headers → filters → rows) obvious. Check CHANGELOG.md before upgrading, because the writer's streaming API and a few reader options have shifted between major versions.

Key mental model. The reader wraps openspout/spout and returns an Illuminate\Support\LazyCollection. Nothing is loaded into memory; the file is read as you iterate. File type is inferred from the extension (.csv / .xlsx); pass a second argument to create() when the extension is missing or wrong (e.g. an uploaded temp file).

First use case — an import command. The 80% case is "read a spreadsheet someone emailed me and put it in the database":

use Spatie\SimpleExcel\SimpleExcelReader;

SimpleExcelReader::create(storage_path('app/imports/contacts.csv'))
    ->headersToSnakeCase()
    ->trimValues()
    ->getRows()
    ->each(function (array $row) {
        Contact::updateOrCreate(
            ['email' => $row['email']],
            ['first_name' => $row['first_name'], 'last_name' => $row['last_name'] ?? null],
        );
    });

Wrap that in an artisan command (php artisan contacts:import path/to/file.csv) rather than a controller — imports are long-running and should be re-runnable.

Implementation Patterns

Always inspect before importing. A dry-run mode saves hours of debugging malformed client files:

$reader = SimpleExcelReader::create($path);

$headers = $reader->getHeaders();          // ['email', 'first_name', ...]
$preview = $reader->take(5)->getRows()->all();

Use getSheetNames() / hasSheet('Sheet1') and prefer fromSheetName() over fromSheet(); headerOnRow(3) plus trimHeaderRow() handles the classic "three title rows above the real table" Excel file, and noHeaderRow() / useHeaders([...]) handles positional reports.

Map awkward headers instead of scattering array keys everywhere. formatHeadersUsing(fn ($h) => Str::slug($h, '_')) or useHeaders([...]) lets you normalise once and keep the rest of the codebase clean. formatValuesUsing(fn ($value, $key) => ...) does the same for cell contents — ideal for lowercasing emails or stripping currency symbols per column.

Validate per row, collect and continue. A thrown exception inside each() aborts the whole import (and leaves the underlying reader open). Validate into a failure list instead:

$errors = [];
$rows->each(function (array $row, $index) use (&$errors) {
    $v = Validator::make($row, ['email' => 'required|email']);
    if ($v->fails()) {
        $errors[$index + 2] = $v->errors()->all();   // +2 ≈ spreadsheet line number
        return;
    }
    // persist
});

For large imports, batch inside the lazy chain (->chunk(500) then Model::upsert($chunk)) rather than one query per row.

Export with cursor() and a generator. The writer accepts any iterable via addRows(), so you never build an array of models:

$writer = SimpleExcelWriter::create(storage_path('app/exports/users.csv'))
    ->addHeader(['id', 'name', 'email']);

$writer->addRows(
    User::query()->orderBy('id')->cursor()
        ->map(fn (User $u) => ['id' => $u->id, 'name' => $u->name, 'email' => $u->email])
);

$writer->close();   // mandatory for .xlsx

Stream a download straight to the browser for on-demand exports in a controller:

return SimpleExcelWriter::streamDownload('users.xlsx')
    ->addRows(User::query()->cursor()->map(fn ($u) => [...]));

(Older 3.x releases required a trailing ->toBrowser(); recent versions return the streamed response directly.)

Remote files need a local path. Neither class accepts a stream or a filesystem disk, so for S3/uploaded files download to a temp path first:

$temp = tempnam(sys_get_temp_dir(), 'import') . '.xlsx';
Storage::disk('s3')->put($temp, Storage::disk('s3')->get($key)); // or copy()
// ... read, then unlink($temp)

Background exports. Put the export in a queued job that writes to storage/app/exports/{uuid}.xlsx, then expose a signed download route — do not attempt to stream from a queue worker.

Gotchas and Tips

  • .xlsx files are not written until the writer is closed. The file is finalised on close() or garbage collection. In queue workers, Octane and long-lived processes, GC may never run — always call ->close() explicitly, and never return response()->download() on a file before closing.
  • getRows() is single-pass. Don't iterate the same LazyCollection twice or call ->all() on a big file. Call getRows() again if you genuinely need a second pass.
  • Duplicate column names silently collapse. Two columns called email become one key in the row array (last wins). Detect duplicates from getHeaders() and rename via useHeaders()/formatHeadersUsing().
  • Ragged rows. Excel files where cells are blank produce null values; rows shorter than the header set can leave keys missing. Always use $row['x'] ?? null, and preserveEmptyRows() if blank lines carry meaning.
  • Numbers get mangled. Excel numeric cells come back as int/float, so phone numbers, IBANs, ISBNs and ZIP codes lose leading zeros and can hit float precision or scientific notation. Format those columns as Text in the source, or cast defensively and warn the user.
  • Dates are DateTimeImmutable from .xlsx — they break 'date' validation rules, json_encode, and strtotime expectations. Use preserveDateTimeFormatting() to get the display string, or convert explicitly. CSV dates stay plain strings, so imports behave differently per format.
  • Formulas. By default you get the cached computed value, which may be null or stale if the file was generated programmatically (Excel never recalculated it). Use keepFormulas() when you want the =SUM(...) text.
  • Delimiters and BOM. Excel in European locales exports ;-separated CSVs. Specify the delimiter on create()/useDelimiter(), and enable the BOM option when writing CSVs that must open cleanly in Excel with accented characters.
  • headerOnRow() counts non-blank lines only — a stray blank line above the header shifts your index. Verify with the preview pattern above.
  • trimValues() takes a character set, not a suffix. trimValues('.com') will also turn Tom into T. Default trimValues() (whitespace) is almost always what you want.
  • Excel row ceiling is 1,048,576. For month-end dumps of real tables, write CSV (or multiple sheets); the writer will not gracefully handle overflow.
  • Sheet indexes are easy to get wrong — use fromSheetName() after calling getSheetNames(), and hasSheet() to guard against client files missing the expected tab.
  • Escape hatch: getReader() returns the underlying OpenSpout\Reader\ReaderInterface for advanced configuration (encodings, options the wrapper doesn't expose). Configure it before calling getRows(), since iteration captures state.
  • Testing: write to Storage::fake()-adjacent temp paths, then read the output back with SimpleExcelReader and assert on rows — testing the round trip catches header and closing bugs cheaply.
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.
mattfalahe/seat-discord-pings
szabrob/laravel-barion
inventas/laravel-apple-maps
yazidkhaldi/laravel-ilsawn
kusikusicms/routes
jeffersongoncalves/filament-centrifugo
itshayu/laravel-admin-v1
kduma/webprint-service-cli
jeffersongoncalves/laravel-centrifugo
pushery/ses-inbound-mail-for-laravel
pushery/http-api-for-laravel
flareia/php-sdk
mrclay/userland-session
leafs/anchor
leafs/alchemy
leafs/password
leafs/session
leafs/date
leafs/http
leafs/form