Skip to content

Session 4a: Direct Implementation

Alex implements all PHP bindings in a single focused session - guards, actions, and events written upfront.

Starting Point

After deriving states in Session 4, Alex has:

  • A complete flow file with guards, actions, and events
  • 6 failing test scenarios waiting for implementations
  • A clear list of 15 bindings needed
bash
$ eventflow test order.flow order.test.flow

@order / checkout

  happy path
 checkout awaiting_payment (12ms)
 payment_success confirmed (15ms)

  :checkout variations
 empty cart rejected (8ms)
 guest user redirected to login (7ms)

  :payment_success variations
 confirmation email is sent (12ms)

  :payment_failed variations
 payment failure notifies customer (6ms)

  :retry_checkout variations
 retry after payment failure (5ms)
 retry limit exceeded (4ms)
 admin can override retry limit (4ms)

3 passing, 6 failing

Identifying Missing Bindings

Let me check what bindings are missing. EventFlow can tell me exactly what needs to be implemented.
bash
$ eventflow bindings:missing order.flow

Missing bindings for @order:

Guards (5):
 '@customer is logged in'
 'cart is not empty'
 '$retry_count is less than 3'
 '@customer is admin'
 'order is in #payment_failed'

Actions (4):
 '$order_id becomes uuid()'
 '$created_at becomes now()'
 '$retry_count increases by 1'
 'send confirmation email'

Events (6):
 ':payment_request'
 ':order_confirmed'
 ':checkout_rejected'
 ':login_required'
 ':payment_failed_notification'
 ':max_retries_exceeded'

15 bindings missing

Implementing All Guards

I'll implement all 5 guards first. Each guard returns a boolean based on context.

CustomerLoggedInGuard

php
<?php

declare(strict_types=1);

namespace App\Order\Guards;

use EventFlow\Attributes\Guard;
use EventFlow\Behavior\GuardBehavior;

#[Guard('@customer is logged in')]
class CustomerLoggedInGuard extends GuardBehavior
{
    public function __invoke(array $context): bool
    {
        $customer = $context['customer'] ?? null;

        if ($customer === null) {
            return false;
        }

        return $customer->isAuthenticated();
    }
}

CartNotEmptyGuard

php
<?php

declare(strict_types=1);

namespace App\Order\Guards;

use EventFlow\Attributes\Guard;
use EventFlow\Behavior\GuardBehavior;

#[Guard('cart is not empty')]
class CartNotEmptyGuard extends GuardBehavior
{
    public function __invoke(array $context): bool
    {
        $cart = $context['cart'] ?? null;

        if ($cart === null) {
            return false;
        }

        return count($cart->items) > 0;
    }
}

RetryLimitGuard

php
<?php

declare(strict_types=1);

namespace App\Order\Guards;

use EventFlow\Attributes\Guard;
use EventFlow\Behavior\GuardBehavior;

#[Guard('$retry_count is less than 3')]
class RetryLimitGuard extends GuardBehavior
{
    private const MAX_RETRIES = 3;

    public function __invoke(array $context): bool
    {
        $retryCount = $context['retry_count'] ?? 0;

        return $retryCount < self::MAX_RETRIES;
    }
}

CustomerIsAdminGuard

php
<?php

declare(strict_types=1);

namespace App\Order\Guards;

use EventFlow\Attributes\Guard;
use EventFlow\Behavior\GuardBehavior;

#[Guard('@customer is admin')]
class CustomerIsAdminGuard extends GuardBehavior
{
    public function __invoke(array $context): bool
    {
        $customer = $context['customer'] ?? null;

        if ($customer === null) {
            return false;
        }

        return $customer->hasRole('admin');
    }
}

OrderInPaymentFailedGuard

php
<?php

declare(strict_types=1);

namespace App\Order\Guards;

use EventFlow\Attributes\Guard;
use EventFlow\Behavior\GuardBehavior;

#[Guard('order is in #payment_failed')]
class OrderInPaymentFailedGuard extends GuardBehavior
{
    public function __invoke(array $context): bool
    {
        $currentState = $context['_state'] ?? null;

        return $currentState === 'payment_failed';
    }
}

Implementing All Actions

Now the 4 actions. Actions modify context or trigger side effects.

GenerateOrderIdAction

php
<?php

declare(strict_types=1);

namespace App\Order\Actions;

use EventFlow\Attributes\Action;
use EventFlow\Behavior\ActionBehavior;
use Ramsey\Uuid\Uuid;

