portphp/spreadsheet
Spreadsheet reader and writer for PortPHP, built on PhpSpreadsheet. Import Excel and CSV files into PHP applications and export data to XLSX, ODS, or CSV through a consistent, extensible API.
portphp/spreadsheet is a thin PortPHP adapter around PhpSpreadsheet: it gives you a spreadsheet-backed reader/writer so Excel files can participate in PortPHP workflows instead of hand-writing IOFactory loops. Install it with Composer; it pulls in phpoffice/phpspreadsheet and the PortPHP core interfaces.
The fastest first use case is “read an uploaded XLSX/ODS/XLS file as associative arrays keyed by the header row”:
use Port\Spreadsheet\Reader\SpreadsheetReader;
use Port\Workflow;
$reader = new SpreadsheetReader(__DIR__ . '/orders.xlsx');
$reader->setHeaderRowNumber(0); // zero-based header row
// $reader->setSheetName('Orders'); // if reading a specific sheet
// $reader->setFirstDataRowNumber(1); // override automatic data start
$workflow = new Workflow();
$workflow->addReader($reader);
$workflow->addWriter(new class implements \Port\Writer\WriterInterface {
public function prepare(): void {}
public function writeItem(array $item): void { var_dump($item); }
public function finish(): void {}
});
$workflow->process();
You can also iterate the reader directly (foreach ($reader as $row)) when you do not need PortPHP’s filters/converters/writers.
Where to look first:
SpreadsheetReader::getHeader() and the first few rows of a real file; this exposes header and type quirks immediately.Workflow, FilterInterface, ConverterInterface, and WriterInterface.IOFactory and Reader\Xlsx docs, because most surprising behavior comes from PhpSpreadsheet, not this adapter.Use portphp/csv instead when the input really is plain CSV and files are large; this package is most useful when you need XLSX/ODS/XLS support or multi-format detection.
Validated import pipeline. The normal pattern is reader → converters → filters → domain writer. Converters run before filters in a PortPHP workflow, so normalize values first, then validate them:
$workflow->addConverter(new class implements \Port\Converter\ConverterInterface {
public function convert($item) {
$item['total'] = (float) str_replace(',', '', $item['total']);
$item['ordered_at'] = new \DateTimeImmutable($item['ordered_at']);
return $item;
}
});
$workflow->addFilter(new class implements \Port\Filter\FilterInterface {
public function filter($item): bool {
return $item['total'] > 0 && $item['email'] !== '';
}
});
For persistence, write a small batching writer that buffers N rows and flushes via Doctrine/PDO/queue. Do not use one INSERT per spreadsheet row.
Database export to XLSX. Use a generator as the reader and let the spreadsheet writer consume arrays:
$reader = (function () use ($pdo) {
foreach ($pdo->query('SELECT id, email, total FROM orders') as $row) {
yield $row;
}
})();
$writer = new SpreadsheetWriter(__DIR__ . '/export.xlsx');
// If your version exposes header control:
$writer->setHeader(['id', 'email', 'total']);
$workflow = new Workflow();
$workflow->addReader($reader);
$workflow->addWriter($writer);
$workflow->process();
Multi-sheet workbooks. A reader represents one sheet at a time. For an import of several sheets, create a reader per sheet name (or reset the sheet and rewind between passes) and run separate workflows with sheet-specific converters/writers. Avoid expecting one workflow to fan out across sheets automatically.
Header-driven mapping. Because rows are keyed by the header row, map spreadsheet columns to domain fields explicitly rather than relying on exact header names. Normalize header keys (trim, lowercase, replace spaces) in a converter or custom reader subclass, and reject duplicate/blank headers early.
Web uploads and queues. Move the uploaded file to a temp path, validate size/MIME/extension, then dispatch an import job. Keep PhpSpreadsheet loading out of the HTTP request for anything beyond tiny files. Treat the spreadsheet as untrusted input: enforce row/column/file-size limits before loading.
Styling and formatting extension. The adapter writes values; it generally does not preserve workbook styling. For production exports, subclass the writer, keep a reference to the underlying PhpOffice\PhpSpreadsheet\Spreadsheet/sheet, set column widths, number formats, freeze panes, and write the header yourself in writeItem()/finish(). This is also the place to add formula-injection protection.
XLSX import of tens of thousands of rows can exhaust PHP memory. Set realistic memory limits, cap uploads, process in chunks, and use CSV streaming for large flat files.setReadDataOnly and cell formatting, dates may appear as DateTime objects or Excel serial floats, and numbers may be strings. Convert explicitly; never trust spreadsheet typing.null where you see a formula in Excel. Decide whether to pre-calculate, reject formula cells, or read formulas as strings.getHeader() and the first two records on a real file.count() on a countable reader may be expensive or force full workbook loading. Do not use it for progress bars on large imports; count source rows or emit progress from the writer instead.finish(). In a workflow this is handled for you; when using a writer manually, the file is not written until the writer’s finish/save step runs. Check output directory permissions and avoid writing over an open source file.\ / ? * [ ] :. Set explicit safe sheet names for exports.portphp/csv or configure a PhpSpreadsheet CSV reader explicitly.portphp/spreadsheet and phpoffice/phpspreadsheet, wrap it behind your own import/export service, and add integration tests with representative XLSX/XLS/ODS files.var_dump, then test the same file through IOFactory::load() directly. If PhpSpreadsheet shows the same odd value, the issue is upstream, not PortPHP.=, +, -, or @ before writing them to spreadsheet cells.How can I help you explore Laravel packages today?