The Integration Class
Anatomy of Integration.php — required interfaces, lifecycle methods, admin service patterns, and the processing order.
Last updated: April 9, 2026
The Integration Class
Every Siren extension is anchored by a single Integration class. This class tells Siren what your extension does, when it should activate, what domain events it bridges, and what platform features it supports. It is the single entry point the framework uses to discover and bootstrap your extension.
Required Interfaces
An integration class implements a specific set of interfaces. Here is the minimum set every commerce extension uses:
use PHPNomad\Di\Interfaces\CanSetContainer;
use PHPNomad\Di\Traits\HasSettableContainer;
use PHPNomad\Events\Interfaces\HasEventBindings;
use PHPNomad\Loader\Interfaces\HasLoadCondition;
use PHPNomad\Loader\Interfaces\Loadable;
use Siren\Extensions\Core\Interfaces\Extension;
class Integration implements Extension, HasEventBindings, HasLoadCondition, CanSetContainer, Loadable
{
use HasSettableContainer;
// ...
}
Each interface serves a distinct purpose. Extension is Siren’s own interface that declares this class as a Siren extension. It extends Module, requiring getId() and getRootPath(), and adds getName(), getDescription(), canActivate(), getIsActive(), and getSupports().
The remaining interfaces come from the PHPNomad framework. HasEventBindings declares that this class maps platform hooks to domain events via getEventBindings(). HasListeners is optional and declares that this class registers handlers for domain events via getListeners(). HasLoadCondition provides shouldLoad(). This is a gate that controls whether the extension bootstraps at all. CanSetContainer allows the framework to inject the DI container (use the HasSettableContainer trait to satisfy it). Loadable provides load(). This is the final setup method called after the framework decides to bootstrap the extension.
When should I add listeners?
Most commerce integrations (WooCommerce, EDD, NorthCommerce, LifterLMS) only implement HasEventBindings because they only need to bridge platform hooks into domain events. Add HasListeners when your extension also needs to react to domain events during bootstrap. The Gravity Forms integration is the canonical example:
// From GravityForms Integration — uses both HasEventBindings AND HasListeners
class Integration implements Extension, HasEventBindings, HasListeners, HasLoadCondition, CanSetContainer, Loadable
{
// ...
public function getListeners(): array
{
return [
Ready::class => [
InitializeGravityFormsAddon::class
]
];
}
}
This registers InitializeGravityFormsAddon to run when the framework fires Ready, allowing the GF add-on to be registered at exactly the right time.
Interface Processing Order
The framework processes these interfaces in a specific order, and the order matters:
The DI container is injected first via CanSetContainer — this must happen before anything else because event binding closures resolve services from the container. Next the framework checks shouldLoad() via HasLoadCondition, and if it returns false, processing stops entirely. Then getEventBindings() runs, binding platform hooks to domain event dispatchers. After that, getListeners() registers domain event handlers. Finally, load() runs for any imperative setup like admin UI initialization and platform-specific hooks.
This order guarantees that by the time getEventBindings() runs, $this->container is available. And by the time load() runs, all event bindings and listeners are already wired.
DI container essentials
The container auto-resolves constructor dependencies. If your class asks for a LoggerStrategy and a CollaboratorDatastore, the container provides them automatically. You only need to register class definitions when you are providing a new concrete implementation for an interface.
Common services available for injection:
| Interface | What it provides |
|---|---|
CollaboratorDatastore | Query and manage collaborators |
ConfigDatastore | Read and write configuration values |
LoggerStrategy | Log messages and exceptions |
EventStrategy | Attach/detach event listeners programmatically |
ExtensionRegistryService | Query active extensions and their features |
If your extension adds a concrete class that implements an interface other code resolves through the container, register it via getClassDefinitions() on any class implementing HasClassDefinitions:
public function getClassDefinitions(): array
{
return [
// Key = concrete class, Value = interface
MyConcreteService::class => MyServiceInterface::class,
];
}
Every entry must be a key-value pair. A bare value like SomeClass::class without a key causes the container to try instantiating a class called "0", which fails with a confusing error. If your class does not implement an interface that other code depends on, skip registration entirely. The container auto-resolves concrete classes.
For the full dependency injection documentation, see the PHPNomad framework docs.
Metadata Methods
Extension ID
A short, unique string identifier for this extension. Used as a module ID for template resolution and as a source identifier in domain events.
public static function getId(): string
{
return 'wc'; // WooCommerce
return 'edd'; // Easy Digital Downloads
return 'nc'; // NorthCommerce
return 'llms'; // LifterLMS
return 'gf'; // Gravity Forms
}
This ID appears throughout the system — in mapping external types (wc_order, edd_order), in event sources, and in template path prefixes (wc::module/template).
Name and description
Human-readable metadata displayed in the admin UI:
public function getName(): string
{
return 'WooCommerce';
}
public function getDescription(): string
{
return 'Makes it possible to award engagements for actions within WooCommerce';
}
Root path
The filesystem root of this extension module, used for locating templates and resources:
public function getRootPath(): string
{
return SIREN_WOOCOMMERCE_ROOT;
}
Each extension defines a root path constant (e.g., SIREN_WOOCOMMERCE_ROOT, SIREN_EDD_ROOT).
How does Siren detect whether the target plugin is installed?
canActivate() answers: “Is the target plugin installed and available?” This is typically a class_exists() check against the target plugin’s main class:
// WooCommerce
public function canActivate(): bool
{
return class_exists('WooCommerce');
}
// EDD
public function canActivate(): bool
{
return class_exists('Easy_Digital_Downloads');
}
// LifterLMS
public function canActivate(): bool
{
return class_exists('LifterLMS');
}
// Gravity Forms — checks multiple possible entry points
public function canActivate(): bool
{
return class_exists('GFForms') || class_exists('GFCommon');
}
canActivate() is part of the Extension interface and is used by the admin UI to show whether an extension could be activated. It does not control loading — that is shouldLoad()’s job.
How do I conditionally control whether my extension loads?
shouldLoad() controls whether the framework actually bootstraps this extension. In most cases it simply delegates to canActivate():
public function shouldLoad(): bool
{
return $this->canActivate();
}
But shouldLoad() can add extra conditions beyond plugin detection. NorthCommerce requires a minimum Siren version:
// NorthCommerce — requires Siren 1.3+
public function shouldLoad(): bool
{
return $this->canActivate() && version_compare(
$this->container->get(VersionProvider::class)->getVersion(),
'1.3',
'>='
);
}
The distinction matters: canActivate() tells the admin “this extension is available,” while shouldLoad() tells the framework “actually load it.” An extension might be activatable but not loadable (e.g., wrong Siren version, missing configuration, license check).
How do I declare which features my extension supports?
getSupports() returns an array of Features enum values declaring what platform capabilities this extension provides:
use Siren\Extensions\Core\Enums\Features;
// WooCommerce
public function getSupports(): array
{
$supports = [
Features::Coupons,
Features::ManualOrdering
];
if (class_exists('WC_Subscriptions')) {
$supports[] = Features::Renewals;
}
return $supports;
}
Available feature constants:
| Constant | Value | Meaning |
|---|---|---|
Features::Coupons | 'coupons' | Extension can track coupon usage |
Features::ManualOrdering | 'manual_ordering' | Extension supports manual order creation |
Features::Renewals | 'renewals' | Extension can track subscription renewals |
Features::Courses | 'courses' | Extension can track course completions |
Features::Lessons | 'lessons' | Extension can track lesson completions |
Features::Forms | 'forms' | Extension can track form submissions |
Features::Posts | 'posts' | Extension can track post interactions |
Note how WooCommerce conditionally adds Features::Renewals based on whether WooCommerce Subscriptions is installed. Feature support can be dynamic.
How do I map hooks to domain events?
This is the core of what an integration does. See Event Bindings and Transformers for full coverage.
How do I register domain event handlers?
When implemented, getListeners() returns a map of domain event classes to handler classes:
public function getListeners(): array
{
return [
Ready::class => [
InitializeGravityFormsAddon::class
]
];
}
The format is EventClass::class => [HandlerClass::class, ...]. Each handler must implement PHPNomad\Events\Interfaces\CanHandle. Handlers are resolved from the DI container, so their constructor dependencies are auto-wired.
What goes in the final setup method?
load() runs after all bindings and listeners are registered. It is where you set up WordPress-specific admin UI, filters, and other side effects:
public function load(): void
{
$this->isActive = true;
add_action('plugins_loaded', fn() => $this->container->get(CouponAdminService::class)->init());
add_action('plugins_loaded', fn() => $this->container->get(ProductAdminService::class)->init());
}
Always set $isActive = true first — getIsActive() reports whether the load method has run. Defer admin service initialization to plugins_loaded to ensure the target plugin’s APIs are available. And always use $this->container->get() to resolve services rather than instantiating directly.
WooCommerce’s load() shows a more complex example that also handles collaborator access to the WooCommerce admin:
public function load(): void
{
$this->isActive = true;
add_action('plugins_loaded', fn() => $this->container->get(CouponAdminService::class)->init());
add_action('plugins_loaded', fn() => $this->container->get(ProductAdminService::class)->init());
add_action('plugins_loaded', function () {
$user = wp_get_current_user();
try {
$collaborator = Collaborators::getCollaboratorFromUserId($user->ID);
} catch (RecordNotFoundException $e) {
return;
} catch (DatastoreErrorException $e) {
Logger::logException($e);
return;
}
if ($collaborator) {
add_filter('woocommerce_prevent_admin_access', '__return_false');
}
});
}
The admin service pattern
Admin services are plain PHP classes that add platform-specific UI panels for mapping Siren entities (collaborators, programs) to external entities (products, coupons). They follow a consistent structure: receive datastores via constructor injection, register WordPress hooks in an init() method, and handle save/render in protected methods.
class ProductAdminService
{
protected MappingDatastore $mappings;
protected CollaboratorDatastore $collaborators;
public function __construct(MappingDatastore $mappings, CollaboratorDatastore $collaborators)
{
$this->mappings = $mappings;
$this->collaborators = $collaborators;
}
public function init(): void
{
add_action('save_post_product', [$this, 'handleSave']);
// Register meta boxes, admin columns, etc.
}
}
The load() method resolves admin services from the container and wraps init() in a plugins_loaded callback to ensure the target plugin’s classes are available. This is the standard pattern — the examples above show it in action.
Admin services commonly use the mapping system to store relationships between external entities and Siren entities. See the mappings guide for the clear-then-recreate save pattern that admin services use internally.
Complete Skeleton
Here is a minimal integration class for a hypothetical “AcmeShop” plugin:
<?php
namespace Siren\WordPress\Extensions\AcmeShop;
use PHPNomad\Di\Interfaces\CanSetContainer;
use PHPNomad\Di\Traits\HasSettableContainer;
use PHPNomad\Events\Interfaces\HasEventBindings;
use PHPNomad\Loader\Interfaces\HasLoadCondition;
use PHPNomad\Loader\Interfaces\Loadable;
use Siren\Commerce\Events\SaleTriggered;
use Siren\Commerce\Events\TransactionCompleted;
use Siren\Extensions\Core\Enums\Features;
use Siren\Extensions\Core\Interfaces\Extension;
class Integration implements Extension, HasEventBindings, HasLoadCondition, CanSetContainer, Loadable
{
use HasSettableContainer;
protected bool $isActive = false;
public static function getId(): string
{
return 'acme';
}
public function getName(): string
{
return 'AcmeShop';
}
public function getDescription(): string
{
return 'Tracks sales and commissions for AcmeShop orders';
}
public function getRootPath(): string
{
return SIREN_ACMESHOP_ROOT;
}
public function canActivate(): bool
{
return class_exists('AcmeShop');
}
public function shouldLoad(): bool
{
return $this->canActivate();
}
public function getSupports(): array
{
return [Features::Coupons, Features::ManualOrdering];
}
public function getIsActive(): bool
{
return $this->isActive;
}
public function getEventBindings(): array
{
$saleCallback = fn($orderId) => $this->container->get(
SaleTransformer::class
)->getSaleTriggeredEvent($orderId);
return [
SaleTriggered::class => [
['action' => 'acme_order_created', 'transformer' => $saleCallback],
],
];
}
public function load(): void
{
$this->isActive = true;
add_action('plugins_loaded', fn() => $this->container->get(
ProductAdminService::class
)->init());
}
}