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.