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

Simplexlsx Laravel Package

shuchkin/simplexlsx

SimpleXLSX is a lightweight PHP library for reading and parsing Microsoft Excel .xlsx files without heavy dependencies. It extracts worksheet rows, cell values, dates, and basic formatting, making spreadsheet import simple and fast for PHP applications.

View on GitHub
Deep Wiki
Context7

Getting Started

Install via Composer; the package ships a namespaced class and is PSR-4 autoloaded (no service provider needed):

composer require shuchkin/simplexlsx

Requires ext-zip (and libxml/SimpleXML). Check with php -m | grep -i zip — this is the most common install failure on shared hosting and minimal Docker images (use docker-php-ext-install zip).

The first thing to read is the README's examples for SimpleXLSX::parse(), ->rows(), and ->rowsEx(). The class is small (~1 file); skim the source so you understand the false-on-error contract.

Minimal first use case — a controller that ingests an uploaded sheet:

use Shuchkin\SimpleXLSX;

$path = $request->file('sheet')->getRealPath();

if ( $xlsx = SimpleXLSX::parse($path) ) {
    $rows = $xlsx->rows();          // array of arrays, row 0 = header
    $header = array_shift($rows);
    // ... map and persist
} else {
    return back()->withErrors(['sheet' => SimpleXLSX::parseError()]);
}

parse() never throws on malformed input — it returns false, and the reason lives in SimpleXLSX::parseError(). Wire that into your validation feedback early; it saves hours of "silent failure" debugging.

Implementation Patterns

FormRequest validation. Don't rely on the extension alone. xlsx is a ZIP container, so validate both mime and size:

'file' => ['required','file','max:10240',
    'mimetypes:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/zip,application/octet-stream'],

application/octet-stream appears often (browsers/OS disagree on xlsx mime), so include it or your users will hit false rejections.

Header-mapped rows (the workhorse pattern). Most imports assume row 0 is a header. Normalize it once so downstream code is key-based, not index-based:

$rows   = $xlsx->rows();
$header = array_map(fn ($h) => Str::slug(trim((string) $h), '_'), array_shift($rows));

$records = collect($rows)
    ->filter(fn ($r) => collect($r)->filter(fn ($v) => $v !== '' && $v !== null)->isNotEmpty())
    ->map(fn ($r) => array_combine($header, array_pad($r, count($header), null)));

This also drops trailing empty rows, which Excel adds liberally.

Streaming for large files. ->rows() loads the entire sheet into memory. For anything over a few thousand rows, use the lazy reader so you can process row-by-row and keep memory flat:

foreach ( $xlsx->readRows() as $index => $row ) {
    if ( $index === 0 ) { $header = $row; continue; }
    // validate, accumulate, flush every N rows
}

Pair this with a queued job for user experience — dispatch ImportSheetJob from the controller, store the upload on the local disk, and let the worker stream it.

Chunked persistence. Don't create() per row. Accumulate a buffer and Model::upsert($buffer, ['external_id'], ['name','email']) every 500 rows inside a transaction. Batch DB writes are the single biggest speedup for imports.

Multiple sheets. ->sheetNames() lists tabs; ->setSheet($i) (0-based) or ->getSheet($i) selects one. For "first sheet only" imports be explicit — don't assume the active tab is the data tab users expect.

Rich cell data. ->rowsEx() returns per-cell metadata (type, formatted value, href, format). Use it when you need hyperlinks, detect dates vs strings reliably, or preserve display formatting:

foreach ( $xlsx->rowsEx() as $row ) {
    $href = $row[0]['href'] ?? null;
    $type = $row[0]['type'] ?? null;   // 's', 'n', 'd' etc.
}

In-memory binaries. SimpleXLSX::parseData($binary) lets you parse a file straight from Storage::get() without touching disk — handy in jobs and tests.

Testing. Use Storage::fake('local'), copy a small fixture .xlsx into the fake disk, and assert rows land in the DB. Keep fixtures tiny (2–3 rows) to keep the suite fast; add one "bad file" fixture to cover the parseError() branch.

Gotchas and Tips

  • false is not an exception. Always branch on the return value or wrap the call yourself (throw_unless($xlsx, ...)). parseError() is static and reflects the last failure.
  • Dates are Excel serial numbers, not strings. Convert with SimpleXLSX::unixstamp($serial) (returns a Unix timestamp) then Carbon::createFromTimestamp(...), or manually: Carbon::create(1899,12,30)->addDays($serial). The 1899-12-30 epoch accounts for Excel's leap-year bug. Watch the timezone on convert — serials are naive, treat them as app timezone deliberately.
  • Merged cells only carry a value in the top-left cell; the rest come back empty. If you need to fill merged regions, inspect $xlsx->merged (or similar) and fan the value out — otherwise your row logic sees blanks.
  • Leading zeros and long numbers are lossy. Phone numbers, zip codes, and IDs may arrive as floats ("00123" → 123). Excel only stores what it was told; prefer reading with rowEx() and checking the cell type, and document for users that ID-like columns should be formatted as text in the source file.
  • Formulas: cells usually return Excel's cached result, but if the file was never recalculated the cached value can be stale or the formula string itself. Don't parse business logic out of formulas.
  • readRows() vs rows(): only readRows() is lazy. Mixing them, or calling rows() first, defeats streaming. Also cap ini_set('memory_limit', ...) only as a last resort — prefer streaming.
  • getRealPath() is unreliable for UploadedFile in some SAPI/test scenarios and after certain moves. Prefer $file->store(...) then parse Storage::path(...), or $file->getPathname().
  • Zip bomb / malformed XML risk: cap upload size at the validation layer, and don't enable external entity loading. The library is fairly safe by default, but the file is untrusted input — validate mime and size before parsing.
  • .xls is not supported. This package is xlsx-only; the same author's simplexlsx covers only xlsx. For legacy .xls you need shuchkin/simplexls or PhpSpreadsheet. Detect the extension early and reject with a clear message.
  • Namespace drift: newer releases use Shuchkin\SimpleXLSX; older docs/examples show a global SimpleXLSX. If you get "class not found," check composer.lock and the use statement before blaming the code.
  • Sheet indexing is 0-based, while users count tabs from 1. Off-by-one errors here are common when adding a sheet selector UI.
  • Empty sheet corner: a sheet with only formatting returns rows of empty strings, not []. Always filter empty rows before mapping headers, or array_combine will throw on mismatched counts.
  • Extension point: there's no event system or driver abstraction — if you need custom behavior (number-format mapping, locale-aware date parsing), wrap the class in a small SheetReader service in your app. That's also the right seam for swapping to PhpSpreadsheet later without touching controllers.
  • CSV is out of scope. Handle uploads with a mime sniff: route CSV to Laravel's native fgetcsv/LazyCollection path and xlsx to SimpleXLSX, otherwise users uploading CSVs (very common) hit a confusing parseError().
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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource