burhan15 / office-pack
Universal Office Document SDK (PHP) — first-party engines
Requires
- php: >=8.1
- ext-dom: *
- ext-openssl: *
- ext-zip: *
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-11 09:05:45 UTC
README
A first-party Office library for PHP and Node.js. Install it into your app and create spreadsheets, Word documents, PowerPoint decks, and PDFs with one API.
The files are standard OOXML (.xlsx, .docx, .pptx) and PDF, so they open in Microsoft Office, Google Sheets, Google Docs, Google Slides, and LibreOffice. This package does not wrap PhpSpreadsheet, ExcelJS, SheetJS, PHPWord, LibreOffice, or OnlyOffice.
Requirements
| Runtime | Version |
|---|---|
| PHP | 8.1+, ext-zip, ext-dom, ext-openssl |
| Node.js | 18+ (ESM) |
Install
composer require burhan15/office-pack
npm install @burhan15/office-pack
import { Office } from '@burhan15/office-pack';
use Vendor\Office\Office;
Spreadsheet
import { Office } from '@burhan15/office-pack'; const workbook = Office.excel().create(); const sheet = workbook.sheet('Sales'); sheet.setCell('A1', 'Product'); sheet.setCell('B1', 'Price'); sheet.setCell('C1', 'Qty'); sheet.setCell('B2', 10); sheet.setCell('C2', 3); sheet.setFormula('D2', '=B2*C2'); await workbook.save('report.xlsx'); const opened = await Office.excel().open('report.xlsx'); opened.calculate(); console.log(opened.sheet('Sales').getCell('D2').value); // 30
<?php require __DIR__ . '/vendor/autoload.php'; use Vendor\Office\Office; $workbook = Office::excel()->create(); $sheet = $workbook->sheet('Sales'); $sheet->setCell('A1', 'Product'); $sheet->setCell('B1', 'Price'); $sheet->setCell('C1', 'Qty'); $sheet->setCell('B2', 10); $sheet->setCell('C2', 3); $sheet->setFormula('D2', '=B2*C2'); $workbook->save('report.xlsx'); $opened = Office::excel()->open('report.xlsx'); $opened->calculate(); echo $opened->sheet('Sales')->getCell('D2')->value; // 30
Calling sheet('Sales') on a new workbook names the first sheet Sales. It does not leave an empty Sheet1 behind.
Formulas
Formulas use Excel-style English (=SUM(A1:A3)). PHP and Node return the same results for:
SUM AVERAGE COUNT COUNTA MIN MAX IF IFS AND OR NOT ROUND ROUNDUP ROUNDDOWN ABS CEILING FLOOR MOD POWER SQRT CONCAT CONCATENATE LEFT RIGHT MID LEN LOWER UPPER TRIM DATE DAY MONTH YEAR TODAY NOW VLOOKUP HLOOKUP XLOOKUP INDEX MATCH SUMIF SUMIFS COUNTIF COUNTIFS AVERAGEIF AVERAGEIFS
Arithmetic (+ - * / ^) and comparisons (= <> < > <= >=) work too. Any other function evaluates to #NAME?.
VLOOKUP and HLOOKUP take a range and a FALSE argument for an exact match:
sheet.setFormula('E2', '=VLOOKUP("banana",A2:C10,2,FALSE)');
Spreadsheet API
| Action | PHP | Node |
|---|---|---|
| Create | Office::excel()->create() |
Office.excel().create() |
| Open | Office::excel()->open($path) |
await Office.excel().open(path) |
| Sheet | $wb->sheet('Sales') |
wb.sheet('Sales') |
| Value | $sheet->setCell('A1', 'Hi') |
sheet.setCell('A1', 'Hi') |
| Formula | $sheet->setFormula('B1', '=A1*2') |
sheet.setFormula('B1', '=A1*2') |
| Many rows | $sheet->fromRows($rows) |
sheet.fromRows(rows) |
| Skip calc on save | $wb->calculateOnSave = false |
Office.excel({ calculateOnSave: false }) |
| Recalculate | $wb->calculate() |
wb.calculate() |
| Save | $wb->save('out.xlsx') |
await wb.save('out.xlsx') |
| HTTP bytes | echo $wb->toString() |
wb.toBuffer() |
| CSV text | Office::csvFromRows($rows) |
Office.csvFromRows(rows) |
| Large file | — | await wb.streamToFile(path) |
withPassword() stores an SDK envelope (AES-256-GCM). PHP and Node can open that file with the same password. Excel’s own password format is not supported.
Word
const doc = await Office.document().create(); doc.addParagraph('Hello {{name}}'); doc.setHeader('Confidential'); doc.setFooter('Page footer'); doc.addPageBreak(); doc.setVariables({ name: 'Ada' }); await doc.save('letter.docx');
$doc = Office::document()->create(); $doc->addParagraph('Hello {{name}}'); $doc->setHeader('Confidential'); $doc->setFooter('Page footer'); $doc->addPageBreak(); $doc->setVariables(['name' => 'Ada']); $doc->save('letter.docx');
PowerPoint
const deck = await Office.presentation().create(); deck.slide(1).addText('Quarterly review', { asTitle: true }).addText('Revenue is up'); await deck.save('deck.pptx');
$deck = Office::presentation()->create(); $deck->slide(1)->addText('Quarterly review')->addText('Revenue is up'); $deck->save('deck.pptx');
In PHP, the first addText() on a slide is the title.
const pdf = await Office.pdf().create(); pdf.addPage('Hello from the SDK'); await pdf.save('note.pdf');
$pdf = Office::pdf()->create(); $pdf->addPage('Hello from the SDK'); $pdf->save('note.pdf');
enableTaggedPdf() writes a page tree with MarkInfo and a language tag. Merge, split, and text redaction are on the PDF object. This is not a veraPDF-certified PDF/UA suite.
HTTP download
import { Office, Mime } from '@burhan15/office-pack'; const wb = Office.excel().create(); wb.sheet('Sales').setCell('A1', 'Hi'); res.writeHead(200, Mime.downloadHeaders('report.xlsx')); res.end(wb.toBuffer());
use Vendor\Office\Mime; $wb = Office::excel()->create(); $wb->sheet('Sales')->setCell('A1', 'Hi'); foreach (Mime::downloadHeaders('report.xlsx') as $name => $value) { header("$name: $value"); } echo $wb->toString();
Verify
From a clone of this repository:
npm test
php tests/php/spreadsheet_test.php
php tests/php/formula_test.php
CLI, after npm install @burhan15/office-pack:
node node_modules/@burhan15/office-pack/packages/cli/src/cli.js inspect report.xlsx node node_modules/@burhan15/office-pack/packages/cli/src/cli.js validate report.xlsx node node_modules/@burhan15/office-pack/packages/cli/src/cli.js health
Documentation
| Topic | Doc |
|---|---|
| Integrator guide | docs/guides/integration.md |
| Excel and Google compatibility | docs/guides/ms-google-compatibility.md |
| Feature matrix | docs/research/feature-matrix.md |
| Architecture | docs/architecture/system-architecture.md |
| Security | SECURITY.md |
License
Apache-2.0. See LICENSE.