os/php-excel
A lightweight PHP package for working with Excel spreadsheets, offering a simple API for generating, reading, and exporting Excel files in Laravel and PHP applications.
Verify what you actually installed first. os/php-excel is a personal fork of the long-abandoned PHPExcel library (repo name abdeltiflouardi/PHPExcel, 5 stars, zero dependents). Before you write a line of code, open vendor/os/php-excel/ and confirm:
composer.json inside the package — check the autoload block. PHPExcel historically shipped a custom autoloader (PHPExcel/Autoloader.php) rather than PSR-0/PSR-4, so you may need an explicit require or a classmap.PHPExcel; 1.8+ uses PHPExcel_ prefixes with an autoloader shim. Your use statements depend on this.PHPExcel_IOFactory file exists — that is your entry point for everything.Minimal happy path:
require_once 'vendor/os/php-excel/PHPExcel/Autoloader.php'; // if not autoloaded
PHPExcel_Autoloader::register();
$reader = PHPExcel_IOFactory::createReader('Excel2007'); // .xlsx
$reader->setReadDataOnly(true);
$book = $reader->load('/path/to/input.xlsx');
$sheet = $book->getActiveSheet();
foreach ($sheet->getRowIterator() as $row) {
$cells = $row->getCellIterator();
$cells->setIterateOnlyExistingCells(false);
foreach ($cells as $cell) {
echo $cell->getCoordinate() . ' = ' . $cell->getValue() . PHP_EOL;
}
}
First real use case: reading a small, trusted .xlsx upload and dumping rows into a database. That is the path of least resistance and where the library is least likely to bite you. Writing is nearly as easy:
$book = new PHPExcel();
$sheet = $book->getActiveSheet();
$sheet->setCellValue('A1', 'Name')->setCellValue('B1', 'Total');
$writer = PHPExcel_IOFactory::createWriter($book, 'Excel2007');
$writer->save('/tmp/out.xlsx');
Where to look first in the source: Classes/PHPExcel/IOFactory.php for the reader/writer registry, Classes/PHPExcel/Worksheet/CellIterator.php for iteration semantics, and Classes/PHPExcel/Shared/Date.php for the date conversion logic you will almost certainly fight with.
Separate the read/transform/write stages into distinct objects. PHPExcel worksheets are mutable and stateful; keeping a workbook alive across a long pipeline is a memory and correctness trap. The reliable shape is: load → extract plain arrays → close/unset the workbook → transform arrays → build a fresh workbook → write.
function readRows(string $path): array {
$reader = PHPExcel_IOFactory::createReaderForFile($path); // sniffs format
$reader->setReadDataOnly(true);
$book = $reader->load($path);
$sheet = $book->getSheet(0);
$rows = $sheet->toArray(null, true, true, false); // no keys, raw values
$book->disconnectWorksheets();
unset($book, $sheet, $reader);
return $rows;
}
createReaderForFile() is the pattern to prefer over hardcoding 'Excel2007' — it handles .xls, .xlsx, .ods, and .csv without you branching, which matters once users start uploading whatever Excel saved.
Use the chunked read filter for large files. The single most valuable feature in the library for day-to-day work:
$reader = PHPExcel_IOFactory::createReader('Excel2007');
$reader->setReadDataOnly(true);
$reader->setReadFilter(new class implements PHPExcel_Reader_IReadFilter {
public function readCell($column, $row, $sheetName = '') {
return $row >= 1 && $row <= 5000; // page through data
}
});
Because .xlsx is a zip of XML, the reader still parses enough to find the sheet, but skipping cells cuts peak memory sharply. Note this only works on row-bounded ranges — reading every cell in every row defeats the purpose.
Writing is a fluent builder; use it that way. setCellValue returns the worksheet, so header/row construction chains cleanly:
$sheet->fromArray($headers, null, 'A1');
$row = 2;
foreach ($records as $r) {
$sheet->setCellValue("A{$row}", $r['name'])
->setCellValue("B{$row}", $r['total'])
->setCellValue("C{$row}", $r['date']);
$sheet->getStyle("B{$row}")->getNumberFormat()->setFormatCode('#,##0.00');
$sheet->getStyle("C{$row}")->getNumberFormat()->setFormatCode('yyyy-mm-dd');
$row++;
}
Batch styling rather than per-cell styling. Every getStyle() call materializes a style object and bloats the XML. Apply ranges once after the data is in:
$sheet->getStyle("A1:C1")->getFont()->setBold(true);
$sheet->getStyle("A2:C{$lastRow}")->getAlignment()->setWrapText(true);
Integration tip — queue the export. For any workbook over a few thousand rows, hand the write off to a queued job and return a signed URL. Synchronous Excel generation inside a web request is the number one source of timeout complaints with this class of library, and PHPExcel's memory profile makes it worse than most.
Validate before you parse. PHPExcel will happily try to open a zip bomb or a mislabeled file. Check extension, MIME, and size at the controller layer, and wrap the load in a try/catch for PHPExcel_Reader_Exception — the failure mode for corruption is an uncaught exception, not a false return.
This is abandoned software — treat it as a liability, not a dependency. PHPExcel was deprecated in favor of PhpSpreadsheet years ago, and a 5-star fork with zero dependents means no security patches, no PHP 8 compatibility guarantees, and no one to file bugs with. If this is a new project, the honest recommendation is PhpSpreadsheet; if you are here because a legacy codebase already uses it, the patterns above will work but pin your PHP version deliberately and consider the migration.
Dates are the classic trap. Excel stores dates as serial numbers. getValue() returns the float; getFormattedValue() returns the display string. Which you want depends on context, and getFormattedValue() on a formula cell can be stale. For real dates use:
$dt = PHPExcel_Shared_Date::excelToDateTimeObject($cell->getValue());
Also watch for timezone drift — excelToDateTimeObject returns a DateTime in the default timezone, and the serial-to-date math has a documented 1900 leap-year quirk for pre-1900 dates.
Formulas are not recalculated on load by default. A .xlsx written by Excel carries cached values; one written by another library often does not. If you need computed results, set $reader->setReadDataOnly(false) and $book->getCalculationEngine() is available, but be aware the PHPExcel calculation engine is incomplete and diverges from Excel on some functions. For anything financial, compute in PHP and write values.
Memory is the limiting factor, not CPU. Peak usage for a 10k-row sheet routinely runs past 100 MB. Mitigations, in order of effectiveness: setReadDataOnly(true), a setReadFilter, disconnectWorksheets() plus unset() the workbook when done, and PHPExcel_Settings::setCacheStorageMethod() with a real backend if you must hold a large book in memory. Raise memory_limit explicitly rather than relying on a default.
getCellIterator() skips empty cells by default. If column position matters, call setIterateOnlyExistingCells(false) first — otherwise a blank cell silently shifts your indexing.
Format codes are a string DSL, not a template. 'yyyy-mm-dd' works; PHP-style 'Y-m-d' does not. Number formats use '0.00', '#,##0', '0%'. A bad format code throws at write time, not at setFormatCode() time, so the error surfaces far from the bug.
The .xls writer has a 65,536 row / 256 column ceiling. Excel 2003 format limits are enforced by PHPExcel_Writer_Excel5 and produce a corrupt file rather than a clean exception in some versions. If you support user-selectable export formats, guard the row count.
CSV needs its own settings. PHPExcel_Reader_CSV defaults to comma delimiter and no BOM detection; European exports with semicolons and UTF-8 BOMs will parse into a single garbled column. Set setDelimiter(';'), setInputEncoding('UTF-8'), and handle the BOM yourself.
Extension point worth knowing: if you need custom formatting or export logic, subclass PHPExcel_Writer_Excel2007 and override writeWorksheet() or register a custom writer via PHPExcel_IOFactory::registerWriter(). It's the cleanest seam in an otherwise monolithic library, and it beats post-processing the generated XML.
Debugging tip: when a file fails to load with a generic exception, PHPExcel_IOFactory::identify($path) will tell you what the library thinks the file is. Mismatches there explain the majority of "it opens in Excel but not here" reports — usually .xls files that are actually HTML tables exported with an .xls extension.
How can I help you explore Laravel packages today?