NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
Packagist · #2716 most downloaded on Packagist
Lightweight and very fast XLSX Excel Spreadsheet Writer in PHP
Last release 6 days ago
01 Oct 2026
Release timing varies
gaps range from 1 weeks to 3 months
Rarely documented
notes for 11 of the last 60 stable releases
Nothing withdrawn
no release was ever pulled
6 years old
103 releases · first in 2020
One column per quarter.
Rich text now supports subscript, superscript, strikethrough, explicit formatting overrides and fractional font sizes. Row sources can be streamed dir
Rich text now supports subscript, superscript, strikethrough, explicit formatting overrides and fractional font sizes. Row sources can be streamed directly from generators with Sheet::writeIterable().
Sheet::writeIterable() consumes arrays, generators and Traversable row sources without collecting them in memory. The existing writeRows() signature is unchanged.<sub> / <sup> tags and setSubscript(), setSuperscript(), setBaseline() methods (#143).RichText::setStrike() and nested <strike> / <del> tags.setBold(), setItalic() and setStrike(), plus removeUnderline().setSize(10.5) and <s=10.5> / <size=10.5> tags._xlnm._FilterDatabase defined name.addText() and RichTextFragment now accept literal text and escape it automatically. Remove manual htmlspecialchars() calls before passing text or font names to these APIs. For example, pass A & B, not A & B.addTaggedText() decode XML entities once and interpret supported tags. Unknown tags are preserved as literal text.setUnderline(false) still means single underline; use removeUnderline() to disable it.setSize(float $size).See the upgrade guide for details. PHP 7.4 remains supported.
The full local test suite passed on PHP 7.4 and PHP 8.3: 93 tests, 979 assertions on each version. External Excel/VBScript automation was disabled; XLSX XML validation and round-trip checks ran normally.
Fixed XML escaping of rich text and font names, including control characters in cell text and notes; addText() accepts literal text, while tagged input decodes XML entities once (see the upgrade guide)
Added explicit enable/disable arguments to rich text setBold(), setItalic() and setStrike(), plus removeUnderline()
Added fractional rich text font sizes through setSize(float $size) and size tags, with validation of positive finite values
Added Sheet::writeIterable() to consume arrays, generators and traversable row sources without collecting them in memory; writeRows() keeps its existing signature
Added RichText::setStrike() and nested <strike> / <del> tags
Fixed reversed single/double underline selection in RichTextFragment::setUnderline()
Fixed disabling an autofilter leaving a stale _xlnm._FilterDatabase defined name
Fixed invalid workbook XML when sheet names contain XML special characters such as & or < and are referenced by an autofilter, a named range, a print area or print titles, see https://github.com/aVadim483/fast-excel-writer/issues/142
Added subscript and superscript rich text through <sub> / <sup> tags and setSubscript(), setSuperscript(), setBaseline() methods, see https://github.com/aVadim483/fast-excel-writer/issues/143
Rich text tags now restore nested formatting and correctly handle font size tags; changes to fragments are reflected in subsequent XML serialization
The hidden attribute of a column and a row was treated as a flag, not as a value of xs:boolean type. Excel writes it as 0 / 1 , but LibreOffice writes
hidden attribute of a column and a row was treated as a flag, not as a value of xs:boolean type. Excel writes it as 0/1, but LibreOffice writes false/true, and the string 'false' is truthy in PHP, so the check !empty($attributes['hidden']) turned a visible column into a hidden one. A template made or edited in LibreOffice was saved with all its columns hidden, see #140hidden resolves to false gets the default width again – the attribute is removed now instead of being written as hidden="0", which used to suppress the widthSheet::setColVisible($col, true) raised an Undefined array key warning on PHP 8 when the column had no attributes yethidden attribute of a column and a row being treated as a flag, not as xs:boolean – hidden="false", as LibreOffice writes it, was read as "hidden", so visible columns of such a template became hidden in the saved file, see https://github.com/aVadim483/fast-excel-writer/issues/140Shared state between workbooks – with two workbooks open in one process the number formats of the locale ( @money , @DATETIME , …) and the default fon
@money, @DATETIME, …) and the default font of the workbook created last were applied to the other one, so a report could be saved with a foreign currency or date format. StyleManager kept a reference to itself in a static property and the static normalizeFont() / numberFormatStandardized() read the state from there. There was no workaround: unset() of the foreign workbook did not help, the static property held it aliveSheet::_writeRow() kept the cache of cell styles in a static variable common to all sheets of all workbooks. Because the current row is flushed lazily, writing into two workbooks alternately gave the cells of one of them the style indexes of the other, which silently lost their formats=R[-4]C[-1] written to B2) was written to the file as is, and Excel refused to open such a file. It becomes #REF! now, the way Excel does it:, so =SUM(R[-4]C[-1]:RC[-1]) could produce a hybrid like SUM(R[-4]C[-1]:A2). A range is captured as a whole nowStyleManager::normalizeFont() takes an optional second argument – the style manager whose default font is used as the base. The old signature keeps working (the last created workbook is used then), but a caller that knows its own manager should pass itStyleManager::numberFormatStandardized() became a method of the instance (it was private static)=R[-4]C[-1] written to B2) was written as is, and Excel refused to open such a file; now it becomes #REF!, as Excel does=SUM(R[-4]C[-1]:RC[-1]) produced a hybrid like SUM(R[-4]C[-1]:A2), now the range is converted as a whole@money, @DATETIME, etc.) of the last created one won, so a report could be saved with a foreign currency; the same applied to the default fontThe cell type of a pre-calculated formula result – a text result was written without t="str" , so Excel treated it as a number and showed #VALUE! ; bo
t="str", so Excel treated it as a number and showed #VALUE!; boolean and error results are now written as t="b"/t="e" too (#139)null pre-calculated result (['=A1+B1', null]) – the cell was written empty and the formula was lost; now it is written without a cached value, so Excel calculates it on openWriter prevented garbage collection – it registered [$this, 'removeFiles'] as a shutdown function, which held a global reference to every instance, so the destructor never ran and memory grew in long-running/CLI processes (#138)null pre-calculated result (['=A1+B1', null]) – the whole cell was written empty and the formula was lostt="str", so Excel treated it as a number and showed #VALUE!; boolean and error results are now written as t="b"/t="e" too, see https://github.com/aVadim483/fast-excel-writer/issues/139Writer preventing garbage collection – it registered [$this, 'removeFiles'] as a shutdown function, which held a global reference to every instance, so the destructor never ran and memory grew in long-running/CLI processes, see https://github.com/aVadim483/fast-excel-writer/issues/138PHP 8.1+ deprecations from strtolower() / htmlspecialchars() on non-string arguments
Style object can be created directly from an options array – new Style($options)Excel::getSharedStringsRefCount()DataValidation/Conditional objects – applying one object to several ranges kept only the last range (#137)Sheet::setStateVeryHidden() made the sheet visible instead of very hiddenSheet::writeCell() rejected the last valid row (1048576) and column (XFD)Excel::rangeDimensionRelative() collapsed a range given as two associative points to a single cellxmlSpecialChars() now uses ENT_SUBSTITUTE)LC_NUMERIC locale (e.g. de_DE) on PHP 7.4 produced invalid <v>3,14</v>StyleManager read default-font state from the wrong instance)sheetN.xml after removeSheet() + makeSheet()Sheet::writeCells() dropped positional (integer-keyed) cell styles"" and national function names containing regex metacharactersxml:space="preserve")Area::getOffsetAddress() ignored its offset argumentSheet::skipRow(null) skipped no rowsstrtolower()/htmlspecialchars() on non-string argumentsZipArchive handle was not closed if saving threw an exceptionExcel::download() strips CR/LF and quotes from the file name (header injection), sends the correct xlsx MIME type, adds an RFC 5987 filename* and checks headers_sent()Excel::loadImageFile() rejects dangerous stream wrappers (php://, phar://, glob://, zip://, data://, expect://, …)_xlfn/_xlws passes when the formula contains no (sharedStrings.xml is streamed to disk instead of built entirely in memory (much lower peak memory when shared_string is enabled)Sheet::writeRow() hot pathremoveSheet() + makeSheet(), new worksheet files use a monotonic index and never reuse a previous sheetN.xmlNew demos: rich text (demo-14), images & notes (demo-15); errors fixed in other demos, deprecated chart methods replaced
Writer::_writeCell()Charts\Layout::getXMode() and Charts\Layout::getYMode() - fatal TypeError when adding pie/donut chartsRichTextFragment::outXml() - font size set via setSize() was not writtenNothing published for this version
Nothing published for this version
The behavior of the Sheet::setColFormats(), Sheet::setColStyleArray() and Sheet::setColDataStyleArray() methods has been changed when column numbers w
The behavior of the Sheet::setColFormats(), Sheet::setColStyleArray() and Sheet::setColDataStyleArray() methods has been changed when column numbers were used as keys (it was incorrect)
Nothing published for this version
upd: numFmts index calculation; upd: styles.xml for extra features; upd: set style to cells without value;
upd: numFmts index calculation;
upd: styles.xml for extra features;
upd: set style to cells without value;
rename Excel::$style to Excel::$styleManager and other updates for extra features
rename Excel::$style to Excel::$styleManager and other updates for extra features
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 →