Skip to content

Dependency Injection

EventMachine uses Laravel's service container for dependency injection in behaviors. This allows you to inject services, repositories, and other dependencies into your actions, guards, calculators, and other behaviors.

Constructor Injection

Class-based behaviors support constructor injection:

php
use Tarfinlabs\EventMachine\Behavior\ActionBehavior;

class ProcessOrderAction extends ActionBehavior
{
    public function __construct(
        private readonly OrderService $orderService,
        private readonly PaymentGateway $paymentGateway,
        private readonly NotificationService $notifications,
    ) {}

    public function __invoke(ContextManager $context): void
    {
        $order = $this->orderService->create($context->items);
        $this->paymentGateway->charge($order->total);
        $this->notifications->orderConfirmed($order);

        $context->orderId = $order->id;
    }
}

Parameter Injection

The __invoke() method receives injected parameters:

php
public function __invoke(
    ContextManager $context,      // Current context
    EventBehavior $event,         // Triggering event
    State $state,                 // Current state
    EventCollection $history,     // Event history
): void {
    // Use injected parameters
}

Available Parameters

TypeDescription
ContextManagerMachine context (or your custom context class)
EventBehaviorThe event that triggered the transition
StateCurrent machine state
EventCollectionHistory of all events
Named paramsConfig-defined parameters matched by name

Framework Types + Named Params

Framework types and config-defined named params can coexist in the same __invoke signature:

php
// Config
'guards' => [[IsAmountInRangeGuard::class, 'min' => 100, 'max' => 10000]],

// Guard
public function __invoke(ContextManager $context, int $min, int $max): bool
{
    return $context->amount >= $min && $context->amount <= $max;
}

Resolution order: framework types (matched by type-hint) → named params (matched by name from config) → PHP defaults.

Partial Injection

Only declare the parameters you need:

php
// Only context needed
public function __invoke(ContextManager $context): void
{
    $context->count++;
}

// Context and event needed
public function __invoke(
    ContextManager $context,
    EventBehavior $event,
): void {
    $context->value = $event->payload['value'];
}

// All parameters
public function __invoke(
    ContextManager $context,
    EventBehavior $event,
    State $state,
    EventCollection $history,
): void {
    // Full access
}

Typed Context Injection

Custom context classes are automatically injected:

php

use Tarfinlabs\EventMachine\ContextManager; class OrderContext extends ContextManager
{
    public string $orderId = '';
    public array $items = [];
}

class ProcessOrderAction extends ActionBehavior
{
    public function __invoke(OrderContext $context): void
    {
        // $context is typed as OrderContext
        // Full IDE autocompletion available
        $orderId = $context->orderId;
    }
}

Union Types for Event Parameters

When a behavior handles multiple event types (e.g., the same action wired to different transitions), use PHP 8 union types in the __invoke signature:

php
class StoreVehicleInfoAction extends ActionBehavior
{
    public function __invoke(
        CarSalesContext $context,
        VehicleAndPricingSubmittedEvent|VehicleEditRequestedEvent $event,
    ): void {
        // Both events share vehicleId in their payload
        $vehicleId = $event->data('vehicleId');

        // Discriminate when needed
        if ($event instanceof VehicleEditRequestedEvent) {
            $context->set('is_edit', true);
        }
    }
}

The injection system resolves the first matching type in the union — if the actual event is a subclass of any union member, it matches.

When to use union types vs base EventBehavior

Union types — when the behavior handles 2-3 specific event types and needs their typed payloads.

Base EventBehavior — when the behavior handles many event types (5+) or doesn't need the event at all. Entry actions on states reachable from many transitions often fall into this category.

Optional parameter (?EventBehavior $event = null) — for actions on @always transitions where the initial state has no triggering event.

Guard with Dependencies

php

use Tarfinlabs\EventMachine\Behavior\GuardBehavior; use Tarfinlabs\EventMachine\ContextManager; class HasPermissionGuard extends GuardBehavior
{
    public function __construct(
        private readonly AuthorizationService $auth,
    ) {}

    public function __invoke(ContextManager $context): bool
    {
        return $this->auth->can($context->userId, 'submit_order');
    }
}

Calculator with Dependencies

php

use Tarfinlabs\EventMachine\Behavior\CalculatorBehavior; use Tarfinlabs\EventMachine\ContextManager; class CalculateTaxCalculator extends CalculatorBehavior
{
    public function __construct(
        private readonly TaxService $taxService,
        private readonly GeoLocationService $geo,
    ) {}

    public function __invoke(ContextManager $context): void
    {
        $location = $this->geo->locate($context->shippingAddress);
        $taxRate = $this->taxService->getRateForLocation($location);

        $context->taxRate = $taxRate;
        $context->tax = $context->subtotal * $taxRate;
    }
}

Event Behavior with Dependencies

php

use Tarfinlabs\EventMachine\Behavior\EventBehavior; use Tarfinlabs\EventMachine\ContextManager; class SubmitOrderEvent extends EventBehavior
{
    public function __construct(
        private readonly RequestValidator $validator,
    ) {
        parent::__construct();
    }

    public static function getType(): string
    {
        return 'SUBMIT_ORDER';
    }

    public function actor(ContextManager $context): mixed
    {
        return auth()->user();
    }
}

Output with Dependencies

php

use Tarfinlabs\EventMachine\Behavior\OutputBehavior; use Tarfinlabs\EventMachine\ContextManager; class OrderOutputBehavior extends OutputBehavior
{
    public function __construct(
        private readonly ReceiptGenerator $receipts,
        private readonly OrderRepository $orders,
    ) {}

    public function __invoke(ContextManager $context): array
    {
        $order = $this->orders->find($context->orderId);
        $receipt = $this->receipts->generate($order);

        return [
            'order' => $order->toArray(),
            'receiptUrl' => $receipt->url,
        ];
    }
}

