NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
Packagist · #3589 most downloaded on Packagist
Lightweight and very fast XLSX Excel Spreadsheet and CSV Reader in PHP
Last release today
08 Oct 2026
Ships fairly regularly
a new release about every 4 weeks
Rarely documented
notes for 12 of the last 60 stable releases
Nothing withdrawn
no release was ever pulled
6 years old
87 releases · first in 2020
One column per quarter.
Explicit openXlsx(): Excel , openCsvBook(): CsvBook and openCsvReader(): CsvReader factories. CSV factories preserve the constructor options and accep
openXlsx(): Excel, openCsvBook(): CsvBook and openCsvReader(): CsvReaderwrapText="1" and wrapText="true" are recognised.validate() restores the libxml error mode, collects this call's XML errors across all parts,openCsv() continues to return CsvReader in 4.x. A return type change to CsvBook isopenCsvReader() for stable low-level access or openCsvBook() forext-dom is now an explicit Composer requirement: XLSX parsing already uses DOM throughclose() or a disk-backed shared string cache.openXlsx(): Excel, openCsvBook(): CsvBook и openCsvReader(): CsvReader.wrapText="1" и wrapText="true".validate() восстанавливает режим libxml, собирает XML-ошибки текущего вызова по всем частям,openCsv() продолжает возвращать CsvReader в 4.x. В 5.0 планируется возврат CsvBook.openCsvReader(), для API книги и листа —openCsvBook(). Существующие успешные чтения и сигнатуры фабрик сохраняются.ext-dom теперь явно указан в Composer: XLSX-парсер уже использует DOM черезclose() книги или дисковый кэш общих строк.The cached string result of a formula is decoded like any other string. A calculated cell keeps its result in the sheet under the type str , and versi
str, and version 4.4.1 taught the reader to turn_xHHHH_ sequences back into the characters they stand for - but only for shared and inline=B1&C1 over cells holding one, returned the literal text _x000D_ instead of a CR. The typestr.A number format the file declares itself is no longer mistaken for the builtin format its index stands for. The format numbers 14-22, 27-36, 45-47, 50
043, stored as 43 under the custom pattern 000, came back as a timestamp offormat-category of the cell style; a number used without a declaration still meansCharacters stored as _xHHHH_ are decoded back to the character they stand for. XML cannot carry control characters (and normalizes CR), so a spreadshe
_xHHHH_ are decoded back to the character they stand for. XML cannot carry
control characters (and normalizes CR), so a spreadsheet keeps them escaped — a cell holding a CR
is stored as _x000D_, and Excel as well as the sibling fast-excel-writer writes it that way.
The reader used to return the literal text _x000D_, so such a value did not survive a write/read
round trip. Text that literally reads _xHHHH_ is stored as _x005F_xHHHH_ and still comes back
unchanged; both forms are resolved in a single pass, so a decoded value is never decoded twice.
Requires avadim/fast-excel-helper 1.4 or above.Opening a workbook from a string or a stream. Excel::openString($content) reads a workbook held in memory (a database BLOB, an HTTP body, an S3/Flysys
Excel::openString($content) reads a workbookExcel::openStream($stream)fopen(), php://memory, a Flysystem read stream).open(), so a string/stream carrying XLSX, XLS or CSV$options as open().Excel::setTempDir()), which is removed onopenStream() is not closed. SeeSheet::getHyperlinks() returns a sheet's hyperlinks keyed by the['link' => ..., 'location' => ..., 'display' => ..., 'tooltip' => ...]. External targets are resolved from the sheet relationships file; internal linkslocation. A sheet without hyperlinks returns an empty array. SeeExcel::getProperties() returns the coredocProps/core.xml) and application (docProps/app.xml) properties as one associative array withcreator, lastModifiedBy, created, modified, title, application,company, ... Only the properties present in the file are returned; a workbook without a docPropsAn empty (zero-byte) file passed to Excel::open() now fails up front with a clear "File ... is empty" error, instead of being silently opened as a zer
Excel::open() now fails up front with a clear "File ... is empty" error, instead of being silently opened as a zero-row CSV — open() guesses the format by signature and an empty file matches none. Excel::openCsv() is unchanged and still reads an empty file as a valid, zero-row CSV.Reading CSV through Excel::open() — returns a one-sheet Csv\CsvBook exposing the same reading API as XLSX/XLS (key modes, read areas, withHeader() , r
Excel::open() — returns a one-sheet Csv\CsvBook exposing the same reading API as XLSX/XLS (key modes, read areas, withHeader(), readColumns(), ...). Format is chosen by signature (OLE2 → XLS, ZIP → XLSX, otherwise CSV); pass Excel::open($file, $options) to configure the CSV reader or ['format' => 'csv'] to force it. openCsv() is unchanged and still returns the low-level Csv\CsvReader (also via CsvBook::getReader()). CSV has no styles/dates/merged cells/images — those accessors return empty results. See docs/20-csv.md.Excel::isXlsx() — ZIP-signature check (PK\x03\x04).allow_binary option (CsvOptions::setAllowBinary()) turns it off.Excel::open(string $file, $options = []) takes an optional second argument (BC-safe); a file that is neither OLE2 nor ZIP is now read as CSV instead of being pushed into the XLSX reader.CsvReader::rewind() now fully resets the read state, so re-reading after a partial pass no longer replays a stale buffer.Full bilingual changelog: CHANGELOG.md · CHANGELOG.ru.md
Excel::open(). open() now opens CSV files too, returning a Csv\CsvBook
— a normal one-sheet workbook that exposes the same AbstractSheet reading API (key modes, read
areas, withHeader(), readColumns(), ...) as XLSX and XLS. Format is chosen by signature (OLE2 →
XLS, ZIP → XLSX, otherwise CSV); pass Excel::open($file, $options) to configure the CSV reader or
force it with ['format' => 'csv']. Through open() the default column keys are Excel letters, for
parity with XLSX. openCsv() is unchanged and still returns the low-level Csv\CsvReader engine
(also reachable via CsvBook::getReader()). CSV carries no styles, number/date typing, merged cells
or images; those accessors return empty results instead of throwing. See docs/20-csv.md.Excel::isXlsx() — signature check (PK\x03\x04) telling a real XLSX/ZIP package apart from plain
text, which open() then reads as CSV.allow_binary option (CsvOptions::setAllowBinary()) turns the guard off.Excel::open(string $file, $options = []) takes an optional second argument (BC-safe). A file that
is neither an OLE2 nor a ZIP container is now read as CSV instead of being pushed into the XLSX
reader (where it used to fail) — a fix in behaviour for non-spreadsheet input.CsvReader::rewind() now fully resets the read state, so re-reading a file after a partial pass
no longer replays a stale buffer.Built-in date formats (numFmtId 14-22, such as the "short date" code 14) are now rendered deterministically. Previously the reader overwrote their pat
ext-intl was loaded, so the same file produced different strings depending on theuseLocaleFormats(?string $locale = null) — opt in to locale-dependent rendering of the built-inext-intl.Full changelog: CHANGELOG.md · по-русски
XLS now reports formula text with the leading = , like XLSX always has. The f field of a formula cell has the same shape whichever format the file cam
=, like XLSX always has. The f field of a formulanull, not "=".Full changelog: CHANGELOG.md · по-русски
Reading of legacy XLS workbooks (Excel 97-2003, BIFF8). Excel::open() now chooses the reader from the file signature, so XLSX and XLS are opened the s
Excel::open() now chooses the readerExcel::openXls() opens a workbook explicitly and Excel::isXls() exposes the test.docs/21-xls.md.
AbstractBook and AbstractSheet, holding the format-independent half of the readers: read areas,read* helper. XLSX and XLS share onewithHeader() now accepts an optional list of column names: withHeader(['name', 'birthday']). TheA1. A shorter list renameswriteHeader() in the sibling fast-excel-writer.Sheet::readCellsWithStylesFrom() returned bare cell values instead of values with styles: it calledreadCells() rather than readCellsWithStyles(), and passed the style key into a bool parameter.Sheet::readCellsWithStyles($styleKey) never narrowed the result to the requested property. The key'fill-color' - thegetCompleteStyleByIdx(), readCellsWithStyles() and everything built onCall to undefined method DOMText::getAttribute() on workbooks whose styles.xmlSheet now return AbstractSheet, and the fluent setters onAbstractBook. The objects handed back are unchanged, and a subclass may stillFull changelog: CHANGELOG.md · по-русски
Four defects in the sheet reading path, all in methods that had no test coverage.
Four defects in the sheet reading path, all in methods that had no test coverage.
Sheet::readFirstRowCellsFrom() always threw a TypeError. It forwarded $columnKeys into readFirstRowCells(?bool $styleIdxInclude), so even a default call failed. Because the result is keyed by cell address, column keys cannot apply at all — renaming a column would corrupt the address — so the parameter was dropped, mirroring readCellsFrom(). The new signature is readFirstRowCellsFrom(string $areaRange, ?bool $styleIdxInclude = null). No working call can break, since every call to the old signature threw.nulls. With a read area in place the row template was keyed by column letter while values were stored under numeric keys, and the values were then filtered out. Affected setReadArea() and setReadAreaColumns() combined with KEYS_COL_ZERO_BASED / KEYS_COL_ONE_BASED (and therefore KEYS_ZERO_BASED / KEYS_ONE_BASED). An explicit column name still takes precedence over the numeric key.Sheet::rewind() discarded its $columnKeys argument, although it is documented as an alias of reset().Sheet::firstCol() ignored the column bounds of the read area, reporting the first cell of the row as stored in the file. firstRow() was unaffected.The suite grows from 84 to 262 tests (357 to 757 assertions), added ahead of the fixes as a regression net:
read* method, the full KEYS_* matrix, result-mode flags, read areas, styles, dates, metadata and degenerate inputs, compared as whole arrays with assertSame.RESULT_MODE_ROW, TRIM_STRINGS, TREAT_EMPTY_STRING_AS_EMPTY_CELL and KEYS_RELATIVE had no coverage at all before and are now tested.Method coverage: Sheet 33.9% → 58.9%, Excel 55.6% → 69.4%.
<x:row>, <x:c>) still returns an empty result. This is a pre-existing limitation, now documented by a test rather than silently unnoticed.docs/ regenerated.Full changelog: v3.1.0...v3.2.0
Four defects, all in methods that had no test coverage.
Sheet::readFirstRowCellsFrom() always threw a TypeError: it forwarded $columnKeys into
readFirstRowCells(?bool $styleIdxInclude), so even a default call failed. Because the result is
keyed by cell address, column keys cannot apply, and the parameter was removed to match
readCellsFrom(). No working call can break, since every call to the old signature threw.nulls. With a read area in
place the row template was keyed by column letter while values were stored under numeric keys, and
the values were then filtered out. Affected setReadArea() and setReadAreaColumns() combined with
KEYS_COL_ZERO_BASED or KEYS_COL_ONE_BASED, and therefore also KEYS_ZERO_BASED and
KEYS_ONE_BASED.Sheet::rewind() discarded its $columnKeys argument although it is documented as an alias of
reset(): the body assigned to the parameter instead of forwarding it.Sheet::firstCol() ignored the column bounds of the read area, reporting the first cell of the row
as stored in the file. firstRow() was unaffected.read* method,
the full KEYS_* matrix, result-mode flags, read areas, styles, dates, metadata and degenerate
inputs, plus targeted tests for generator semantics, read areas, merged cells and streaming memory
behaviour. RESULT_MODE_ROW, TRIM_STRINGS, TREAT_EMPTY_STRING_AS_EMPTY_CELL and KEYS_RELATIVE
had no coverage at all before.<x:row>, <x:c>) read back as empty.Add stat() to Excel and Sheet, refactor countActualDimension() to str…
Add stat() to Excel and Sheet, refactor countActualDimension() to str…
…eaming
- Sheet::stat() returns rows/cols/cell counts in a single streaming pass,
cached in actualRows/actualCols/cellStat
- Excel::stat() aggregates per-sheet stats with workbook totals
- Rewrite countActualDimension() to a memory-bounded streaming scan that
glues tags across block boundaries and finds the last row via a tail window
- Document the full-scan cost in related methods' PHPDoc
- Add XlsxStatTest covering sheet/workbook stat and consistency with readCells
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Your coding agent can read these notes before it upgrades. Set up the MCP server →