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.
composer require, so the decision is "buy" on almost every dimension — the real budget question becomes how much product-specific import logic we still own.getRows() returns a Laravel LazyCollection backed by generators, so memory stays flat instead of loading the file into an array. That's a product-defining detail: it enables "upload a 200MB file" as a supported promise rather than a support ticket. take()/skip() let us preview a file (e.g., "show first 5 rows" before the user confirms the mapping) cheaply.noHeaderRow(), useHeaders(), headerOnRow(), trimHeaderRow(), headersToSnakeCase(), formatHeadersUsing(), plus trimValues()/formatValuesUsing() for cell cleanup. That means we can promise "we'll cope with messy supplier spreadsheets" without building a normalisation layer from scratch.SimpleExcelWriter::streamDownload() makes server-side file storage optional — relevant for exports containing PII or tenant data, where "don't persist the file" simplifies retention policy, GDPR scoping, and cleanup cron jobs.getReader() (raw openspout reader), we can defer "we need advanced spreadsheet features" requests and de-risk later by going one layer down — a real option, not a rewrite.Adopt when:
.xlsx only, and may be large enough that memory behaviour matters.getSheetNames(), hasSheet()), formula-value vs. formula-string control (keepFormulas()), or date-cell control (preserveDateTimeFormatting()).Look elsewhere when:
.xls (legacy binary), password-protected workbooks, or embedded images/objects.Adoption gate before committing: the supplied metadata is noisy — Dependents: 0 and a release date of 2026-10-19 are both implausible and suggest an incomplete data snapshot. Verify the actual latest release, PHP/Laravel version support, and commit activity on Packagist/GitHub directly. The upstream dependency (openspout/spout) is the real engine, so confirm its maintenance posture too. Also note this is a Spatie-maintained package with a support-us model rather than an enterprise SLA — acceptable for a commodity dependency, but worth naming as a risk.
To executives: We have a steady stream of requests that boil down to "let users upload a spreadsheet" and "let admins download their data." Both normally mean weeks of engineering to handle the file formats safely, and a permanent maintenance tax every time a customer sends a file that breaks our parser. This is a free, MIT-licensed, widely used Laravel package that already solves the format problem, and solves it in a memory-safe way so large uploads don't take the app down. The result: those features ship in days, the licence cost is zero, and we keep our data in our own systems instead of buying a third-party file-processing service. What we still build — and therefore still control — is the part that's actually differentiating: what the columns mean, how we validate them, and what the user sees when a row fails.
To engineering: Small, idiomatic surface on top of openspout: SimpleExcelReader::create($path)->getRows() gives you a LazyCollection, so it's filter/map/each like the rest of Laravel, and memory stays flat on big files. SimpleExcelWriter mirrors it, with streamDownload() for exports. It handles multiple sheets, header offsets, header/value normalisation, formulas, and date cells. It won't style workbooks or read .xls. getReader() is the escape hatch if we outgrow it. Caveat: SimpleExcelWriter finalises .xlsx files on destruct or explicit close() — call close() in queued jobs, or the file won't be there when the next step reads it.
What we own either way (call it out now, not in the retro): delimiter/encoding detection for real-world files, a validation + error-reporting layer with per-row feedback, queueing and progress for big imports, idempotency and duplicate handling, PII retention for exports, and audit logging. Track import success rate and time-to-first-import as the adoption metrics; if those stay healthy, the build-vs-buy call was right.
How can I help you explore Laravel packages today?