stacks/php-spreadsheets
publicClone
Push over the same URL. A password will not work: create a token under access tokens and use it in place of one.
- src
- tests
- .gitignore 32 B
- composer.json 627 B
- LICENSE.md 1.1 KB
- README.md 3.6 KB
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.phpA 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