Developer Guide
Developer Guide#
Emboss - PDF Invoices and Packing Slips for WooCommerce is built to be extended without forking. This guide covers the hooks, filters, classes, CLI commands, and architectural patterns developers need to customise behaviour, integrate with other plugins, or build add-ons.
Architecture overview#
The plugin follows a strict layered architecture:
src/
├── Plugin.php # Singleton boot, hook registration
├── Lifecycle/ # Activator, Deactivator (uninstall.php at root)
├── Settings/ # SettingsRepository, settings page, setup wizard
├── Document/ # AbstractDocument, Invoice, Receipt, CreditNote, PackingSlip, DocumentFactory
├── PDF/ # GeneratorInterface, DompdfGenerator, Storage
├── Numbering/ # NumberGenerator with DB locking
├── AutoGeneration/ # StatusListener, Subscriptions compat
├── Email/ # EmailAttacher, EmailLink
├── Frontend/ # MyAccountActions, ThankYouPage, DownloadHandler, RateLimiter
├── Admin/ # OrderColumns, OrderMetaBox, OrderActions, BulkActions, BulkDownloadZip, BulkEmail
├── Compatibility/ # WPML, CurrencySwitcher
├── REST/ # InvoiceController
├── Export/ # CsvExporter
├── Tools/ # Backfill
└── CLI/ # WP-CLI command classes
Every PHP file starts with declare(strict_types=1); and defined( 'ABSPATH' ) || exit;. PHPStan level 8 with zero errors. WPCS compliant.
Public symbol prefix#
All public symbols use the epdi_ prefix:
- Options:
epdi_settings,epdi_next_invoice_number,epdi_next_credit_note_number,epdi_version,epdi_setup_complete. - Order meta:
_epdi_invoice_number,_epdi_invoice_display_number,_epdi_document_type,_epdi_generated_date,_epdi_pdf_path,_epdi_pdf_hash. - Hooks/filters:
epdi_*. - REST namespace:
epdi/v1. - Text domain:
easy-pdf-invoices-for-woocommerce. - PHP namespace:
EasyPDFInvoices.
Hooks reference (actions)#
epdi_before_generate#
Fires immediately before a PDF is generated.
do_action( 'epdi_before_generate', WC_Order $order, string $document_type );
Use for: logging, switching locale (handled internally for WPML/Polylang), pre-flight validation.
epdi_after_generate#
Fires after a PDF is successfully generated and saved.
do_action( 'epdi_after_generate', WC_Order $order, string $document_type, string $pdf_path );
Use for: syncing to external systems, restoring locale, audit trail.
epdi_document_type_resolved#
Fires when the document type has been determined for an order.
do_action( 'epdi_document_type_resolved', WC_Order $order, string $document_type );
epdi_number_allocated#
Fires when a sequential number has been allocated to an order.
do_action( 'epdi_number_allocated', WC_Order $order, int $invoice_number, string $display_number );
Use for: sync to accounting tools, audit trail, custom number-based logic.
epdi_before_email_attach#
Fires immediately before a PDF is added to an email’s attachment list.
do_action( 'epdi_before_email_attach', WC_Order $order, string $email_id );
epdi_download_streamed#
Fires after a successful download stream completes.
do_action( 'epdi_download_streamed', string $path );
Use for: download analytics, audit trail.
Filters reference#
epdi_should_generate#
Prevent or force PDF generation for specific orders.
apply_filters( 'epdi_should_generate', bool $should_generate, WC_Order $order );
Example:
add_filter( 'epdi_should_generate', function( $should, $order ) {
if ( $order->get_total() == 0 ) {
return false; // Skip $0 orders
}
return $should;
}, 10, 2 );
epdi_document_type#
Override the auto-resolved document type for a specific order.
apply_filters( 'epdi_document_type', string $type, WC_Order $order );
epdi_pdf_html#
Modify the rendered HTML before it’s passed to DOMPDF.
apply_filters( 'epdi_pdf_html', string $html, WC_Order $order, string $document_type );
Use this for last-mile HTML manipulation when template overrides aren’t enough. Be careful — anything you add lands directly in the PDF and bypasses template escaping.
epdi_pdf_css#
Modify the CSS before it’s inlined into the PDF.
apply_filters( 'epdi_pdf_css', string $css, string $variant, WC_Order $order );
epdi_dompdf_options#
Modify the DOMPDF options array.
apply_filters( 'epdi_dompdf_options', array $options );
Default options (locked down):
array(
'isRemoteEnabled' => false,
'isPhpEnabled' => false,
'isJavascriptEnabled' => false,
'chroot' => ABSPATH,
'fontDir' => EPDI_PLUGIN_DIR . 'vendor/dompdf/dompdf/lib/fonts/',
'tempDir' => get_temp_dir(),
'defaultFont' => 'DejaVu Sans',
)
epdi_paper_size#
Override paper size per order.
apply_filters( 'epdi_paper_size', string $size, WC_Order $order );
Returns one of A4, Letter, or a custom DOMPDF-compatible string.
epdi_paper_options#
Override the full paper options array (orientation, custom dimensions).
apply_filters( 'epdi_paper_options', array $options, WC_Order $order );
epdi_display_number#
Override the formatted display number.
apply_filters( 'epdi_display_number', string $display, int $raw_number, WC_Order $order );
epdi_pdf_filename#
Override the PDF filename used in Content-Disposition headers.
apply_filters( 'epdi_pdf_filename', string $filename, WC_Order $order, string $document_type );
epdi_template_path#
Override the template file path resolution.
apply_filters( 'epdi_template_path', string $path, string $template_name );
epdi_template_data#
Add custom data to the template rendering context.
apply_filters( 'epdi_template_data', array $data, WC_Order $order, string $document_type );
The returned array is extracted into the template scope as $data plus individual variables.
epdi_attach_to_email#
Surgical control over per-email attachment behaviour.
apply_filters( 'epdi_attach_to_email', bool $should_attach, string $email_id, WC_Order $order );
epdi_email_document_type#
Override the document type used for a specific email’s attachment.
apply_filters( 'epdi_email_document_type', string $type, string $email_id, WC_Order $order );
epdi_attachable_email_ids#
Register custom email IDs in the Emails settings tab.
apply_filters( 'epdi_attachable_email_ids', array $ids );
epdi_download_url#
Modify the public download URL for an order.
apply_filters( 'epdi_download_url', string $url, WC_Order $order );
epdi_download_rate_limit / epdi_download_rate_window#
Control the download rate limiter.
apply_filters( 'epdi_download_rate_limit', int $max_requests );
apply_filters( 'epdi_download_rate_window', int $seconds );
Defaults: 10 requests per 60 seconds.
epdi_download_button_label#
Customise the My Account / Thank You / email link label.
apply_filters( 'epdi_download_button_label', string $label, string $document_type, WC_Order $order );
epdi_show_download_button#
Show or hide the download button on a specific surface.
apply_filters( 'epdi_show_download_button', bool $show, WC_Order $order, string $context );
$context is one of my_account, thank_you, email.
epdi_paid_badge_text#
Customise the PAID badge text.
apply_filters( 'epdi_paid_badge_text', string $text, WC_Order $order );
Default: PAID. Could be PAGADO, BEZAHLT, or anything else.
epdi_items_columns#
Modify the columns shown in the items table.
apply_filters( 'epdi_items_columns', array $columns, WC_Order $order, string $document_type );
epdi_item_data#
Modify per-item data before rendering in the items table.
apply_filters( 'epdi_item_data', array $item_data, WC_Order_Item $item, WC_Order $order );
epdi_template_variants#
Register additional template variants.
apply_filters( 'epdi_template_variants', array $variants );
epdi_pdf_generator#
Swap the PDF generator implementation entirely.
apply_filters( 'epdi_pdf_generator', GeneratorInterface $generator );
This is how the upcoming Emboss mPDF companion plugin swaps DOMPDF for mPDF when RTL or CJK content is detected.
Class API#
\EasyPDFInvoices\Document\DocumentFactory#
Resolves and creates document instances.
$factory = \EasyPDFInvoices\Document\DocumentFactory::instance();
$type = $factory->resolve_type( $order ); // 'invoice', 'receipt', etc.
$document = $factory->create( $order, $type ); // returns Invoice|Receipt|CreditNote|PackingSlip
$document->generate(); // returns PDF file path
\EasyPDFInvoices\Numbering\NumberGenerator#
Allocates sequential numbers atomically.
$gen = \EasyPDFInvoices\Numbering\NumberGenerator::instance();
$result = $gen->allocate_invoice_number( $order );
// $result = ['number' => 42, 'display' => 'INV-2026-000042']
$result = $gen->allocate_credit_note_number( $order );
The methods are idempotent — calling twice for the same order returns the previously assigned number.
\EasyPDFInvoices\PDF\Storage#
Manages PDF file storage with security checks.
$storage = \EasyPDFInvoices\PDF\Storage::instance();
$path = $storage->resolve_path( $order ); // returns realpath-validated path or null
$path = $storage->existing_path( $order ); // returns path if file exists, null otherwise
$path = $storage->save( $order, $document_type, $pdf_data ); // saves new PDF, returns path
resolve_path() validates the path is inside the upload directory via realpath(). Path traversal attempts return null.
\EasyPDFInvoices\Settings\SettingsRepository#
Typed settings access.
$repo = \EasyPDFInvoices\Settings\SettingsRepository::instance();
$prefix = $repo->get( 'numbering.invoice_prefix', 'INV-' );
$is_on = $repo->is_enabled( 'enable_pdf_generation' );
$repo->update( 'branding.accent_color', '#0073aa' );
WP-CLI commands#
# Plugin and counter health report
wp epdi diagnose
# Generate a test PDF
wp epdi test-pdf <order_id> [--type=invoice|receipt|credit-note|packing-slip] [--force]
# List generated documents (paginated)
wp epdi list-documents [--limit=N]
# Print the secure download URL for an order
wp epdi download-url <order_id>
All commands respect plugin settings and run through the same generation pipeline as the web UI.
Template overrides#
See Templates & Branding for the full theme override hierarchy.
Quick summary:
your-theme/easy-pdf-invoices/{variant}/{template}.php # variant-specific
your-theme/easy-pdf-invoices/{template}.php # generic
plugin/templates/{variant}/{template}.php # plugin variant
plugin/templates/{template}.php # plugin default
Testing your extensions#
The plugin uses standard WordPress integration test patterns. We recommend:
- WP-CLI for end-to-end testing:
wp epdi test-pdf <order_id>produces a real PDF you can inspect. - PHPUnit with WC_Helper_Order for unit tests of your hooks and filters.
- wp-env / Local / DDEV for local development.
The plugin has its own tests under .dev/tests/ (excluded from the release zip via .distignore).
Compatibility checks#
If you’re building an add-on plugin, declare the dependency on Emboss - PDF Invoices and Packing Slips for WooCommerce:
add_action( 'plugins_loaded', function() {
if ( ! defined( 'EPDI_VERSION' ) ) {
add_action( 'admin_notices', function() {
echo '<div class="error"><p>My Add-on requires Emboss - PDF Invoices and Packing Slips for WooCommerce.</p></div>';
} );
return;
}
// Initialise your add-on
} );
EPDI_VERSION is defined in the bootstrap and is the canonical “is the plugin active” check.
Release packaging#
The release zip is built via composer install --no-dev and excludes anything matching .distignore:
.dev/
.distignore
.git*
.phpcs.xml*
.phpstan.neon*
TASKS.md
phpcs.xml.dist
phpstan.neon.dist
composer.json
composer.lock
.wordpress-org/
The result is < 5MB on disk.
Contributing#
The plugin is open source under GPL v2+. For bug reports and feature requests, open a support ticket.
For substantial code contributions, contact us first to discuss scope and architecture fit. We keep the plugin lightweight on purpose — features that don’t fit the design principles (zero-config, single PDF library, HPOS-native, no telemetry) won’t be merged.
Related#
- Settings Reference — every setting documented.
- REST API — programmatic access.
- Compatibility — third-party plugin integrations.