WooCommerce PDF Invoices
Source |
Private (Premium) — baked from the licensed zip |
Purpose |
Generates the PDF invoice attached to WooCommerce order emails |
Special infra |
None beyond the shared uploads PVC; writes PDFs to a temp dir |
The invoice generator itself, by Andrew Benbow (Chrome Orange), bought from the woocommerce.com marketplace. It renders an HTML template to PDF and attaches the result to WooCommerce order emails. Branding and per-customer content are not configured here — they come from EMS PDF Invoice, which hooks this plugin’s filters.
|
The licence is per site
This is a paid extension. A woocommerce.com subscription covers a fixed number of sites, so a new site needs its own licence entitlement before it can be baked into that site’s image. Confirm the entitlement before adding it to a new build — it is the one part of the invoice stack that cannot be provisioned from git. |
Install — hand-staged licensed artefact
Like Divi and UpdraftPlus, the zip is downloaded from the vendor account and staged by hand in
the build-only packages/ directory, then extracted in the Dockerfile:
unzip -q packages/woocommerce-pdf-invoice-*.zip -d web/app/plugins # -> web/app/plugins/woocommerce-pdf-invoice
Pin the version. The extension is ~31 MB and bundles its own dompdf and mPDF builds, so an
upgrade changes the PDF renderer as well as the plugin. It also carries an undocumented markup
contract that our template depends on (see below). Record the tested version in
packages/README.md and re-run a render check after any bump.
Activated in bootstrap after WooCommerce, and before ems-pdf-invoice, which depends
on it.
Configuration
All settings live in a single serialized option, woocommerce_pdf_invoice_settings, edited
under WooCommerce → PDF Invoice. The per-customer subset worth seeding declaratively:
| Option key | Meaning |
|---|---|
|
Header company block. |
|
Footer registration block. |
|
Logo, stored as an uploads URL and resolved to a filesystem path at render time — so it
presupposes a media-library upload. To declare branding in git instead, use the plugin’s
|
|
Typography and output naming. |
|
Page whose content is appended as a terms-and-conditions sheet. |
|
Two ways to corrupt live data through this option
It errors when the option does not yet exist, so seeding must run after the plugin is activated. Never seed the invoice-number sequence. |
Template override contract
The extension resolves its template as {plugin}/templates/{filename} and filters the
filename through woocommerce_pdf_invoice_filename. It also checks
{active-theme}/pdf_templates/{filename} first — which is unusable here, because the theme
directory is read-only in the image.
A plugin-supplied template therefore has to be reached by a relative path that climbs out of
the extension’s own templates directory. That is what
EMS PDF Invoice does, and why it ships a doctor command: if the
traversal ever fails, the invoice still renders — just with the vendor’s stock design, which is
easy to miss.
The extension’s own CSS class names (pdf_table_row_heading, pdf_orderdetails_header,
pdf_table_cell, pdf_table_row_even/_odd) are part of that contract and are restyled by our
template. They are internal to the vendor and undocumented, which is the main reason the version
is pinned.
Storage
PDFs are written to a temp directory at render time and attached to the outgoing email; they are
not retained as site content. Under a read-only root filesystem the writable /tmp emptyDir
covers this — see Immutable WordPress architecture.
The dompdf font cache
The image bakes dompdf’s user font registry at build time, with
docker/bake-dompdf-font-cache.php. Without it every invoice render fails a
write; with the wrong version of it, invoices do not render at all. Both
failure modes are worth knowing, because the second one looks like the fix for
the first.
dompdf keeps two registries side by side in its font directory:
| File | Role |
|---|---|
|
The bundled fonts, shipped by dompdf. |
|
The user fonts, written at runtime. Not shipped by the plugin. |
There is also a third file, and it is the one that matters:
dompdf_font_family_cache.php, the older format. This is where the plugin’s
DejaVu families actually come from — the same families its template asks for.
FontMetrics::loadFontFamilies() reads the bundled registry, then checks
whether installed-fonts.json is_readable(). When it is not — the state the
plugin ships in — it does not conclude "no user fonts". It takes the
legacy-migration branch, reads dompdf_font_family_cache.php, populates the
font list from it, and then calls saveFontFamilies() to persist the
migration. That last step does an unconditional file_put_contents() into the
plugin’s own read-only directory, and fails:
PHP Warning: file_put_contents(.../woocommerce-pdf-invoice/lib/fonts//installed-fonts.json):
Failed to open stream: Read-only file system in .../dompdf/src/FontMetrics.php on line 97
The invoice still renders. The fonts were loaded; only the attempt to cache that work failed.
|
Do not silence this by shipping an empty An empty file makes
This shipped in image 1.0.9 and was caught on the dev site before production saw it. The lesson generalises: the failing write was the migration trying to persist its work, not the work itself, so suppressing the write by faking its output removes the work as well. |
The build step performs the migration properly, while the filesystem is still
writable — mirroring the loader’s own rule of storing a bare basename for fonts
inside the font directory, so the result stays portable. It refuses to write,
failing the build, if fewer than five families resolve or if dejavu sans is
absent. A plugin upgrade that reshapes the legacy cache therefore stops the
build rather than quietly producing an image whose invoices cannot render.
|
Do not mount a writable volume over the font directory either. It holds the 67 bundled font files dompdf must read, and a mount hides whatever the image put at the path — the same trap the plugins PVC has in the mutable posture. Redirecting the path is not available: dompdf takes it from |