ReviewOS

stacks/php-spreadsheets

public
Clone

Push over the same URL. A password will not work: create a token under access tokens and use it in place of one.

main
· 2 branches · 1 commit

Mirrored from stacksjs/php-spreadsheets · syncing is switched off

README.md

php-spreadsheets

CSV and xlsx from PHP, with no dependencies and no extensions.

The PHP half of ts-spreadsheets. Both write the exports of the same product from different halves of the stack, so the API is deliberately the same shape in both.

use Stacks\Spreadsheets\Sheet;
use Stacks\Spreadsheets\Spreadsheet;

$revenue = new Sheet('Revenue per day', ['Point', 'Value'], [
    ['2026-08-01', 4250],
    ['2026-08-02', 3980],
]);

Spreadsheet::csv($revenue);                 // a string
Spreadsheet::xlsx([$revenue, $orders]);     // bytes, one tab each
Spreadsheet::store($revenue, 'report.xlsx');

store picks the writer from the extension, because a file called .xlsx holding CSV is a support ticket rather than a flexible API.

Downloads

download returns the bytes and the headers rather than a framework's response object, so it works the same in Laravel, in a plain script, and in a queued job writing to storage.

['bytes' => $bytes, 'headers' => $headers] = Spreadsheet::download($sheets, 'report.xlsx');

Sheets

One sheet or many. A workbook gets a tab per sheet, named after it. CSV has no word for a tab, so several sheets are written one after another with the name on a row of its own, which is something a person scrolling the file can follow where three heading rows silently concatenated is not.

Excel's own naming rules are applied when the workbook is written: 31 characters, none of : \ / ? * [ ], not blank, and unique. A name that breaks one is corrected rather than refused, because "Revenue / cost" is a reasonable thing to call a report and losing the export over the slash is not a reasonable answer to it.

No dependencies, and no zip extension either

ZipArchive is an optional PHP extension. A library whose only job is producing a file should not be why a deploy fails on a box built without it, and the format is small enough to write correctly, so the archive is assembled here. gzdeflate is core.

What the tests assert

That the archive opens. Every xlsx test walks the central directory, inflates each entry and checks it against its own CRC, rather than asserting the bytes begin PK.

That is not defensiveness for its own sake. The TypeScript sibling had 29 passing tests while emitting a workbook no reader on earth could open: it compressed with gzip and stored the result as raw deflate, wrote the compression method into the version field, left every CRC at zero, and finished the archive with 22 zero bytes where the end-of-central-directory record goes. Each of those alone makes the file unopenable. The tests said length > 0 and "starts with PK", and a broken archive satisfies both.

php tests/run.php

A plain runner, so testing needs php and nothing else: no composer install, no vendor directory, no network.

Cells

Strings, integers, floats and null. Anything else is refused by the Sheet constructor rather than stringified into a cell somebody later reads as data.

Text is written as inlineStr. A string in a bare <v> is a shared string index to Excel, which is how the sibling library turned every heading row into either #VALUE! or an unrelated integer.

Floats go through json_encode, not a string cast. Casting uses precision, which is 14 by default and rounds: 0.1 + 0.2 becomes 0.3, and a spreadsheet disagreeing with the report it came from is a bug nobody can explain.

A leading =, +, - or @ in a CSV cell is prefixed with a tab. Those are how a spreadsheet is talked into running a formula out of a downloaded file, and an export of somebody else's data is exactly the file that arrives carrying one.

License

MIT