#[Action('$order_id becomes uuid()')]
class GenerateOrderIdAction extends ActionBehavior
{
    public function __invoke(array $context): array
    {
        return [
            'order_id' => Uuid::uuid4()->toString(),
        ];
    }
}

GenerateCreatedAtAction

php
<?php

declare(strict_types=1);

namespace App\Order\Actions;

use EventFlow\Attributes\Action;
use EventFlow\Behavior\ActionBehavior;
use Carbon\Carbon;

#[Action('$created_at becomes now()')]
class GenerateCreatedAtAction extends ActionBehavior
{
    public function __invoke(array $context): array
    {
        return [
            'created_at' => Carbon::now()->toIso8601String(),
        ];
    }
}

IncrementRetryCountAction

php
<?php

declare(strict_types=1);

namespace App\Order\Actions;

use EventFlow\Attributes\Action;
use EventFlow\Behavior\ActionBehavior;

#[Action('$retry_count increases by 1')]
class IncrementRetryCountAction extends ActionBehavior
{
    public function __invoke(array $context): array
    {
        $currentCount = $context['retry_count'] ?? 0;

        return [
            'retry_count' => $currentCount + 1,
        ];
    }
}

SendConfirmationEmailAction

php
<?php

declare(strict_types=1);

namespace App\Order\Actions;

use EventFlow\Attributes\Action;
use EventFlow\Behavior\ActionBehavior;
use App\Services\EmailService;

#[Action('send confirmation email')]
class SendConfirmationEmailAction extends ActionBehavior
{
    public function __construct(
        private EmailService $emailService
    ) {}

    public function __invoke(array $context): array
    {
        $this->emailService->sendOrderConfirmation(
            customerId: $context['customer']->id,
            orderId: $context['order_id'],
            total: $context['total']
        );

        return []; // No context changes
    }
}

Implementing All Events

Finally, the 6 event classes. Events are value objects that structure data between actors.

PaymentRequestEvent

php
<?php

declare(strict_types=1);

namespace App\Order\Events;

use EventFlow\Attributes\Event;
use EventFlow\Behavior\EventBehavior;

#[Event(':payment_request')]
class PaymentRequestEvent extends EventBehavior
{
    public function __construct(
        public readonly string $orderId,
        public readonly float $total,
    ) {}
}

OrderConfirmedEvent

php
<?php

declare(strict_types=1);

namespace App\Order\Events;

use EventFlow\Attributes\Event;
use EventFlow\Behavior\EventBehavior;

#[Event(':order_confirmed')]
class OrderConfirmedEvent extends EventBehavior
{
    public function __construct(
        public readonly string $orderId,
    ) {}
}

CheckoutRejectedEvent

php
<?php

declare(strict_types=1);

namespace App\Order\Events;

use EventFlow\Attributes\Event;
use EventFlow\Behavior\EventBehavior;

#[Event(':checkout_rejected')]
class CheckoutRejectedEvent extends EventBehavior
{
    public function __construct(
        public readonly string $reason,
    ) {}
}

LoginRequiredEvent

php
<?php

declare(strict_types=1);

namespace App\Order\Events;

use EventFlow\Attributes\Event;
use EventFlow\Behavior\EventBehavior;

#[Event(':login_required')]
class LoginRequiredEvent extends EventBehavior
{
    public function __construct(
        public readonly string $message,
    ) {}
}

PaymentFailedNotificationEvent

php
<?php

declare(strict_types=1);

namespace App\Order\Events;

use EventFlow\Attributes\Event;
use EventFlow\Behavior\EventBehavior;

#[Event(':payment_failed_notification')]
class PaymentFailedNotificationEvent extends EventBehavior
{
    public function __construct(
        public readonly string $orderId,
        public readonly string $reason,
    ) {}
}

MaxRetriesExceededEvent

php
<?php

declare(strict_types=1);

namespace App\Order\Events;

use EventFlow\Attributes\Event;
use EventFlow\Behavior\EventBehavior;

#[Event(':max_retries_exceeded')]
class MaxRetriesExceededEvent extends EventBehavior
{
    public function __construct(
        public readonly string $message,
    ) {}
}

Registering the Machine

All bindings are registered with the machine definition. EventFlow auto-discovers them via attributes.
php
<?php

declare(strict_types=1);

namespace App\Order;

