Repository navigation
The documents this library writes are valid OOXML, a gate keeps them that way, and 0.3.1 carries it - #49
Merged
Conversation
The properties of a hyperlink's run were written by hand, because PHPWord's Link element holds one plain string and cannot carry a label with emphasis in it. They were written in the order they came to mind: the weight, the slant, the strike, the underline, the colour, the size and the typeface. w:rPr is the sequence EG_RPrBase in ECMA-376's wml.xsd and not a set, so a reader walks the children in the order they are written and stops at the first one it did not expect. The typeface comes before the weight, and the underline comes after the size, and a document that gets this wrong is one Word refuses to open. Microsoft's own validator reported "The element has unexpected child element 'w:color'" nineteen times across the example documents, every one of them a link whose label was styled. There are none of them now.
Two of them named a position in the schema's sequence, which is a number that goes stale the moment the schema is read again, and one of them said a run can hold every property, which is a claim about the schema rather than about this library.
A `.docx` is Office Open XML, and a document that breaks the schema is a document that is not what it claims to be. Word opens it anyway, because Word is lenient where the schema is not, and nothing else in the suite could see it. The validator is Microsoft's own — the one the Open XML SDK Productivity Tool uses — and it is run over the examples: the whole corpus of documents this library writes, built by `examples/build.php`. A gate over a selection is a gate over the selection. The test skips itself where the .NET SDK is not, and says what is missing; the job that installs the SDK runs it where it is, so the gate cannot be skipped there. A second test asks the validator about a document with an error in it, which is what proves that the gate can fail.
Drawing a preview of each document's first page takes longer than everything else the build does, and the gate that reads the documents wants the documents. The previews are still emptied with the rest of `out/`: a preview of a document that is no longer built is a picture of nothing, which is the same thing the directory is emptied for.
The gate is red when it lands: over the twenty-nine documents the build writes, Microsoft's validator reports a thousand and two errors, and with the four fixes upstream in PHPWord's writer it reports a hundred and fifty-one, of which forty-four belong to the three committed templates and eighty-eight to PHPWord itself.
…e suite The runner image ships a .NET SDK, so the test does not skip itself there and the gate would have failed the test matrix over a document that is nobody's business but the gate's — and the analysis job needs the test job to have passed. The gate has a job of its own, with the SDK version the validator is built for, and the other jobs leave the suite out.
…t the release measures
…elease/0.3.1 # Conflicts: # composer.json # patches.lock.json
…elease/0.3.1 # Conflicts: # composer.lock
…ys how many parts it holds
…oxml-0.3.1 # Conflicts: # CHANGELOG.md
This was referenced Oct 31, 2026
Closed
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