Practical Examples

Complete Action with Multiple Services

php

use Tarfinlabs\EventMachine\Behavior\ActionBehavior; use Tarfinlabs\EventMachine\Behavior\EventBehavior; class CompleteCheckoutAction extends ActionBehavior
{
    public function __construct(
        private readonly OrderRepository $orders,
        private readonly InventoryService $inventory,
        private readonly PaymentProcessor $payments,
        private readonly EmailService $email,
        private readonly AnalyticsService $analytics,
    ) {}

    public function __invoke(
        CheckoutContext $context,
        EventBehavior $event,
    ): void {
        // Create order
        $order = $this->orders->create([
            'customer_id' => $context->customerId,
            'items' => $context->items,
            'total' => $context->total,
        ]);

        // Reserve inventory
        foreach ($context->items as $item) {
            $this->inventory->reserve($item['id'], $item['quantity']);
        }

        // Process payment
        $payment = $this->payments->charge(
            $context->paymentMethod,
            $context->total,
        );

        // Send confirmation
        $this->email->sendOrderConfirmation($order);

        // Track analytics
        $this->analytics->track('order_completed', [
            'order_id' => $order->id,
            'total' => $order->total,
        ]);

        // Update context
        $context->orderId = $order->id;
        $context->paymentId = $payment->id;
    }
}

Guard with External Service

php

use Tarfinlabs\EventMachine\Behavior\GuardBehavior; use Tarfinlabs\EventMachine\ContextManager; class CheckInventoryGuard extends GuardBehavior
{
    public function __construct(
        private readonly InventoryService $inventory,
    ) {}

    public function __invoke(ContextManager $context): bool
    {
        foreach ($context->items as $item) {
            if (!$this->inventory->isAvailable($item['id'], $item['quantity'])) {
                return false;
            }
        }
        return true;
    }
}

Validation Guard with Repository

php

use Tarfinlabs\EventMachine\Behavior\ValidationGuardBehavior; use Tarfinlabs\EventMachine\ContextManager; class ValidateUserGuard extends ValidationGuardBehavior
{
    public ?string $errorMessage = null;

    public function __construct(
        private readonly UserRepository $users,
    ) {}

    public function __invoke(ContextManager $context): bool
    {
        $user = $this->users->find($context->userId);

        if (!$user) {
            $this->errorMessage = 'User not found';
            return false;
        }

        if ($user->isSuspended()) {
            $this->errorMessage = 'User account is suspended';
            return false;
        }

        if (!$user->hasVerifiedEmail()) {
            $this->errorMessage = 'Please verify your email first';
            return false;
        }

        return true;
    }
}

Binding Custom Implementations

In Service Provider

php
// app/Providers/AppServiceProvider.php
public function register(): void
{
    $this->app->bind(PaymentGateway::class, StripeGateway::class);

    $this->app->bind(NotificationService::class, function ($app) {
        return new SlackNotificationService(
            config('services.slack.webhook_url'),
        );
    });
}

Environment-Based Binding

php
public function register(): void
{
    if ($this->app->environment('testing')) {
        $this->app->bind(PaymentGateway::class, FakePaymentGateway::class);
        $this->app->bind(EmailService::class, FakeEmailService::class);
    } else {
        $this->app->bind(PaymentGateway::class, StripeGateway::class);
        $this->app->bind(EmailService::class, SendGridService::class);
    }
}

Testing with DI

Full Testing Guide

For complete constructor DI testing patterns, see Constructor DI Testing.

Quick Example

php
it('processes order with mocked services', function () {
    // Use $this->mock() — NOT app()->instance()
    $this->mock(OrderService::class)
        ->shouldReceive('create')->once()
        ->andReturn(new Order(['id' => 'order-123']));

    $state = State::forTesting(['items' => [['id' => 1]]]);
    ProcessOrderAction::runWithState($state);

    expect($state->context->get('orderId'))->toBe('order-123');
});

For isolated testing, faked behavior patterns, and decision guides, see the full Constructor DI testing guide.

Best Practices

1. Use Interface Bindings

php
// Define interface
interface PaymentGatewayInterface
{
    public function charge(float $amount): PaymentResult;
}

// Bind in service provider
$this->app->bind(PaymentGatewayInterface::class, StripeGateway::class);

// Use in behavior
public function __construct(
    private readonly PaymentGatewayInterface $payments,
) {}

2. Keep Dependencies Minimal

php
// Good - focused dependencies
public function __construct(
    private readonly OrderRepository $orders,
) {}

// Avoid - too many dependencies
public function __construct(
    private readonly OrderRepository $orders,
    private readonly UserRepository $users,
    private readonly ProductRepository $products,
    private readonly PaymentService $payments,
    private readonly ShippingService $shipping,
    private readonly TaxService $tax,
    private readonly NotificationService $notifications,
    private readonly AnalyticsService $analytics,
) {}
// Consider breaking into smaller actions

3. Use Readonly Properties

php
public function __construct(
    private readonly OrderService $orders,  // readonly prevents reassignment
) {}

4. Inject Interfaces, Not Implementations

php
// Good
public function __construct(
    private readonly LoggerInterface $logger,
) {}

// Avoid
public function __construct(
    private readonly MonologLogger $logger,
) {}

Detailed Guide

For comprehensive design guidelines with Do/Don't examples, see Action Design.

Released under the MIT License.