Skip to content

fix(polyfill): bind the Compat surface eagerly at bootstrap#3

Open
saifulferoz wants to merge 10 commits into
xiidea:mainfrom
saifulferoz:main
Open

fix(polyfill): bind the Compat surface eagerly at bootstrap#3
saifulferoz wants to merge 10 commits into
xiidea:mainfrom
saifulferoz:main

Conversation

@saifulferoz

Copy link
Copy Markdown
Contributor

No description provided.

saifulferoz and others added 10 commits July 15, 2026 22:58
Lazy aliasing left two holes the autoloader cannot close:

1. PHP never autoloads for parameter/return/instanceof checks. A Compat
   object passed to consumer code type-hinted with the PhpOffice name
   (e.g. `function f(Worksheet $ws)`) threw
   "TypeError: ... must be of type PhpOffice\...\Worksheet,
   EasyExcel\Compat\Worksheet\Worksheet given" unless something had
   happened to reference the class name first.

2. composer prepends its own autoloader, so a polyfill bootstrap loaded
   before vendor/autoload.php silently lost the PhpOffice\* namespace to
   a co-installed real phpoffice/phpspreadsheet.

eagerAliasCompat() now class_aliases every implemented name at bootstrap
(~60 classes, ~12 ms cold, one-time per process), enumerated by scanning
the Compat tree (not .compat-surface.json, which tracks the full upstream
surface including unimplemented classes). The prepended autoloader stays
as the strict-mode tripwire for unimplemented classes. Already-defined
names are skipped; EASY_EXCEL_EAGER=0 restores lazy-only aliasing.

Found migrating a report pipeline whose SheetContext constructor
type-hints Worksheet: creating the context fataled under strict mode.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(polyfill): bind the Compat surface eagerly at bootstrap
Worksheet::getDefaultRowDimension()->setRowHeight() and
getDefaultColumnDimension()->setWidth() were unimplemented, so the only
way to get uniform row heights was one RowDimension per row — and per-row
heights on a streamed sheet replay at save (measured 487 ms save / 215 MB
RSS @10k rows vs 33 ms / 98 MB with the sheet default; a real ERP's
report configs use a uniform rowHeight in 40 of 123 reports).

Native side: SetDefaultRowHeight / SetDefaultColWidth map to excelize
SetSheetProps (sheetFormatPr defaultRowHeight+customHeight /
defaultColWidth). Called before the first streamed row they ride the
worksheet preamble for free; after that the StreamWriter has snapshotted
the preamble, so the call queues a save-time op like any other mutation
of written state (correctness over speed, covered by
TestDefaultDimensionsMidStreamReplay).

Compat side: RowDimension/ColumnDimension accept a null index — as
PhpSpreadsheet's default dimensions do — and route to the new natives;
Worksheet::getDefaultRowDimension()/getDefaultColumnDimension() hand out
the cached instance. setAutoSize on the default column dimension is
state-only, matching PhpSpreadsheet's writer, which never consumes it.

Verified against the built extension: sheetFormatPr lands in the saved
worksheet XML, values read back intact, save time equals the unstyled
baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat(dims): sheet-wide default row height / column width
Saving a report to a stream-wrapper target (gaufrette://, s3://, ...)
failed with "unsupported workbook file format": the shim stages wrapped
targets through tempnam(), which produces no file extension, and
excelize's SaveAs validates the extension. Direct extension-less paths
hit the same wall — PhpSpreadsheet writes to any filename, so consumer
code relies on that parity. (The auto-filter patch path already knew:
it staged via abs+".unpatched.xlsx".)

Shim: stage wrapped targets under tempnam()+".xlsx".
Native: SaveXlsx routes non-workbook-extension paths through a staged
.eexcel.xlsx sibling and renames into place (same directory, so the
rename cannot cross devices); known extensions save directly as before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(save): support extension-less target paths
… eager alias surface

Three PhpSpreadsheet-parity gaps hit by a real ERP report suite:

- Spreadsheet::setValueBinder()/getValueBinder(): the PhpSpreadsheet >= 2.x
  workbook-level binder API. The workbook binder wins over the legacy static
  Cell::setValueBinder(); setCellValue()/fromArray() route through it.

- new Worksheet() may now be constructed detached (both ctor args optional)
  and attached later via the new Spreadsheet::addSheet($sheet, ?$index) —
  the FilterLoopReportRunner/multi-sheet idiom. Title get/set work while
  detached; native-touching calls throw a clear message via workbookHandle().
  rebindParent() rejects cross-workbook moves.

- aliasCompatSurface(): eagerly alias every Compat class at bootstrap. The
  engine skips autoloading for instanceof and typed-parameter checks, so a
  lazily-aliased class that was only ever *returned* (never explicitly
  loaded) failed type hints like function (Worksheet $ws): the alias did not
  exist yet. Lazy aliasing stays for unimplemented classes (strict throw /
  fallback defer).

Validated against the ERP report generator suite under EASY_EXCEL_ALIAS=strict:
19/19 tests pass (previously 6 binder errors + 1 ctor error + latent
type-check failures).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Mirrors PhpSpreadsheet's StringValueBinder (per-type conversion
suppression, DateTime/RichText/Stringable object binding). Callers that
call $spreadsheet->setValueBinder(new StringValueBinder()) previously
hit UnsupportedApiException under strict aliasing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…add-sheet

feat(polyfill): detached Worksheet + addSheet, eager alias surface, value binder, IOFactory registration
…able

The compat-surface CI gate resolves phpoffice/phpspreadsheet fresh; a new
upstream release added Chart\DataTable, which showed up as a NEW uncovered
class vs the frozen baseline and failed the gate on every PR. Regenerated
with tools/compat-surface-diff.php --update-baseline inside the same
compat-surface-deps image (470 missing classes, +1). Deliberate bump per
the COMPAT.md protocol; no Compat behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant