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 failingIdentifying 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 missingImplementing 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.flowSummary
| Type | Count | Files Created |
|---|---|---|
| Guards | 5 | Guards/*.php |
| Actions | 4 | Actions/*.php |
| Events | 6 | Events/*.php |
| Machine | 1 | OrderMachine.php |
| Total | 16 |
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.