Siren

Dependency Injection

How Siren replaces WordPress globals and singletons with constructor injection and facades.

Last updated: April 9, 2026

Dependency Injection

If you have built WordPress plugins, you have used global functions and singletons to access services. global $wpdb, get_post(), WC()->cart. These are all forms of service location where you reach for a global to get what you need. Siren replaces this pattern with constructor injection: you declare what your class needs in its constructor, and the DI container provides it automatically.

// WordPress: reach for globals and singletons
function get_order_total($order_id) {
    global $wpdb;

    $result = $wpdb->get_var(
        $wpdb->prepare("SELECT total FROM {$wpdb->prefix}orders WHERE id = %d", $order_id)
    );

    // Or use a global function
    $post = get_post($order_id);
    $total = get_post_meta($order_id, '_order_total', true);

    return $total;
}
// Siren: declare dependencies in the constructor
class OrderTotalService
{
    protected TransactionDatastore $transactions;

    public function __construct(TransactionDatastore $transactions)
    {
        $this->transactions = $transactions;
    }

    public function getOrderTotal(int $transactionId): int
    {
        $transaction = $this->transactions->getById($transactionId);
        return $transaction->getTotal();
    }
}

How does constructor injection work?

When the DI container creates a class, it reads the constructor’s type hints and resolves each dependency. If OrderTotalService asks for a TransactionDatastore, the container finds the concrete class bound to that interface and instantiates it (resolving its own dependencies recursively). You never call new for services. The container handles the entire chain.

This works automatically for any class resolved through the container: listeners, transformers, admin services, and anything you fetch with $this->container->get(). You just declare what you need and it appears.

// The container resolves the full dependency chain
$service = $this->container->get(OrderTotalService::class);
// OrderTotalService gets TransactionDatastore injected
// TransactionDatastore gets its own dependencies injected
// ...all the way down

When should I use facades instead?

Facades are static wrappers that give you quick access to container-managed services without needing constructor injection. Siren provides facades for common operations:

use Siren\Programs\Core\Facades\Programs;
use Siren\Collaborators\Core\Facades\Collaborators;
use Siren\Configs\Core\Facades\Configs;
use Siren\Extensions\Core\Facades\Extensions;

// Quick lookups without constructor injection
$program = Programs::getById(42);
$collaborator = Collaborators::getCollaboratorFromUserId($userId);
$value = Configs::getConfigValue('program', '42', 'maxCommission', '0');

Use facades in theme files, one-off scripts, template files, and prototyping, anywhere you do not have access to the DI container. Use constructor injection in extension classes, listeners, transformers, and services, anywhere the container instantiates the class.

The rule of thumb: if your class is resolved through the container (listeners, handlers, services, transformers), use constructor injection. If you are writing code outside the container context (a WordPress hook callback in a theme, a quick CLI script), use facades.

What does a class with multiple injected services look like?

Listeners and services commonly depend on several services at once. The container resolves all of them:

use PHPNomad\Events\Interfaces\CanHandle;
use PHPNomad\Events\Interfaces\Event;
use PHPNomad\Logger\Interfaces\LoggerStrategy;
use Siren\Collaborators\Core\Datastores\Collaborator\Interfaces\CollaboratorDatastore;
use Siren\Configs\Core\Datastores\Config\Interfaces\ConfigDatastore;

class SyncCollaboratorSettings implements CanHandle
{
    protected LoggerStrategy $logger;
    protected CollaboratorDatastore $collaborators;
    protected ConfigDatastore $config;

    public function __construct(
        LoggerStrategy $logger,
        CollaboratorDatastore $collaborators,
        ConfigDatastore $config
    ) {
        $this->logger = $logger;
        $this->collaborators = $collaborators;
        $this->config = $config;
    }

    public function handle(Event $event): void
    {
        // All three services are available, fully wired by the container.
        $collaborator = $this->collaborators->getById($event->getCollaboratorId());
        $maxCommission = $this->config->getConfigValue(
            'program',
            (string) $event->getProgramId(),
            'maxCommission',
            '0'
        );
        $this->logger->info("Syncing settings for collaborator {$collaborator->getId()}");
    }
}

No globals, no singletons, no manual instantiation. Every dependency is explicit in the constructor, which makes the class easy to test and easy to understand at a glance.

Where can I learn more about the container?

The Integration Class guide covers container essentials for extension development, including getClassDefinitions() and common gotchas.

For the full PHPNomad framework documentation on dependency injection, see Dependency Injection.