use EventFlow\Definition\MachineDefinition;
use App\Order\Guards\CustomerLoggedInGuard;
use App\Order\Guards\CartNotEmptyGuard;
use App\Order\Guards\RetryLimitGuard;
use App\Order\Guards\CustomerIsAdminGuard;
use App\Order\Guards\OrderInPaymentFailedGuard;
use App\Order\Actions\GenerateOrderIdAction;
use App\Order\Actions\GenerateCreatedAtAction;
use App\Order\Actions\IncrementRetryCountAction;
use App\Order\Actions\SendConfirmationEmailAction;
use App\Order\Events\PaymentRequestEvent;
use App\Order\Events\OrderConfirmedEvent;
use App\Order\Events\CheckoutRejectedEvent;
use App\Order\Events\LoginRequiredEvent;
use App\Order\Events\PaymentFailedNotificationEvent;
use App\Order\Events\MaxRetriesExceededEvent;

class OrderMachine
{
    public static function definition(): MachineDefinition
    {
        return MachineDefinition::define(
            config: 'order.flow',
            behavior: [
                'guards' => [
                    CustomerLoggedInGuard::class,
                    CartNotEmptyGuard::class,
                    RetryLimitGuard::class,
                    CustomerIsAdminGuard::class,
                    OrderInPaymentFailedGuard::class,
                ],
                'actions' => [
                    GenerateOrderIdAction::class,
                    GenerateCreatedAtAction::class,
                    IncrementRetryCountAction::class,
                    SendConfirmationEmailAction::class,
                ],
                'events' => [
                    PaymentRequestEvent::class,
                    OrderConfirmedEvent::class,
                    CheckoutRejectedEvent::class,
                    LoginRequiredEvent::class,
                    PaymentFailedNotificationEvent::class,
                    MaxRetriesExceededEvent::class,
                ],
            ]
        );
    }
}

Validating All Bindings

bash
$ eventflow validate order.flow --bindings

Validating bindings for @order...

Guards:
 '@customer is logged in' CustomerLoggedInGuard
 'cart is not empty' CartNotEmptyGuard
 '$retry_count is less than 3' RetryLimitGuard
 '@customer is admin' CustomerIsAdminGuard
 'order is in #payment_failed' OrderInPaymentFailedGuard

Actions:
 '$order_id becomes uuid()' GenerateOrderIdAction
 '$created_at becomes now()' GenerateCreatedAtAction
 '$retry_count increases by 1' IncrementRetryCountAction
 'send confirmation email' SendConfirmationEmailAction

Events:
 ':payment_request' PaymentRequestEvent
 ':order_confirmed' OrderConfirmedEvent
 ':checkout_rejected' CheckoutRejectedEvent
 ':login_required' LoginRequiredEvent
 ':payment_failed_notification' PaymentFailedNotificationEvent
 ':max_retries_exceeded' MaxRetriesExceededEvent

All 15 bindings validated

Final Test Run

All bindings are in place. Let's run the full test suite.
bash
$ eventflow test order.flow order.test.flow --with-bindings

Loading bindings... 15 found

@order / checkout

  happy path
 checkout awaiting_payment (18ms)
 payment_success confirmed (21ms)

  :checkout variations
 empty cart rejected (12ms)
 guest user redirected to login (10ms)

  :payment_success variations
 confirmation email is sent (14ms)

  :payment_failed variations
 payment failure notifies customer (11ms)

  :retry_checkout variations
 retry after payment failure (16ms)
 retry limit exceeded (13ms)
 admin can override retry limit (12ms)

9 passing
All 9 tests pass. The implementation is complete and ready for review.

File Structure

After implementation, the project structure looks like:

app/
  Order/
    Guards/
      CustomerLoggedInGuard.php
      CartNotEmptyGuard.php
      RetryLimitGuard.php
      CustomerIsAdminGuard.php
      OrderInPaymentFailedGuard.php
    Actions/
      GenerateOrderIdAction.php
      GenerateCreatedAtAction.php
      IncrementRetryCountAction.php
      SendConfirmationEmailAction.php
    Events/
      PaymentRequestEvent.php
      OrderConfirmedEvent.php
      CheckoutRejectedEvent.php
      LoginRequiredEvent.php
      PaymentFailedNotificationEvent.php
      MaxRetriesExceededEvent.php
    OrderMachine.php
resources/
  flows/
    order.flow
    order.test.flow

Summary

TypeCountFiles Created
Guards5Guards/*.php
Actions4Actions/*.php
Events6Events/*.php
Machine1OrderMachine.php
Total16

The Direct Implementation approach is fast and efficient:

  • All code written in one session
  • Immediate feedback from validation
  • Best for experienced teams with clear requirements

For an alternative approach with unit tests for each binding, see Session 4b: TDD Implementation.


Next: Session 5: Review - The team reviews the complete implementation together.

Released under the MIT License.