PHP Binding
EventFlow integrates with PHP through attribute-based bindings. Natural language patterns in .flow files map to PHP classes.
Overview
flow
// order.flow
? cart is not empty
send confirmation email
order moves to #paid
expect:
= $total equals 1200php
// PHP bindings
#[Guard('cart is not empty')]
class CartNotEmpty { ... }
#[Action('send confirmation email')]
class SendConfirmationEmail { ... }
#[Assert('$total equals {number}')]
class TotalEquals { ... }Attribute Reference
#[Given(...)] - Test Setup
Maps given: block statements to PHP:
php
#[Given('@customer is logged in')]
public function customerLoggedIn(Context $ctx): void
{
$customer = Customer::factory()->create();
$ctx->set('customer', $customer);
Auth::login($customer);
}
#[Given('@customer is logged in as {string}')]
public function customerLoggedInAs(Context $ctx, string $email): void
{
$customer = Customer::factory()->create(['email' => $email]);
$ctx->set('customer', $customer);
Auth::login($customer);
}
#[Given('cart contains')]
public function cartContains(Context $ctx, array $table): void
{
$cart = Cart::factory()->create();
foreach ($table as $row) {
$product = Product::findByName($row['product']);
$cart->add($product, $row['quantity'] ?? 1, $row['price']);
}
$ctx->set('cart', $cart);
}
#[Given('order is in {state}')]
public function orderInState(Context $ctx, string $state): void
{
$order = Order::factory()->create([
'customer_id' => $ctx->get('customer')->id,
'state' => $state,
]);
$ctx->set('order', $order);
}#[Guard(...)] - Conditions
Maps ? prefixed lines to PHP:
php
#[Guard('cart is not empty')]
class CartIsNotEmpty implements Guard
{
public function check(Context $ctx): bool
{
return $ctx->get('cart')->isNotEmpty();
}
}
#[Guard('payment is authorized')]
class PaymentIsAuthorized implements Guard
{
public function check(Context $ctx): bool
{
return $ctx->get('payment.authorized', false);
}
}
#[Guard('$total is greater than {number}')]
class TotalGreaterThan implements Guard
{
public function check(Context $ctx, float $amount): bool
{
return $ctx->get('total') > $amount;
}
}#[Action(...)] - Side Effects
Maps action lines (no prefix) to PHP:
php
#[Action('send confirmation email')]
class SendConfirmationEmail implements Action
{
public function __construct(
private MailService $mail,
private TemplateEngine $templates
) {}
public function execute(Context $ctx): void
{
$this->mail->send(
to: $ctx->get('customer.email'),
body: $this->templates->render('confirmation', [
'order' => $ctx->get('order')
])
);
}
}
#[Action('process the payment')]
class ProcessPayment implements Action
{
public function __construct(private PaymentGateway $gateway) {}
public function execute(Context $ctx): void
{
$result = $this->gateway->charge(
amount: $ctx->get('total'),
card: $ctx->get('payment_method')
);
$ctx->set('payment_result', $result);
}
}#[Assert(...)] - Verifications
Maps = prefixed lines to PHP:
php
#[Assert('$total equals {number}')]
public function assertTotal(Context $ctx, float $expected): void
{
expect($ctx->get('total'))->toBe($expected);
}
#[Assert('order is in {state}')]
public function assertOrderState(Context $ctx, string $state): void
{
expect($ctx->get('order')->state)->toBe($state);
}
#[Assert('confirmation was sent')]
public function assertConfirmationSent(): void
{
SendConfirmationEmail::assertInvoked();
}
#[Assert('@customer received {event}')]
public function assertCustomerReceived(Context $ctx, string $event): void
{
$customer = $ctx->get('customer');
expect($customer->receivedEvents)->toContain($event);
}#[Step(...)] - General Patterns
For patterns that don't fit other categories:
php
#[Step('@customer adds {string} to cart')]
public function addToCart(Context $ctx, string $product): void
{
$item = Product::findByName($product);
$ctx->get('cart')->add($item);
}
#[Step('@customer clicks {string}')]
public function customerClicks(Context $ctx, string $button): void
{
// Handle UI action
}#[On(...)] - Event Handlers
For explicit event handling:
php
#[On(':checkout from @customer')]
public function handleCheckout(Machine $machine, Event $event): void
{
$machine->emit(':payment_request', [
'order_id' => $event->data['order_id'],
'amount' => $event->data['total'],
])->to('@payment');
}
#[On(':api_checkout> from @customer')] // API event
public function handleApiCheckout(Machine $machine, Event $event): void
{
// Handle API event
}Configuration
php
// config/eventflow.php
return [
'bindings' => [
'App\\EventFlow\\Given',
'App\\EventFlow\\Steps',
'App\\EventFlow\\Actions',
'App\\EventFlow\\Guards',
'App\\EventFlow\\Assertions',
],
'machines_path' => resource_path('flows'),
'cache' => [
'enabled' => env('EVENTFLOW_CACHE', true),
'path' => storage_path('framework/eventflow'),
],
];Type Handling
EventFlow types map to PHP types:
| EventFlow | PHP |
|---|---|
string | string |
number | int|float |
boolean | bool |
array | array |
object | object|array |
php
#[Step('$total: number becomes {number}')]
public function setTotal(Context $ctx, float $value): void
{
$ctx->set('total', $value);
}
#[Step('$items: array adds {string}')]
public function addItem(Context $ctx, string $item): void
{
$items = $ctx->get('items', []);
$items[] = $item;
$ctx->set('items', $items);
}Context Access
The Context object provides access to machine state:
php
public function example(Context $ctx): void
{
// Read
$cart = $ctx->get('cart');
// Read with default
$qty = $ctx->get('quantity', 1);
// Read with type
$total = $ctx->get('total', 0, 'number');
// Write
$ctx->set('lastAdded', $product);
// Check existence
if ($ctx->has('discount')) {
// apply
}
// Remove
$ctx->forget('temporary');
// Get all
$all = $ctx->all();
}Dependency Injection
All binding classes support constructor injection via Laravel's container:
php
#[Action('send confirmation email')]
class SendConfirmationEmail implements Action
{
public function __construct(
private MailService $mail,
private TemplateEngine $templates,
private LoggerInterface $logger
) {}
public function execute(Context $ctx): void
{
$this->logger->info('Sending confirmation email');
$this->mail->send(
to: $ctx->get('customer.email'),
body: $this->templates->render('confirmation', [
'order' => $ctx->get('order')
])
);
}
}Pattern Placeholders
Use placeholders to capture values:
| Placeholder | Matches | PHP Type |
|---|---|---|
{string} | Quoted string | string |
{number} | Number | int|float |
{word} | Single word | string |
{state} | State reference | string |
{event} | Event reference | string |
{actor} | Actor reference | string |
php
#[Guard('$total is greater than {number}')]
public function totalGreaterThan(Context $ctx, float $amount): bool
{
return $ctx->get('total') > $amount;
}
#[Action('notify {actor} about {event}')]
public function notifyActor(Context $ctx, string $actor, string $event): void
{
// $actor = "@customer", $event = ":order_confirmed"
}Table Data
Handle table data from given: or with: blocks:
php
#[Given('cart contains')]
public function cartContains(Context $ctx, array $table): void
{
// $table = [
// ['product' => 'Laptop', 'price' => '1200', 'quantity' => '1'],
// ['product' => 'Mouse', 'price' => '25', 'quantity' => '2'],
// ]
foreach ($table as $row) {
$product = Product::findByName($row['product']);
$ctx->get('cart')->add(
$product,
(int) ($row['quantity'] ?? 1),
(float) $row['price']
);
}
}Testing Bindings
Test your bindings in isolation:
php
class SendConfirmationEmailTest extends TestCase
{
public function test_sends_email_to_customer(): void
{
$mail = Mockery::mock(MailService::class);
$mail->shouldReceive('send')->once();
$action = new SendConfirmationEmail($mail, new TemplateEngine());
$ctx = new Context([
'customer' => ['email' => '[email protected]'],
'order' => ['id' => 'ORD-123'],
]);
$action->execute($ctx);
}
}Auto-Discovery
Bindings are auto-discovered from configured namespaces:
app/
EventFlow/
Given/
CustomerSetup.php
CartSetup.php
Guards/
CartGuards.php
PaymentGuards.php
Actions/
EmailActions.php
PaymentActions.php
Assertions/
OrderAssertions.phpphp
// app/EventFlow/Guards/CartGuards.php
namespace App\EventFlow\Guards;
use EventFlow\Attributes\Guard;
use EventFlow\Context;
class CartGuards
{
#[Guard('cart is not empty')]
public function cartNotEmpty(Context $ctx): bool
{
return $ctx->get('cart')->isNotEmpty();
}
#[Guard('cart is valid')]
public function cartValid(Context $ctx): bool
{
return $ctx->get('cart')->validate();
}
}Running Tests
bash
# Run EventFlow scenarios
php artisan eventflow:test order.flow
# Run specific scenario
php artisan eventflow:test order.flow --scenario="complete purchase"
# Run all flows
php artisan eventflow:test
# Watch mode
php artisan eventflow:test --watchGenerating Diagrams
bash
# Generate diagram
php artisan eventflow:diagram order.flow
# Specify type
php artisan eventflow:diagram order.flow --type=lane
php artisan eventflow:diagram order.flow --type=state
php artisan eventflow:diagram order.flow --type=combined
# Output format
php artisan eventflow:diagram order.flow --format=svg
php artisan eventflow:diagram order.flow --format=png
php artisan eventflow:diagram order.flow --format=html