Siren

Events Introduction

How domain events drive Siren's pipeline. Covers listening to events, event flow, and when to use events vs direct data access.

Last updated: April 8, 2026

Events Introduction

Siren is an event-driven system. Almost everything that happens in the attribution pipeline is a reaction to a domain event. When a customer clicks an affiliate link, an event fires. When a sale completes, an event fires. When conversions are awarded, obligations are created, or payouts are generated, each step is triggered by the event that came before it. The core business logic lives in event listeners, not in procedural service calls.

This architecture is built on PHPNomad’s event system. Every event implements the Event interface, every listener implements CanHandle, and the framework’s initializer system wires them together through the DI container. Understanding events is essential for working with Siren at the code level, whether you are building an extension, debugging the pipeline, or writing custom integration logic.

How do I listen to events?

There are three ways to attach a listener to an event, ranging from the simplest to the most structured.

use PHPNomad\Core\Facades\Event;
use Siren\Commerce\Events\SaleTriggered;

// Closest to WordPress add_action — works anywhere, no setup required
Event::attach(SaleTriggered::class, function(SaleTriggered $event) {
    $opportunityId = $event->getOpportunityId();
    $details = $event->getTransactionDetails();
    // React to the sale
});

// You can also broadcast events from anywhere
Event::broadcast(new SaleTriggered($opportunityId, $details, $source, $dataType, $bindingId));
use PHPNomad\Events\Interfaces\CanHandle;
use PHPNomad\Events\Interfaces\Event;
use PHPNomad\Events\Interfaces\HasListeners;
use Siren\Commerce\Events\SaleTriggered;

// Step 1: Create a handler class that implements CanHandle
class OnSaleTriggered implements CanHandle
{
    protected ObligationDatastore $obligations;

    // Constructor dependencies are injected by the DI container
    public function __construct(ObligationDatastore $obligations)
    {
        $this->obligations = $obligations;
    }

    public function handle(Event $event): void
    {
        $opportunityId = $event->getOpportunityId();
        // Use injected services to react to the sale
    }
}

// Step 2: Register the handler in your extension's initializer
class MyInitializer implements HasListeners
{
    public function getListeners(): array
    {
        return [
            // Maps event class => handler class (or array of handler classes)
            SaleTriggered::class => OnSaleTriggered::class,
        ];
    }
}
use PHPNomad\Events\Interfaces\EventStrategy;
use Siren\Commerce\Events\SaleTriggered;

// Inject the event strategy into your service
class MyService
{
    protected EventStrategy $events;

    public function __construct(EventStrategy $events)
    {
        $this->events = $events;
    }

    public function setup(): void
    {
        $this->events->attach(SaleTriggered::class, function($event) {
            // React to the sale
        });
    }
}

The Event facade is the simplest option and works anywhere without setup, similar to WordPress’s add_action. Use it in theme files, standalone scripts, or quick integrations. It also provides Event::broadcast() for firing events and Event::detach() for removing listeners. An optional priority parameter controls execution order.

The HasListeners approach is preferred for extension code. Handler classes implement CanHandle and are resolved through the DI container at dispatch time, which means their constructors can inject any registered service. The initializer maps event classes to handler classes (or arrays of handler classes for multiple handlers on the same event). This keeps event subscriptions explicit and testable.

The EventStrategy approach is available when you need to attach listeners from within a DI-wired service class, but don’t want to declare them statically in an initializer.

See the Listeners & Event Handlers guide for the full details on writing handler classes. For a comparison of all approaches in the context of WordPress development, see the WordPress Developer Guide.

How does the event pipeline flow?

Events in Siren form a pipeline that tracks the full lifecycle of a referral, from the initial customer interaction through to the collaborator payout.

SaleTriggered Extension detects a purchase
EngagementsTriggered Attribution links sale to collaborator
ConversionsAwarded Credit assigned per program
ObligationIssued Business records what it owes
PayoutPaid Collaborator receives payment

The pipeline begins with commerce events. When an e-commerce platform detects a sale, refund, or lead form submission, the appropriate extension produces a commerce event like SaleTriggered or LeadTriggered. These are the entry points into Siren’s attribution system.

Commerce events trigger attribution events. When a sale is triggered, the system locates the opportunity that tracked the customer’s referral. It evaluates which collaborators had engagements with that opportunity, determines which programs apply, and fires events like EngagementCompleted and TransactionTriggered as it works through the attribution logic.

Attribution events trigger conversion events. The conversion system picks up where attribution leaves off, building conversion records for each program that should credit a collaborator. The ConversionsAwarded event signals that credit has been assigned.

Conversion events trigger payment events. When conversions are awarded, obligations are created to track what the business owes each collaborator. When conversions are approved (either automatically when payment confirms, or manually by an admin), obligations advance to pending status and become eligible for fulfillment and payout.

This flow is not a rigid sequence of hardcoded steps. Each stage is connected only by events, which means extensions and custom code can hook into any point in the pipeline without modifying the core logic.

When should I listen to events vs access data directly?

Listening to events is for reacting to state changes as they happen. If you need to do something when a sale occurs, when a conversion is awarded, or when an obligation status changes, attach a listener to the relevant event. Your listener runs at the moment the state changes, with full context about what happened.

Accessing datastores directly is for reading current state. If you need to display a list of active programs, look up a collaborator’s engagement history, or generate a report from existing records, query the datastore. The data is already there; you do not need an event to access it.

A common mistake is writing code that polls a datastore to detect changes, or that creates records manually instead of firing the appropriate event. If your logic starts with “check if something has changed since last time,” you probably want a listener. If your logic starts with “show me the current state,” you want a datastore query.

What is documented in the following pages?

The event reference is organized by pipeline stage. Each category page explains the flow and links to individual event pages with full details on payload, listeners, and code examples.

  • Commerce Events covers the events produced by e-commerce integrations: sales, refunds, coupon usage, renewals, and leads.
  • Attribution Events covers opportunity tracking, engagement creation, and the bridge from commerce activity into the conversion system.
  • Conversion Events covers the conversion lifecycle from initialization through approval, rejection, and renewal.
  • Payment Events covers transaction creation, obligation issuance, fulfillment processing, and payouts.
  • Distribution Events covers the scheduled reward system: heartbeat triggers, allocations, and metric tracking.
  • System Events covers collaborator registration, organization lifecycle, custom events, LMS integration, and recipe import.