Siren

Extension Architecture Overview

How Siren extensions work — registration, lifecycle, event-driven architecture, and the PHPNomad interfaces that drive it all.

Last updated: April 8, 2026

Extension Architecture Overview

Siren’s extension system is built on PHPNomad, a platform-agnostic PHP framework. If you’re familiar with PHPNomad’s initializer system, you already understand how extensions work — Siren extensions are PHPNomad initializers with an additional Extension interface on top. If you’re not familiar with PHPNomad, the links throughout this guide point to the relevant framework documentation.

This article covers the parts that are specific to Siren: how extensions register, what the Extension interface adds, how Siren’s domain events flow, and what types of extensions you can build.

How do extensions register with Siren?

A Siren extension is a standard WordPress plugin that registers itself through the siren_ready action. Each extension has its own plugin.php, its own composer.json, and its own namespace. Siren does not scan directories or rely on naming conventions — your extension tells Siren it exists by calling Extensions::add().

// In your extension's plugin.php
add_action('siren_ready', function () {
    Extensions::add(Integration::getId(), fn() => new Integration());
}, 0);

Registration happens at priority 0 and stores a lazy factory — your Integration class is not instantiated until Siren is ready to process it. On plugins_loaded, Siren retrieves all registered extensions and runs each one through PHPNomad’s initializer pipeline. That pipeline processes interfaces like event bindings, listeners, class definitions, and load conditions in a specific order — all documented on PHPNomad’s site.

Because extensions are independent plugins, they install and activate through the normal WordPress plugin admin, maintain their own version lifecycle, and only load when their dependencies are satisfied.

What does the Extension interface add?

Every Siren extension implements Siren\Extensions\Core\Interfaces\Extension, which builds on PHPNomad’s initializer interfaces with Siren-specific metadata:

interface Extension extends Module
{
    public function getName(): string;
    public function getDescription(): string;
    public function canActivate(): bool;
    public function getIsActive(): bool;
    public function getSupports(): array; // Features::* constants
}

getName() and getDescription() provide human-readable metadata for Siren’s admin UI. canActivate() reports whether the extension’s dependencies exist — a WooCommerce extension returns class_exists('WooCommerce'), and Siren’s admin uses this to show available vs. unavailable extensions. getIsActive() reports whether the extension has successfully loaded. getSupports() declares what capabilities the extension provides (coupons, renewals, forms, etc.) using Features::* constants, which Siren uses to conditionally enable UI features and engagement triggers.

The Module parent interface adds getId() (a short unique string like 'wc' or 'edd' used for module registration and template paths) and getRootPath() (the filesystem path to the extension root).

What types of extensions can I build?

Third-party integrations

These bridge an external system (WooCommerce, EDD, Gravity Forms, etc.) into Siren’s domain model. Their primary job is translation: converting platform-specific hooks and data structures into Siren’s domain events. See the integration feature matrix for a comparison of what each built-in integration supports.

A WooCommerce integration, for example, listens to woocommerce_new_order and transforms the WooCommerce order data into a SaleTriggered domain event that Siren’s core can process without knowing anything about WooCommerce.

Integrations typically implement event bindings to map platform hooks to domain events, use transformers to convert hook arguments into event objects, use adapters to reshape platform data structures into Siren’s formats, and gate loading behind a canActivate() check for the third-party plugin.

Feature extensions

These add new capabilities to Siren without necessarily integrating a third party. They typically implement listeners to react to domain events and may register admin pages or other services. They usually don’t need transformers or event bindings at all.

The line between the two types is not rigid. Some extensions do both — integrating a third party while also adding Siren-specific features.

How does Siren’s event-driven architecture work?

Siren’s core processes domain events, not platform-specific data. Your extension’s job is to translate between the two.

WordPress HookDomain EventSiren Core Handler
woocommerce_new_orderSaleTriggeredProcessSale
gform_after_submissionLeadTriggeredProcessLead
edd_complete_purchaseTransactionCompletedFinalizeTransaction

At the platform level, WordPress fires an action. Your extension’s transformer converts the hook’s arguments into a platform-agnostic domain event. Then handler classes in Siren’s core react to those domain events, working identically regardless of which extension triggered the event.

This is why the distinction between event bindings and listeners matters. Event bindings connect platform hooks to domain events — they define what triggers an event. Listeners connect domain events to handler classes — they define what reacts to an event. Most third-party integrations only need event bindings because Siren’s core already has listeners for the standard domain events. You add listeners when your extension needs to react to events that core doesn’t handle by default.

How do extensions access services?

Extensions do not instantiate their own dependencies. PHPNomad’s DI container resolves everything:

// In a transformer callback inside getEventBindings()
$callback = fn($orderId) => $this->container->get(
    TransactionTransformerService::class
)->getSaleTriggeredEvent($orderId);

The container auto-wires constructor dependencies. If your transformer’s constructor asks for a MappingDatastore and a LoggerStrategy, the container provides them automatically. You only need to register class definitions when you are providing a new concrete implementation for an interface that other code resolves through the container.

Getting started

The fastest way to build an extension is to clone the extension template:

https://github.com/Novatorius/siren-extension-template

The template provides a working skeleton with all the interfaces wired up, a sample transformer, adapter, and handler. See the Quickstart guide and Template Reference for detailed walkthroughs.