rap2hpoutre/fast-excel
Fast Excel import/export for Laravel, powered by Spout. Export Eloquent models, queries, or collections to xlsx, ods, or csv with a simple API, customize columns via callbacks, and handle large files efficiently. Benchmark-tested for speed.
Architecture fit
FastExcel is a deliberately thin adapter: a single FastExcel class wrapping OpenSpout's reader/writer, plus optional facade and global helper. It is stateless, has no service provider requirements for the core API (the facade alias is optional), publishes no config, and requires no migrations. That makes it a low-coupling choice for a Laravel application — it slots in behind a controller, an artisan command, or a queued job without touching application structure.
The one architectural caveat a TPM should enforce: do not let Rap2hpoutre\FastExcel\FastExcel leak across the codebase. Because it is a thin wrapper over OpenSpout, upstream changes (Box/Spout → OpenSpout, Style namespace moves) surface directly in calling code. Budget a small internal port — an ImportService/ExportService interface with a FastExcel-backed implementation — so swapping to Laravel Excel or a direct OpenSpout integration later is a single-class change rather than a codebase-wide refactor.
Integration feasibility
Install is one composer line; a working export is roughly five lines of code. The README is unusually complete for a package this size, covering multi-sheet, CSV delimiters/encodings, per-column types, styling, row limits, and the memory characteristics of every code path. Realistic effort:
The dominant effort is not integration — it is the validation, error-handling, and job orchestration that FastExcel intentionally does not provide.
Technical risk
The package's stated position is "simple tasks only," and the risk profile follows directly from that. The failure modes are developer-induced rather than package bugs, but they are severe and easy to trip:
| Risk | Severity | Notes |
|---|---|---|
| Materialized collections defeat streaming | High | User::all() before export() loads everything; README documents >512 MB failure on 1M rows vs ~4 MB with cursor(). A code review omission converts a working feature into an outage. |
| No validation layer | High | Row callbacks receive raw header-keyed arrays. Type coercion, missing columns, and null headers are the caller's problem. Silent data corruption is plausible. |
| Header/column coupling | Medium | Import logic keys off exact header strings. A renamed column upstream yields missing-key warnings or nulls, not a clean error. |
| Date and numeric coercion | Medium | Excel serial dates and leading-zero loss (phone numbers, IDs) require explicit stringValues()/setColumnFormat(). Easy to miss until production data is wrong. |
| Untrusted file upload | Medium | Parsing user-supplied XLSX/ODS introduces zip-bomb and malformed-XML exposure inherited from the underlying reader. Needs upload size caps and isolated processing. |
| Partial-write imports | Medium | Row-by-row User::create() inside the callback is not atomic by default. A mid-file failure leaves a half-imported table unless the caller wraps a transaction or uses staging. |
| Bus factor / governance | Medium | Small contributor base, no vendor SLA. History shows steady maintenance (v5.9 → v5.14 with CI, security policy, issue templates) but continuity is not contractual. |
| Feature ceiling | Low–Medium | No formula evaluation, charts, queued-import machinery, or rich validation. Requirements creep past the wrapper means either raw OpenSpout or Laravel Excel. |
Key questions
Stack fit
FastExcel assumes Laravel plus OpenSpout, which pins PHP 8.1+ for current versions. It composes naturally with the rest of a standard Laravel stack: Eloquent cursor()/lazy() for streaming sources, Storage for landing exported files, queues plus Horizon for long-running imports, LazyCollection::chunk() for batched inserts, and Laravel's validator applied inside the row callback. It is format-compatible where it matters (xlsx, ods, csv, with configurable delimiter, enclosure, and encoding such as GBK) and returns the absolute written path from export(), which makes it straightforward to hand off to storage or attach to a notification.
Migration path
Run this as a feature-by-feature replacement, not a big-bang swap:
Compatibility
Two things to verify before committing:
OpenSpout\... classes. Any existing code written against the older Box\Spout\... namespace will not port cleanly. Confirm the installed OpenSpout major and that Style, Color, and writer options used in examples resolve.hideColumnsPrefixedWith, withSheetContext, writer configuration), pin to a caret-minor and update deliberately. Additions have been additive so far, but a thin wrapper means an upstream OpenSpout major is a coordinated upgrade, not a passive one.Sequencing
| Phase | Scope | Exit criteria |
|---|---|---|
| 0 | Spike: one export from a cursor(), one import via importLazy()->chunk() |
Both complete within memory budget on a production-shaped file |
| 1 | Export pilot behind the adapter, wired to storage (local/S3) and download response | Feature parity with incumbent for that report; peak memory measured |
| 2 | Import pilot with per-row validation, staging table or transaction, and a failed-rows report | Bad rows rejected with actionable errors; no partial writes |
| 3 | Async execution: queued job, progress state, retry and dead-letter handling | Imports survive worker restart; operator can see status |
| 4 | Multi-sheet, CSV encoding, per-column type forcing as required by remaining call sites | Remaining in-scope call sites migrated |
| 5 | Rollout, incumbent decommissioning, runbook and review checklist published | No production call sites on incumbent |
Phase 3 is the one most likely to be underestimated. It is entirely application engineering, and it is the difference between a demo and an operable feature.
Maintenance
Low and predictable. No config file, no database objects, no scheduled tasks, no long-lived state. The recurring cost is dependency hygiene: monitor OpenSpout majors, run composer audit on the normal cadence, and watch this package's releases (the 5.9–5.14 window shows active maintenance, CI coverage, a security policy, and issue/PR templates — all positive signals). Budget a fraction of an engineer per quarter, plus a coordinated upgrade whenever OpenSpout majors.
Support
Support burden is almost entirely about developer misuse rather than package defects. The two failure patterns to institutionalize against are: passing a materialized collection to export() instead of a cursor or generator; and returning a value from an import callback when only the side effect is wanted (which accumulates every row in memory and reintroduces the exhaustion you were avoiding). Both are documented, both are invisible in code review unless someone is looking. Publish a short review checklist: cursor or generator on export; return null or importLazy on import; explicit stringValues/setColumnFormat for identity-like fields; headers treated as a contract.
Scaling
Streaming is genuine — the README's benchmarks (10k × 20: 4 MB generator vs 218 MB Laravel Excel; 1M rows under 512 MB) hold because rows never accumulate. Scaling therefore stops being a memory problem and becomes a throughput and scheduling problem:
max_execution_time long before memory.->chunk(1000) + insert()) rather than per-row create() — the difference is order-of-magnitude on import wall time.limitRows() is available for defensive truncation) before parsing begins.Failure modes
| Mode | Symptom | Mitigation |
|---|---|---|
| Memory exhaustion | 500 with "Allowed memory size exhausted" on large files | Streaming sources only; memory ceiling in the job; alert on the error string |
| Request timeout | Truncated file or gateway timeout on export download | Move to queued job + stored file + notification/download link |
| Partial import | Some rows present, no error surfaced | Transaction or staging table with promote step; idempotency key per import |
| Header drift | Nulls or Undefined array key in row callbacks |
Validate the header row up front; fail fast with a named missing column |
| Type coercion | Leading zeros stripped, IDs changed, dates shifted | stringValues() + setColumnFormat(); golden-file tests asserting cell types |
| Encoding corruption | Mojibake on CSV with non-UTF-8 source | configureCsv(delimiter, enclosure, encoding); assert round-trip in tests |
| Untrusted file abuse | Worker hangs or memory spike on a crafted XLSX/ODS | Size cap, format allowlist, parse in an isolated queue with limits |
| Temp/disk leak | Disk fills over weeks | Cleanup job for export artifacts; monitor volume |
Ramp-up
Genuinely fast: a mid-level Laravel developer is productive in half a day on the README alone, and the API is small enough to hold in working memory. The real ramp-up is organizational, not individual — getting the team to internalize when FastExcel is appropriate. Position it explicitly as the tool for "simple, high-volume, memory-sensitive tabular I/O," and keep a documented list of requirements that force escalation to Laravel Excel or raw OpenSpout (formulas, charts, styled templates, complex cell-level validation, queued-import UI). Without that boundary written down, the first complex export request will pull the package past its design envelope and the resulting workaround will be more expensive than the feature.
How can I help you explore Laravel packages today?