avadim/fast-excel-reader
Lightweight and very fast XLSX Excel Spreadsheet and CSV Reader in PHP
v4.4.2
822K downloads/mo
#3787 most downloaded on Packagist
aVadim483/fast-excel-reader
What this package is like to depend on
Last release 6 days ago
17 Aug 2026
Ships fairly regularly
a new release about every 4 weeks
Rarely documented
notes for 10 of 85 stable releases
Nothing withdrawn
no release was ever pulled
6 years old
85 releases · first in 2020
21 releases in the last 12 months
see the full history below
Release timeline
84 releases · Nov 2020 to Aug 2026Releases
latest 60 of 85-
v4.4.217 Aug 2026Release notes
Open source →Fixed
- 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-58 and 71-81 are reserved for
builtin date formats, several of them localized, so a cell using one carries no format code
of its own. But a file may register a code for such a number - XLS writers do it routinely,
1C among them, and a workbook converted from XLS keeps doing it in XLSX - and it has then
redefined that number. The reader typed such cells by the number alone, so an article number
like043, stored as 43 under the custom pattern000, came back as a timestamp of
1900-02-12 with no error raised. The declared pattern now decides, for the cell value as well
as forformat-categoryof the cell style; a number used without a declaration still means
the builtin format it always did.
Release notes
Open source →Fixed
- 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-58 and 71-81 are reserved for
builtin date formats, several of them localized, so a cell using one carries no format code
of its own. But a file may register a code for such a number - XLS writers do it routinely,
1C among them, and a workbook converted from XLS keeps doing it in XLSX - and it has then
redefined that number. The reader typed such cells by the number alone, so an article number
like
043, stored as 43 under the custom pattern000, came back as a timestamp of 1900-02-12 with no error raised. The declared pattern now decides, for the cell value as well as forformat-categoryof the cell style; a number used without a declaration still means the builtin format it always did.
- A number format the file declares itself is no longer mistaken for the builtin format its
-
v4.4.116 Aug 2026Release notes
Open source →Fixed
- Characters stored as
_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 siblingfast-excel-writerwrites 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. Requiresavadim/fast-excel-helper1.4 or above.
- Characters stored as
-
v4.4.008 Aug 2026Release notes
Open source →Added
- 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/Flysystem read), andExcel::openStream($stream)
reads it from an open stream resource (a URL viafopen(),php://memory, a Flysystem read stream).
Both pick the reader by signature exactly likeopen(), so a string/stream carrying XLSX, XLS or CSV
reads back identically to the same file on disk, and both accept the same$optionsasopen().
The content is copied to a temporary file (honouringExcel::setTempDir()), which is removed on
script shutdown; the stream passed toopenStream()is not closed. See
docs/10-getting-started.md. - Reading cell hyperlinks.
Sheet::getHyperlinks()returns a sheet's hyperlinks keyed by the
cell (or range) they are anchored to, each as['link' => ..., 'location' => ..., 'display' => ..., 'tooltip' => ...]. External targets are resolved from the sheet relationships file; internal links
expose their in-documentlocation. A sheet without hyperlinks returns an empty array. See
docs/16-sheet-metadata.md. - Reading workbook document properties.
Excel::getProperties()returns the core
(docProps/core.xml) and application (docProps/app.xml) properties as one associative array with
normalised keys —creator,lastModifiedBy,created,modified,title,application,
company, ... Only the properties present in the file are returned; a workbook without adocProps
part returns an empty array. See docs/12-advanced-reading.md.
Release notes
Open source →Added
- 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/Flysystem read), andExcel::openStream($stream)reads it from an open stream resource (a URL viafopen(),php://memory, a Flysystem read stream). Both pick the reader by signature exactly likeopen(), so a string/stream carrying XLSX, XLS or CSV reads back identically to the same file on disk, and both accept the same$optionsasopen(). The content is copied to a temporary file (honouringExcel::setTempDir()), which is removed on script shutdown; the stream passed toopenStream()is not closed. See docs/10-getting-started.md. - Reading cell hyperlinks.
Sheet::getHyperlinks()returns a sheet's hyperlinks keyed by the cell (or range) they are anchored to, each as['link' => ..., 'location' => ..., 'display' => ..., 'tooltip' => ...]. External targets are resolved from the sheet relationships file; internal links expose their in-documentlocation. A sheet without hyperlinks returns an empty array. See docs/16-sheet-metadata.md. - Reading workbook document properties.
Excel::getProperties()returns the core (docProps/core.xml) and application (docProps/app.xml) properties as one associative array with normalised keys —creator,lastModifiedBy,created,modified,title,application,company, ... Only the properties present in the file are returned; a workbook without adocPropspart returns an empty array. See docs/12-advanced-reading.md.
- Opening a workbook from a string or a stream.
-
v4.3.026 Jul 2026Release notes
Open source →Changed
- An 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 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.
Release notes
Open source →Changed
- An 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 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.
- An empty (zero-byte) file passed to
-
v4.2.026 Jul 2026Release notes
Open source →Added
- Reading CSV through
Excel::open()— returns a one-sheetCsv\CsvBookexposing the same reading API as XLSX/XLS (key modes, read areas,withHeader(),readColumns(), ...). Format is chosen by signature (OLE2 → XLS, ZIP → XLSX, otherwise CSV); passExcel::open($file, $options)to configure the CSV reader or['format' => 'csv']to force it.openCsv()is unchanged and still returns the low-levelCsv\CsvReader(also viaCsvBook::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).- CSV binary guard — binary input (a NUL byte, or a high share of control characters) is rejected up front with a clear "file appears to be binary" error; UTF-16/UTF-32 text is exempt. New
allow_binaryoption (CsvOptions::setAllowBinary()) turns it off.
Changed
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.- A ZIP that is not an XLSX (a DOCX/PPTX or a plain archive) now fails with "Not an XLSX workbook: the ZIP archive has no xl/workbook.xml" (with a DOCX/PPTX hint) instead of a cryptic "Internal file not found".
Fixed
- CSV: an empty file (or one whose length is a multiple of the read buffer) no longer emits a PHP warning and a phantom trailing row.
- CSV:
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
Release notes
Open source →Added
- Reading CSV through
Excel::open().open()now opens CSV files too, returning aCsv\CsvBook— a normal one-sheet workbook that exposes the sameAbstractSheetreading API (key modes, read areas,withHeader(),readColumns(), ...) as XLSX and XLS. Format is chosen by signature (OLE2 → XLS, ZIP → XLSX, otherwise CSV); passExcel::open($file, $options)to configure the CSV reader or force it with['format' => 'csv']. Throughopen()the default column keys are Excel letters, for parity with XLSX.openCsv()is unchanged and still returns the low-levelCsv\CsvReaderengine (also reachable viaCsvBook::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, whichopen()then reads as CSV.- CSV: binary input (a NUL byte, or a high share of control characters) is now rejected up front with a
clear "file appears to be binary" error instead of failing deep in the parser. UTF-16/UTF-32 text is
exempt. The new
allow_binaryoption (CsvOptions::setAllowBinary()) turns the guard off.
Changed
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.- A ZIP that is not an XLSX (a DOCX/PPTX, or a plain archive) now fails with a clear "Not an XLSX workbook: the ZIP archive has no xl/workbook.xml" message — with a DOCX/PPTX hint — instead of the cryptic "Internal file not found: xl/_rels/workbook.xml.rels".
Fixed
- CSV: an empty file (or one whose length is an exact multiple of the read buffer) no longer emits a PHP warning and a phantom trailing row — it is now correctly read as zero rows.
- CSV:
CsvReader::rewind()now fully resets the read state, so re-reading a file after a partial pass no longer replays a stale buffer.
- Reading CSV through
-
v4.1.025 Jul 2026Release notes
Open source →Fixed
- Built-in date formats (numFmtId 14-22, such as the "short date" code 14) are now rendered
deterministically. Previously the reader overwrote their patterns from the ambient ICU locale
wheneverext-intlwas loaded, so the same file produced different strings depending on the
server locale and on whether the extension was installed (#53).
These codes now resolve to fixed patterns regardless of environment. Formats that a file spells
out explicitly are unaffected.
Added
useLocaleFormats(?string $locale = null)— opt in to locale-dependent rendering of the built-in
date codes (the previous behaviour, now explicit). Pass a locale for reproducible output, or nothing
to use the process default locale. Requiresext-intl.
Full changelog: CHANGELOG.md · по-русски
Release notes
Open source →Fixed
- Built-in date formats (numFmtId 14-22, such as the "short date" code 14) are now rendered
deterministically. Previously the reader overwrote their patterns from the ambient ICU locale
whenever
ext-intlwas loaded, so the same file produced different strings depending on the server locale and on whether the extension was installed (#53). These codes now resolve to fixed patterns regardless of environment. Formats that a file spells out explicitly are unaffected.
Added
useLocaleFormats(?string $locale = null)— opt in to locale-dependent rendering of the built-in date codes (the previous behaviour, now explicit). Pass a locale for reproducible output, or nothing to use the process default locale. Requiresext-intl.
- Built-in date formats (numFmtId 14-22, such as the "short date" code 14) are now rendered
-
v4.0.123 Jul 2026Release notes
Open source →Fixed
- XLS now reports formula text with the leading
=, like XLSX always has. Theffield of a formula
cell has the same shape whichever format the file came from, so code no longer needs to special-case
XLS. Formulas whose text cannot be recovered still reportnull, not"=".
Full changelog: CHANGELOG.md · по-русски
Release notes
Open source →Fixed
- XLS now reports formula text with the leading
=, like XLSX always has. Theffield of a formula cell has the same shape whichever format the file came from, so code no longer needs to special-case XLS. Formulas whose text cannot be recovered still reportnull, not"=".
- XLS now reports formula text with the leading
-
v4.0.022 Jul 2026Release notes
Open source →Added
- 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 same way; the file extension is never
consulted.Excel::openXls()opens a workbook explicitly andExcel::isXls()exposes the test.
Seedocs/21-xls.md.- Values and types, with dates detected through number formats
- Cell styles: fonts, fills, borders, alignment, number formats and palette colours
- Formula text, including shared formulas
- Embedded images
- Multiple worksheets, hidden and very hidden sheets, merged cells, sheet dimensions
- Encrypted workbooks and BIFF5/BIFF7 files are rejected with a message saying which
AbstractBookandAbstractSheet, holding the format-independent half of the readers: read areas,
key modes, result-mode flags, the row generator and everyread*helper. XLSX and XLS share one
implementation of the whole public reading API.withHeader()now accepts an optional list of column names:withHeader(['name', 'birthday']). The
header row is still skipped, but the names come from the list instead of from its values. Names are
positional - the first name goes to the first column of the read area - so no column letters are
involved and the same call works on a sheet whose data does not start atA1. A shorter list renames
only the columns it covers. Supported for XLSX, XLS and CSV. Calling it with no argument is
unchanged. This mirrors the naming ofwriteHeader()in the sibling fast-excel-writer.
Fixed
Sheet::readCellsWithStylesFrom()returned bare cell values instead of values with styles: it called
readCells()rather thanreadCellsWithStyles(), and passed the style key into a bool parameter.Sheet::readCellsWithStyles($styleKey)never narrowed the result to the requested property. The key
was looked up on the nested style, where properties sit inside their group, so'fill-color'- the
example in the method's own docblock - always returned the complete style instead.- Complete cell styles -
getCompleteStyleByIdx(),readCellsWithStyles()and everything built on
them - died withCall to undefined method DOMText::getAttribute()on workbooks whosestyles.xml
is written with indentation, which several writers do.
Changed
- Reading XLSX is about 1.5 times faster. Values, types and peak memory are unchanged.
- The accessors that returned a concrete
Sheetnow returnAbstractSheet, and the fluent setters on
the workbook returnAbstractBook. The objects handed back are unchanged, and a subclass may still
narrow the return type back, so this only affects explicit type declarations in calling code.
Full changelog: CHANGELOG.md · по-русски
Release notes
Open source →Added
- 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 same way; the file extension is never consulted.Excel::openXls()opens a workbook explicitly andExcel::isXls()exposes the test. Seedocs/21-xls.md.- Values and types, with dates detected through number formats
- Cell styles: fonts, fills, borders, alignment, number formats and palette colours
- Formula text, including shared formulas
- Embedded images
- Multiple worksheets, hidden and very hidden sheets, merged cells, sheet dimensions
- Encrypted workbooks and BIFF5/BIFF7 files are rejected with a message saying which
AbstractBookandAbstractSheet, holding the format-independent half of the readers: read areas, key modes, result-mode flags, the row generator and everyread*helper. XLSX and XLS share one implementation of the whole public reading API.withHeader()now accepts an optional list of column names:withHeader(['name', 'birthday']). The header row is still skipped, but the names come from the list instead of from its values. Names are positional - the first name goes to the first column of the read area - so no column letters are involved and the same call works on a sheet whose data does not start atA1. A shorter list renames only the columns it covers. Supported for XLSX, XLS and CSV. Calling it with no argument is unchanged. This mirrors the naming ofwriteHeader()in the sibling fast-excel-writer.
Fixed
Sheet::readCellsWithStylesFrom()returned bare cell values instead of values with styles: it calledreadCells()rather thanreadCellsWithStyles(), and passed the style key into a bool parameter.Sheet::readCellsWithStyles($styleKey)never narrowed the result to the requested property. The key was looked up on the nested style, where properties sit inside their group, so'fill-color'- the example in the method's own docblock - always returned the complete style instead.- Complete cell styles -
getCompleteStyleByIdx(),readCellsWithStyles()and everything built on them - died withCall to undefined method DOMText::getAttribute()on workbooks whosestyles.xmlis written with indentation, which several writers do.
Changed
- Reading XLSX is about 1.5 times faster. Values, types and peak memory are unchanged.
- The accessors that returned a concrete
Sheetnow returnAbstractSheet, and the fluent setters on the workbook returnAbstractBook. The objects handed back are unchanged, and a subclass may still narrow the return type back, so this only affects explicit type declarations in calling code.
- Reading of legacy XLS workbooks (Excel 97-2003, BIFF8).
-
v3.2.021 Jul 2026Release notes
Open source →Bug fixes
Four defects in the sheet reading path, all in methods that had no test coverage.
Sheet::readFirstRowCellsFrom()always threw aTypeError. It forwarded$columnKeysintoreadFirstRowCells(?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, mirroringreadCellsFrom(). The new signature isreadFirstRowCellsFrom(string $areaRange, ?bool $styleIdxInclude = null). No working call can break, since every call to the old signature threw.- Restricting columns while requesting numeric column keys returned only
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. AffectedsetReadArea()andsetReadAreaColumns()combined withKEYS_COL_ZERO_BASED/KEYS_COL_ONE_BASED(and thereforeKEYS_ZERO_BASED/KEYS_ONE_BASED). An explicit column name still takes precedence over the numeric key. Sheet::rewind()discarded its$columnKeysargument, although it is documented as an alias ofreset().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.
Tests
The suite grows from 84 to 262 tests (357 to 757 assertions), added ahead of the fixes as a regression net:
- 105 characterization snapshots covering every
read*method, the fullKEYS_*matrix, result-mode flags, read areas, styles, dates, metadata and degenerate inputs, compared as whole arrays withassertSame. - Dedicated tests for generator semantics, read areas, result modes, merged cells, workbook delegation and streaming memory behaviour.
RESULT_MODE_ROW,TRIM_STRINGS,TREAT_EMPTY_STRING_AS_EMPTY_CELLandKEYS_RELATIVEhad no coverage at all before and are now tested.
Method coverage:
Sheet33.9% → 58.9%,Excel55.6% → 69.4%.Notes
- Reading XLSX files that use namespace-prefixed tags (
<x:row>,<x:c>) still returns an empty result. This is a pre-existing limitation, now documented by a test rather than silently unnoticed. - API reference under
docs/regenerated.
Full changelog: v3.1.0...v3.2.0
Release notes
Open source →Fixed
Four defects, all in methods that had no test coverage.
Sheet::readFirstRowCellsFrom()always threw aTypeError: it forwarded$columnKeysintoreadFirstRowCells(?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 matchreadCellsFrom(). No working call can break, since every call to the old signature threw.- Restricting columns while requesting numeric column keys returned only
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. AffectedsetReadArea()andsetReadAreaColumns()combined withKEYS_COL_ZERO_BASEDorKEYS_COL_ONE_BASED, and therefore alsoKEYS_ZERO_BASEDandKEYS_ONE_BASED. Sheet::rewind()discarded its$columnKeysargument although it is documented as an alias ofreset(): 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.
Added
- A regression suite covering the reading path: characterization snapshots over every
read*method, the fullKEYS_*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_CELLandKEYS_RELATIVEhad no coverage at all before.
Known limitations
- XLSX files that use namespace-prefixed tags (
<x:row>,<x:c>) read back as empty.
-
v3.1.016 Jul 2026Release notes
Open source →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 -
v3.0.314 Jul 2026Nothing published for this version
-
v3.0.222 May 2026Nothing published for this version
-
v3.0.112 Mar 2026Nothing published for this version
-
v3.0.014 Feb 2026Nothing published for this version
-
v2.30.024 Jan 2026Nothing published for this version
-
v2.29.123 Jan 2026Nothing published for this version
-
v2.29.018 Jan 2026Nothing published for this version
-
v2.28.114 Dec 2025Nothing published for this version
-
v2.28.007 Nov 2025Nothing published for this version
-
v2.27.021 Sep 2025Nothing published for this version
-
v2.26.208 Sep 2025Nothing published for this version
-
v2.26.1no dateNothing published for this version
-
v2.26.001 Jun 2025Nothing published for this version
-
v2.25.103 May 2025Nothing published for this version
-
v2.25.016 Apr 2025Nothing published for this version
-
v2.24.027 Feb 2025Nothing published for this version
-
v2.23.006 Feb 2025Nothing published for this version
-
v2.22.121 Dec 2024Nothing published for this version
-
v2.22.030 Nov 2024Nothing published for this version
-
v2.21.101 Nov 2024Nothing published for this version
-
v2.21.027 Oct 2024Nothing published for this version
-
v2.20.012 Oct 2024Nothing published for this version
-
v2.19.022 Sep 2024Nothing published for this version
-
v2.18.215 Sep 2024Nothing published for this version
-
v2.18.128 Aug 2024Nothing published for this version
-
v2.18.011 Aug 2024Nothing published for this version
-
v2.17.129 Jun 2024Nothing published for this version
-
v2.17.017 Jun 2024Nothing published for this version
-
v2.16.007 Jun 2024Nothing published for this version
-
v2.15.521 May 2024Nothing published for this version
-
v2.15.411 Apr 2024Nothing published for this version
-
v2.15.310 Apr 2024Nothing published for this version
-
v2.15.210 Apr 2024Nothing published for this version
-
v2.15.107 Apr 2024Nothing published for this version
-
v2.15.006 Apr 2024Nothing published for this version
-
v2.14.207 Mar 2024Nothing published for this version
-
v2.14.105 Mar 2024Nothing published for this version
-
v2.14.007 Dec 2023Nothing published for this version
-
v2.12.003 Nov 2023Nothing published for this version
-
v2.11.025 Oct 2023Nothing published for this version
-
v2.10.029 Sep 2023Nothing published for this version
-
v2.9.209 Sep 2023Nothing published for this version
-
v2.9.106 Sep 2023Nothing published for this version
-
v2.9.006 Sep 2023Nothing published for this version
-
v2.8.429 Aug 2023Nothing published for this version
-
v2.8.329 Aug 2023Nothing published for this version
-
v2.8.228 Aug 2023Nothing published for this version
-
v2.8.125 Aug 2023Nothing published for this version
-
v2.8.024 Aug 2023Nothing published for this version
-
v2.7.011 Aug 2023Nothing published for this version