Get Plugin

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.
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 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.