What this is
One pull request for the whole campaign: the documents this library writes are made
valid Office Open XML, a gate keeps them that way, and 0.3.1 carries it.
It supersedes #41,
#42,
#43,
#44 and
#48, and their commits are
in it.
The issue it carries is PHPOffice/PHPWord#2963,
"The documents this library writes are not valid OOXML", and the five pull requests that
fix it upstream are
#2964,
#2965,
#2966,
#2968 and
#2969 — all drafts, all waiting for
a merge by the maintainer.
The documents this library writes were not valid Office Open XML, and nothing in the
suite could see it: the files open, because Word is lenient where the schema is not. A
reader that walks the schema — a validator, a converter, an accessibility tool —
stops at the first child it did not expect.
Microsoft's Open XML SDK validator — the one the Open XML SDK Productivity Tool uses —
reported 1002 errors over the twenty-nine documents
examples/build.phpwrites.This pull request brings that to 0, and adds the gate that keeps it there.
The measure
maintodayThe twenty-nine documents and the three templates were measured again on this branch:
29 documents, 0 errors. 3 templates, 0 errors.
1. The gate
tools/ooxml-validator/is a small console program over the Open XML SDK: it opens adocument, validates it, and exits non-zero on any error. The SDK version is pinned,
because the validator is the oracle the gate is measured against and a gate whose
oracle moves is not a gate.
The test runs it over the whole corpus — the twenty-nine documents the build writes —
rather than over documents the test writes itself, because a gate over a selection is
a gate over the selection. It is a gate and not a report: the test fails on a document
the validator rejects, and a second test asks the validator about a document with an
error in it and insists it is found. Without that, a validator that said "no errors"
about everything would pass the gate.
On a machine without the .NET SDK the test skips itself and says what is missing. A
job of its own installs the SDK, so the gate cannot be skipped in CI — and the jobs
that run the rest of the suite leave it out, so it is one red cross rather than five.
examples/build.php --no-previewsis new: drawing a preview of each document's firstpage takes longer than everything else the build does, and the gate wants the
documents.
2. The patch
PHPWord 1.4.0 writes the children of
w:rPr, ofw:pPr, ofw:tblPr, ofw:sectPrand of
w:stylein an order of its own, writes a page size in twips as a fraction,and writes a few elements where the format does not allow them. Every document this
library writes is a document PHPWord writes.
The fixes are in five pull requests upstream. Until they are released the library
carries them as a patch — the same way it has carried PHPPresentation's fixes until
now.
The project that installs this one has to allow
cweagans/composer-patches. Theplugin applies the patch, and Composer runs it only when the project allows it:
{ "config": { "allow-plugins": { "cweagans/composer-patches": true } } }The README says so, and
markdown-presentationsays it the same way.The patch is against the release, not against the branch. A patch of
1.4.0 → masterwould carry everything upstream has done since 1.4.0 to those files,and one of those changes is a feature rather than a fix: upstream writes a style of
its own for a title depth used without
addTitleStyle(), and its style id collideswith the one this library registers for its own headings. Two styles with one id is a
document Word repairs, and it breaks the reader here. So each file is merged instead —
the released file with our change applied on top of upstream's
masteras the commonancestor — and the result is a patch that carries the fixes and one piece of upstream's
own: the outline level of a heading style, which upstream moved to the end of a
paragraph's properties at the same time as the fixes, and which cannot be carried
without it.
It applies to the released 1.4.0 with no conflict and no fuzz.
The version is held to the 1.4 line (
~1.4.0, not^1.4): the patch is against1.4.0, and a later release of the line would carry a different writer.
3. The templates
invoice.dotx,meridian-qbr.dotxandplain-letterhead.dotxwere written by PHPWordbefore the fixes, so they carried four of its defects inside them: a page size in
twips with a fraction, a style whose properties were in the wrong order, the settings
in the wrong order, and an inline picture in a header. Every document rendered from a
template inherited them — 44 of the 1002 errors were in the templates and not in
the code that writes them.
They are written by the fixed library now, and they score 0 errors each. The header
logo of
meridian-qbr.dotxandplain-letterhead.dotxis the picture the examplescarry today — the old templates carried an earlier one, left behind when the asset
itself changed.
4. The link's run properties
src/Writer/HyperlinkPass.phpwrites thew:rProf a hyperlink's run by hand,because PHPWord's
Linkelement holds one plain string and cannot carry a label withemphasis in it. The children were appended in the order the properties came to mind —
w:b,w:i,w:strike,w:u,w:color,w:sz,w:rFonts— andw:rPris asequence in ECMA-376 and not a set, so the typeface was written last where the schema
names it second, and the underline was written before the colour where the schema names
it after the size. 19 errors, one for every styled link.
The order is the schema's now:
w:rFonts,w:b,w:i,w:strike,w:color,w:sz,w:u.The two comments that said a document with the order wrong is one Word refuses to open
were corrected: Word opens it, because Word is lenient where the schema is not.
5. The template path is bounded like the read path
A template is somebody else's file, and a zip says how its contents are laid out and
nothing about how much room they will take up: forty kilobytes of a part is forty
megabytes of paragraph, and the entry count is whatever the writer felt like.
The read path bounds both —
Reverse\OptionscarriesmaxPartBytes(256 MiB),maxEntries(4096) andmaxStyleDepth(32) — and the write path read a templatewithout any of them.
Template\TemplatePackage::of()opened the caller's archivewith
ZipArchive::open()and nothing else, and read[Content_Types].xmlwhole.The bounds are the read path's, so a document is as welcome as a template:
They are checked against the archive's central directory, which costs one lookup per
entry and no decompression, and every entry is measured — the template is opened
again by PHPWord's own processor, and a part read there is read whole. The archive is
opened with
CHECKCONS, which refuses one whose directory disagrees with itscontents.
A template that is over a bound is refused with a
MarkdownWord\Exception\UnreadableTemplate, which is anInvalidInput: the file isthe caller's to fix. A template that cannot be opened at all keeps the
FileNotWritableit has always had, so nothing that catches it stops working. Acaller who wants the bounds tighter says so:
The measure, on a
.dotxof sixty-five kilobytes whose[Content_Types].xmlinflates to sixty-four MiB:
6. The message that says a document has too many parts reports zero parts
Reverse\Packageclosed the archive before it read the count, and closing an archiveresets
numFilesto zero — so the message said0whatever the document held. Thecount is read before the archive is closed now.
7. The examples build the way the analyser wants them to
Two findings the analyser raises on
examples/build.php: the directory the Markdownsources live in was named twenty times, and the autoloader was loaded with
requirerather than
require_once. Neither changed what the build writes.The audit that came with it
Every place in the repository that builds a
w:rPror aw:pPrby hand was looked at—
BuiltInHeadingPass,WordFields,TocPass,NumberingMerger,ContentControland the example build — and every one of them is right.
The checks
composer checkis green: the manifest, the audit,smoke.php,stress.phpand3960 tests, the gate among them. PHPStan at level 1 over
src,tools,testsand
examples/build.phpreports no errors — noignoreErrors, no baseline, nosuppression.
The release
The changelog gains a heading for the version, which is what the release job reads the
release notes out of, and a link reference for it.
The tag is the maintainer's to cut:
This release carries the patch that puts the documents in the order the schema names
them, the templates written by the fixed library, the gate that keeps them there, the
link's run properties in the right order, and the bounds on what a template may hold.