PHP Classes

File: README.md

Recommend this page to a friend!
  Packages of Roni   Easy-Excel   README.md   Download  
File: README.md
Role: Documentation
Content type: text/markdown
Description: Documentation
Class: Easy-Excel
Export and read XLSX files with a PHP Go extension
Author: By
Last change:
Date: 6 days ago
Size: 7,396 bytes
 

Contents

Class file image Download

easy-excel

A PHP spreadsheet extension written in Go for FrankenPHP, exposing a PhpSpreadsheet-compatible API while doing all heavy lifting (XML generation, ZIP compression, parsing, formulas) in compiled Go via excelize.

Target: PhpSpreadsheet's ergonomics at OpenSpout-class (constant) memory and a multiple of both libraries' throughput. Design rationale, bottleneck analysis and measured claims live in PLAN.md; API coverage in COMPAT.md; formula coverage in FORMULAS.md; the raw extension API (no composer package needed) in NATIVE.md; known gaps in MISSING.md.

How it works

PhpOffice\PhpSpreadsheet\* (aliases)        ? your existing code, unchanged
        ?
EasyExcel\Compat\* PHP shim                 ? tiny objects, write-behind row buffer
        ?  flat easy_excel_*() calls, batched rows (1 CGO call / 512 rows)
Go extension: handle registry ? excelize    ? StreamWriter, style interner,
                                              admission control, path policy

Key properties:

  • Dual write path ? sequential writes stream at constant memory; an out-of-order write triggers a one-time documented fallback to random access.
  • Load control ? heavy operations pass a weighted semaphore + memory budget; overload surfaces as `EasyExcel\Exception\Overloaded` (map it to HTTP 429 or your queue) instead of OOM-killing the worker.
  • Security defaults ? unzip-bomb limits, path allowlist (`EASY_EXCEL_ALLOWED_PATHS`), capability-style random handles, opt-in CSV injection guard.

Quick start (Docker)

# run the test suites
make test                 # = docker build --target=go-test / --target=php-test

# build FrankenPHP with the extension baked in
make build                # produces the frankenphp-easy-excel image

# or pull the published image
docker pull ghcr.io/xiidea/frankenphp8.5-easy-excel:latest

Use the shim in your app (the image ships it at /opt/easy-excel/php):

{
    "repositories": [{ "type": "path", "url": "/opt/easy-excel/php" }],
    "require": { "easy-excel/polyfill": "*" }
}

use PhpOffice\PhpSpreadsheet\Spreadsheet;          // resolved to EasyExcel\Compat\*
use PhpOffice\PhpSpreadsheet\IOFactory;

$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->fromArray($hugeDataset);                    // batched straight into Go
$sheet->setCellValue('A1', 'Hello');                // buffered, flushed in row batches
IOFactory::createWriter($spreadsheet, 'Xlsx')->save('report.xlsx');
$spreadsheet->disconnectWorksheets();               // frees the native workbook

The aliases stay dormant when the real phpoffice/phpspreadsheet is installed or the extension is missing, so adoption can be incremental.

The shim is optional: the extension's flat easy_excel_*() ABI is usable straight from PHP with no composer dependency ? see NATIVE.md.

Configuration

| Env var | Default | Meaning | |---|---|---| | EASY_EXCEL_MAX_CONCURRENT | max(2, NumCPU) | heavy ops (open/save/scan) in flight | | EASY_EXCEL_ACQUIRE_TIMEOUT_MS | 30000 | wait before Overloaded is raised | | EASY_EXCEL_MEMORY_BUDGET_MB | 512 | estimated live-workbook bytes circuit breaker | | EASY_EXCEL_ALLOWED_PATHS | unset (any local path) | colon-separated base dirs for load/save |

Repository layout

extension/   Go module: registry, limits, exio, compat, core + easy_excel.go (bridge)
php/         Composer package: EasyExcel\Compat shim + alias bootstrap + tests
bench/       Phase-0 rig: identical workloads across 5 libraries
Dockerfile   go-test | php-test | generate | build | runner stages

Development

make test            # Docker: Go (race) + PHP suites
make host-test       # local toolchains, faster iteration
make bench           # baseline all libraries (results.csv)

Notes:

  • `extension/easy_excel.go` is excluded from gofmt: gofmt mangles `//export_php:` directives (underscores aren't recognized as directive names). `make fmt` formats only the pure packages.
  • The bridge compiles only inside the `generate`/`build` Docker stages (needs PHP ZTS headers); the five pure-Go packages build and test anywhere.
  • CI publishes `ghcr.io/<owner>/frankenphp-easy-excel` on main pushes and semver tags (see `.github/workflows/publish.yml`).

Measured performance

Write N rows × 10 mixed columns, one process per run (Docker, PHP 8.5, Apple Silicon; bench/baseline-2026-07-09-php8.5.csv). "Workload memory" is the whole-process peak RSS (VmHWM) minus the idle runtime baseline, so for easy-excel it includes the Go-side buffers that PHP's own peak-memory counter cannot see:

| Library | 100k rows | 1M rows | Workload memory (1M) | |---|---|---|---| | PhpSpreadsheet 5.8 | 23.64s | out of memory (6GB cap) | ~647MB at 100k | | rap2hpoutre/fast-excel | 6.58s | 64.49s | ~3MB | | OpenSpout 4.x | 5.85s | 60.07s | ~3MB | | fast-excel-writer 6.x | 4.29s | 45.73s | ~3MB | | easy-excel (PhpSpreadsheet API) | 1.37s | 11.91s | ~189MB | | easy-excel (native API) | 1.28s | 11.35s | ~186MB |

The two easy-excel lanes produce byte-identical files; the Compat shim costs about 5% over raw EasyExcel\Native calls. PHP-side peak memory stays at 4MB in both lanes ? the ~186MB lives in the Go extension (excelize stream buffers), which still writes 1M rows in a fraction of the memory PhpSpreadsheet needs for 100k. (The 2026-06-11 baseline ran ~1.4× faster across all lanes ? sessions vary, compare ratios, not absolute times.)

Styled report (same data + styled header, two column number formats, widths, freeze pane, auto-filter; write-styled workload, bench/baseline-2026-06-12-php8.5-phase3.csv; that session's unstyled 1M control was 11.9s, matching the table above, so its times are directly comparable):

| Library | 100k rows | 1M rows | Peak PHP memory | |---|---|---|---| | PhpSpreadsheet 5.8 | 26.79s | ? | 670MB at 100k | | easy-excel | 1.93s (13.9×) | 18.5s | 4MB |

Styles stream inline, and since Phase 3 the auto-filter no longer degrades either: the <autoFilter> element is injected into the saved container directly (COMPAT.md §16). In Phase 2 the same styled 1M lane took 121s; the streaming filter and the batch-uniform style resolver cut it to ~1.6× the unstyled write.

Status

Phase 3 (advanced compat) complete, on top of Phase 2's full style graph and worksheet structure:

  • Formulas: `getCalculatedValue()` and bulk `toArray(calculateFormulas: true)` via excelize's engine ? 466/529 PhpSpreadsheet functions, per- function table in FORMULAS.md.
  • Data validation, conditional formatting, images, sheet protection with the PhpSpreadsheet APIs; charts via a native declarative API (`addNativeChart`).
  • Streaming auto-filter: filtered million-row exports no longer degrade ? the filter is injected into the saved container.

Styles applied before their rows are written are inlined into the stream, so the usual "style header ? bulk write" report keeps constant memory and full write speed. See COMPAT.md for the precise matrix and documented divergences. Reproduce the numbers with make bench.