PHP processes more Excel files than any language except maybe Python. Payroll exports, inventory imports, financial reports, data migrations. If your business runs on spreadsheets (and it does), your PHP app touches them constantly.
The standard approach is PhpSpreadsheet: a pure-PHP library that parses XML, builds an in-memory object graph, and promptly devours your server's RAM. It works fine for small files. It falls apart the moment someone uploads a 50,000-row export from SAP. (Don't ask me how I know. 😢)
php_excel takes a different path. It's a PHP extension that wraps LibXL, a commercial C/C++ library purpose-built for reading and writing Excel files. Unlike most alternatives, LibXL handles both modern xlsx (Office 2007+) and the legacy xls binary format (Excel 97-2003), so you don't need separate codepaths for old and new files.
Instead of parsing XML in userland PHP, every cell read and write is a single C function call. In my benchmarks it's 7-10x faster than PhpSpreadsheet, and its memory footprint stays flat while PhpSpreadsheet's climbs past a gigabyte.
Version 2.0 shipped on April 5, 2026. It's the first major release in a long while, and it brings the extension fully into modern PHP.
What Changed in 2.0
The last official release (1.0.2) dates back to 2016. There was an unreleased 1.0.3 with PHP 7 support contributed by community members a few years later, but it never shipped. PHP has changed a lot since then. 2.0 is a ground-up modernization:
PHP 8.3 is the minimum version. Full support for 8.4, 8.5, and the development master branch. The extension builds and tests clean against all four, with zero warnings. On the LibXL side, the minimum is 4.6.0 (released over a year ago), with support up to the latest 5.1.0. I gate newer LibXL features at compile time: you get everything your installed version supports, and the extension still builds clean against 4.6.0.
2.0 adds six new classes to the existing six:
I added methods across every existing class. ExcelBook now handles rich strings, conditional formats, VBA removal, DPI awareness, and picture-as-link support. ExcelSheet got pixel-based column/row sizing, tab colors, active cell management, selection ranges, form controls, data validation, and structured tables.
Every parameter and return type now has proper arginfo. That's 399 typed parameters and 277 typed return values, which means IDE autocompletion and static analysis tools actually work with the extension.
I also cleaned up the internals. Constructors now throw exceptions on error instead of silently returning false. I fixed several use-after-free bugs, null pointer dereferences, and memory leaks, the kind of issues that only surface under heavy load or with AddressSanitizer enabled. Serialization is disabled on all classes (serializing a file handle was never going to end well). The full list is in the ChangeLog.
Why a C Extension?
PHP's process-per-request model means every request starts from scratch. When PhpSpreadsheet opens a 100,000-row file, it parses the underlying XML with SimpleXML (DOM-based, so the entire XML tree lives in memory), then builds a Cell object for every cell with coordinate indexes, style references, and type metadata. In my benchmarks, PhpSpreadsheet 5.5 used about 1,066 MB to load 2 million cells, roughly 0.5 KB per cell for the object graph alone. That's already over a gigabyte for a single 100,000-row sheet. Add formatting, and it climbs further.
LibXL does the same work in optimized C/C++. The XML parsing, ZIP decompression, and cell storage all happen in native code. The extension itself is a thin translation layer: it takes PHP zvals, converts them to C types, calls the LibXL function, and converts the result back. The overhead per cell is a few hundred nanoseconds.
LibXL still uses real memory on the C heap for its internal workbook structures, so this isn't a free lunch. But it uses that memory more efficiently than PhpSpreadsheet's PHP object graph, and it's close to an order of magnitude faster.
Benchmarks
I ran these on PHP 8.4.19 (release build, NTS) against PhpSpreadsheet 5.5.0. Each test writes or reads a mix of integers, floats, and strings across 20 columns. Same data, same machine, measured with hrtime().
Here's the php_excel write test:
$book = new ExcelBook(null, null, true); // xlsx mode
$sheet = $book->addSheet('Bench');
for ($r = 1; $r <= $rows; $r++) {
for ($c = 0; $c < $cols; $c++) {
if ($c % 3 === 0) {
$sheet->write($r, $c, $r * $cols + $c); // integer
} elseif ($c % 3 === 1) {
$sheet->write($r, $c, "Row{$r}_Col{$c}_Text"); // string
} else {
$sheet->write($r, $c, $r * 1.5 + $c * 0.3); // float
}
}
}
$book->save($outFile);
And the PhpSpreadsheet equivalent:
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
for ($r = 1; $r <= $rows; $r++) {
for ($c = 1; $c <= $cols; $c++) {
if (($c - 1) % 3 === 0) {
$sheet->setCellValue([$c, $r], $r * $cols + $c);
} elseif (($c - 1) % 3 === 1) {
$sheet->setCellValue([$c, $r], "Row{$r}_Col{$c}_Text");
} else {
$sheet->setCellValue([$c, $r], $r * 1.5 + $c * 0.3);
}
}
}
$writer = new Xlsx($spreadsheet);
$writer->save($outFile);
Write results (time / total process memory via VmPeak):
| Rows | Cells | php_excel | PhpSpreadsheet | Speed |
|---|---|---|---|---|
| 1,000 | 20K | 0.05s / 85 MB | 0.45s / 162 MB | 10x |
| 10,000 | 200K | 0.55s / 153 MB | 4.59s / 282 MB | 9x |
| 50,000 | 1M | 2.72s / 508 MB | 24.7s / 790 MB | 9x |
| 100,000 | 2M | 5.37s / 908 MB | 51.1s / 1,415 MB | 10x |
All memory figures are total process RSS (VmPeak from /proc/self/status), including the C heap. LibXL does use real memory for its internal structures, so php_excel isn't "free" on that front. But even counting everything, it uses 40-65% of what PhpSpreadsheet needs and finishes 9-10x faster.
One practical detail: PHP's memory_get_peak_usage() reports 2 MB for php_excel throughout, versus 1,254 MB for PhpSpreadsheet at 100K rows. LibXL allocates on the C heap, invisible to PHP's memory manager and memory_limit. If your PHP-FPM pool has a 128 MB memory_limit, PhpSpreadsheet will OOM on a 10K-row file. php_excel won't.
Read results (time / total process memory via VmPeak):
| Rows | Cells | php_excel | PhpSpreadsheet | OpenSpout |
|---|---|---|---|---|
| 1,000 | 20K | 0.05s / 83 MB | 0.39s / 175 MB | 0.16s / 126 MB |
| 10,000 | 200K | 0.47s / 144 MB | 3.74s / 578 MB | 1.48s / 130 MB |
| 50,000 | 1M | 2.60s / 422 MB | 20.1s / 2,317 MB | 8.00s / 130 MB |
| 100,000 | 2M | 5.19s / 767 MB | 40.8s / 4,501 MB | 16.6s / 130 MB |
Three different profiles. php_excel is fastest but uses memory proportional to the file (LibXL loads the whole workbook). PhpSpreadsheet is slowest and its memory explodes, 4.5 GB for 100K rows. OpenSpout streams row by row, so memory stays flat at 130 MB regardless of size, but it's 3x slower than php_excel.
To be fair to PhpSpreadsheet: it's the most feature-complete pure-PHP Excel library. If you don't need a C extension and your files stay under a few thousand rows, it works fine. Past that, it doesn't.
OpenSpout fills a different niche. It can't do random cell access, conditional formatting, formulas, rich text, form controls, or xls format. If your workload is "read CSV-like data from xlsx," it's a good free choice. If you need actual Excel features, it won't help.
php_excel gives you C library speed with the full Excel feature set. The tradeoff: LibXL requires a commercial license, and installing a C extension takes more effort than composer require. Whether that tradeoff makes sense depends on your workload. If you're generating a 200-row report once a day, PhpSpreadsheet is fine. If you're processing bulk imports, financial data, or anything where users upload files of unpredictable size, the C extension pays for itself quickly.
Code Examples
Creating a workbook and writing data:
$book = new ExcelBook();
$sheet = $book->addSheet('Sales Q1');
// Headers
$boldFont = new ExcelFont($book);
$boldFont->bold(true);
$headerFormat = new ExcelFormat($book);
$headerFormat->font($boldFont);
$headers = ['ID', 'Date', 'Customer', 'Amount', 'Status'];
foreach ($headers as $col => $header) {
$sheet->write(1, $col, $header, $headerFormat);
}
// Data rows
$data = [
[1001, '2026-03-15', 'Acme Corp', 15750.00, 'Paid'],
[1002, '2026-03-16', 'Globex Inc', 8200.50, 'Pending'],
[1003, '2026-03-17', 'Initech', 42000.00, 'Paid'],
];
$dateFormat = new ExcelFormat($book);
$dateFormat->numberFormat(ExcelFormat::NUMFORMAT_DATE);
$row = 2;
foreach ($data as $record) {
$sheet->write($row, 0, $record[0]);
$sheet->write($row, 1, strtotime($record[1]), $dateFormat, ExcelFormat::AS_DATE);
$sheet->write($row, 2, $record[2]);
$sheet->write($row, 3, $record[3]);
$sheet->write($row, 4, $record[4]);
$row++;
}
// Summary formula
$sheet->write($row, 3, '=SUM(D2:D4)');
$book->save('sales-q1.xlsx');
Reading an uploaded file:
$book = new ExcelBook();
$book->loadFile($_FILES['report']['tmp_name']);
$sheet = $book->getSheet(0);
$lastRow = $sheet->lastFilledRow();
$lastCol = $sheet->lastFilledCol();
for ($row = $sheet->firstFilledRow(); $row <= $lastRow; $row++) {
$rowData = $sheet->readRow($row, 0, $lastCol);
// process $rowData
}
Rich text in a single cell:
$book = new ExcelBook();
$sheet = $book->addSheet('Notes');
$richString = $book->addRichString();
$boldFont = new ExcelFont($book);
$boldFont->bold(true);
$richString->addText('Important: ', $boldFont);
$richString->addText('Review before end of quarter.');
$sheet->writeRichStr(1, 0, $richString);
$book->save('notes.xlsx');
Working with conditional formatting:
$book = new ExcelBook();
$sheet = $book->addSheet('Metrics');
// Write some data
for ($row = 1; $row <= 20; $row++) {
$sheet->write($row, 0, rand(0, 100));
}
// Highlight cells above 75 in green
$cf = $book->addConditionalFormat();
$cf->font()->bold(true);
$cf->font()->color(ExcelFont::COLOR_GREEN);
$formatting = $sheet->addConditionalFormatting();
$formatting->addRange(1, 0, 20, 0);
$formatting->addRule(
ExcelConditionalFormat::CELLIS,
$cf,
ExcelConditionalFormat::OPERATOR_GREATERTHAN,
'75'
);
$book->save('metrics.xlsx');
Installation
Linux / macOS
First, get LibXL 4.6.0 or newer from libxl.com. It's a commercial library; there's a trial version that works without a license key (limited to ~300 cells and row 0 is inaccessible).
The easiest path is PIE (PHP Installer for Extensions), the official PECL replacement. I added PIE support as part of 2.0:
pie install iliaal/php-excel \
--with-libxl-incdir=/opt/libxl/include_c \
--with-libxl-libdir=/opt/libxl/lib64
Or build from source:
git clone https://github.com/iliaal/php_excel.git
cd php_excel
phpize
./configure --with-excel \
--with-libxl-incdir=/opt/libxl/include_c \
--with-libxl-libdir=/opt/libxl/lib64
make
sudo make install
Then add to your php.ini:
extension=excel.so
Make sure the LibXL shared library is in your linker path. Either install it to a standard location or set LD_LIBRARY_PATH:
echo "/opt/libxl/lib64" | sudo tee /etc/ld.so.conf.d/libxl.conf
sudo ldconfig
Windows
Pre-built DLLs for PHP 8.3 and 8.4 (both x64 and x86, TS and NTS) are attached to every GitHub release. Download the DLL matching your PHP version and architecture, drop it into your ext directory, and add to php.ini:
extension=excel
You'll also need libxl.dll from the LibXL Windows distribution placed somewhere in your system PATH, or in the same directory as php.exe.
License Key Configuration
If you have a LibXL license, store the credentials in php.ini rather than your source code:
[excel]
excel.license_name="Your Name"
excel.license_key="your-license-key-here"
The extension reads these automatically. Pass null for the license parameters in the ExcelBook constructor and they'll be picked up from the ini settings.
The extension is open source under the PHP-3.01 license. LibXL itself requires a commercial license for production use.
Source on GitHub. Issues, PRs, and stars welcome.