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.
Architecture fit
spatie/simple-excel is a thin, opinionated facade over openspout/openspout that collapses Excel/CSV I/O into two entry points (SimpleExcelReader, SimpleExcelWriter) and returns an Illuminate\Support\LazyCollection on read. That makes it an unusually clean fit for Laravel import/export pipelines: it is a library, not a framework extension — no service provider, no config file, no published assets, no database surface. It slots in behind a queued job, an artisan command, or a controller without imposing structure. The deliberate scope ceiling (simple Excel/CSV; no charts, styling, macros, or complex workbook manipulation) is the main architectural constraint: treat it as the data-movement layer, not the document-generation layer.
Integration feasibility
composer require spatie/simple-excel and you are writing rows. The reader auto-detects .csv vs .xlsx by extension, exposes header controls (noHeaderRow, useHeaders, headerOnRow, trimHeaderRow, headersToSnakeCase, formatHeadersUsing), value normalization (trimValues, formatValuesUsing), pagination (skip/take), sheet selection (fromSheet, fromSheetName, hasSheet, getSheetNames), and escape hatches (getReader, keepFormulas, preserveDateTimeFormatting, preserveEmptyRows). The writer supports streaming downloads (streamDownload), which covers the common "export to browser" use case without touching disk. Integration cost is genuinely low — the work is not wiring the package, it is everything around it.
Technical risk
Risk is concentrated in three places. First, the write path: an xlsx file is not finalized until close() runs or the writer is garbage collected, which is a real hazard in queue workers, Octane, and long-lived processes — an interrupted job can leave a truncated or corrupt artifact with no exception raised. Second, dependency coupling: the package's behavior for edge-case files is inherited from openspout, so upstream major-version shifts and parser quirks propagate. Third, implicit data semantics: dates return DateTimeImmutable objects by default (which serialize badly if leaked into JSON or logs), empty rows are dropped unless explicitly preserved, and formula cells return computed values unless keepFormulas is set. Each of these is a silent-corruption vector rather than a loud failure. The metadata itself carries a caveat worth escalating: "Dependents: 0" is implausible for a package at this download tier, and a last-release date of 2026-10-19 is either a scheduled/future-dated tag or an indexing artifact — both should be verified against Packagist and the repo's tag history before this is cited in any decision record.
Key questions
openspout?maatwebsite/excel or PhpSpreadsheet footprint that this would duplicate or displace?Stack fit
Requires PHP 8.x plus ext-zip, ext-xml, and ext-mbstring (standard on most images, worth asserting in CI). It pulls illuminate/support for LazyCollection, which is a non-issue in Laravel and a mild consideration in standalone PHP services. It is framework-idiomatic without being framework-locked — that matters if any of the import/export logic lives in a shared internal package consumed outside Laravel.
Migration path
If replacing maatwebsite/excel, the migration is not one-to-one: Laravel Excel provides import classes, chunk reading, validation hooks, and styled exports; simple-excel provides rows and lazy iterators. The realistic path is to keep the orchestration concepts and rewrite the row-handling core — map each existing import class to a reader configuration plus a row-processing closure, and move validation into explicit Laravel validators or form requests. If replacing PhpSpreadsheet for read-only workloads, migration is straightforward and usually drops memory usage substantially. If the current solution produces formatted output, do not migrate those paths at all; run both libraries side by side with a clear ownership boundary (simple-excel for data, PhpSpreadsheet for documents). Wrap whichever you use behind a narrow internal interface (RowImporter, RowExporter) so the vendor choice stays reversible.
Compatibility
Version-pin the package and openspout together and let Dependabot surface major bumps for manual review — the package's own major versions have historically tracked upstream breaking changes. Confirm the support matrix against your Laravel and PHP versions at install time rather than assuming; the README's examples are version-current but not a compatibility table. Verify CSV dialect handling (delimiter, enclosure, encoding, BOM) against your actual inbound files early — real-world CSVs exported from Excel on Windows are the most common source of "works in the demo, fails in production" incidents.
Sequencing
trimHeaderRow, headersToSnakeCase, formatValuesUsing) → validation → idempotent persistence, dispatched to a queue with explicit close()/cleanup semantics.streamDownload for interactive exports; chunked, resumable generation for large exports; explicit close() inside a finally block.Maintenance
Low and predictable. Small public API, MIT license, active changelog and tagged releases from a well-known vendor, CI test workflow visible. The maintenance burden is upgrade windows tied to openspout majors, not day-to-day churn. Pin versions, review major bumps manually, and keep the internal adapter thin enough that a swap is a contained change. The anomalous release date in the metadata should be reconciled before it becomes a stale line in a runbook.
Support Community support via GitHub issues with no SLA. There is no vendor escalation path unless a paid Spatie arrangement exists. This is acceptable for a library this small and well-documented, but it means your team owns the failure modes — budget for that explicitly rather than assuming upstream will absorb an incident.
Scaling
Reads scale well: generators plus LazyCollection mean memory stays roughly flat, so a worker can process large files without raising memory_limit. Scaling is then a function of CPU (xlsx parsing is not cheap) and wall-clock time, which argues for queue workers with generous timeouts and horizontal fan-out by file rather than by row. Writes scale less gracefully: each writer holds a temp file and a handle, so concurrency is bounded by worker count and temp-disk capacity, not by memory. For object storage, write locally then move — do not assume the writer can target S3 directly. Add per-tenant or per-file concurrency limits to prevent a bulk upload from saturating the queue.
Failure modes
close() is bypassed by an exception, timeout, or SIGKILL — no exception is raised, the artifact is simply invalid.DateTimeImmutable leaking into JSON responses, logs, or cache, producing unexpected serialization.headerOnRow is wrong, or columns shifted by an off-by-one on the header index.fromSheetName, or silently reading the wrong sheet with fromSheet.Ramp-up Genuinely fast: the README is example-driven and a mid-level Laravel developer can ship a working import or export in under a day. The learning curve is not the package — it is the surrounding discipline: validation, idempotency, error reporting per row, temp-file lifecycle, and progress feedback for long-running jobs. Estimate accordingly; the package is a day, the production-grade import pipeline is a sprint.
How can I help you explore Laravel packages today?