Developer Guide
Developer Guide
Easy PDF Invoices 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 Easy PDF Invoices 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.
.dev/tests/ (excluded from the release zip via .distignore).
Compatibility checks
If you're building an add-on plugin, declare the dependency on Easy PDF Invoices:
add_action( 'plugins_loaded', function() {
if ( ! defined( 'EPDI_VERSION' ) ) {
add_action( 'admin_notices', function() {
echo '<div class="error"><p>My Add-on requires Easy PDF Invoices 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.