Installation
composer require ecotone/ecotone
Ensure ecotone/ecotone is listed in composer.json under require.
Bundle Configuration
Add to config/bundles.php:
return [
// ...
Ecotone\SymfonyBundle\EcotoneBundle::class => ['all' => true],
];
First Use Case: Command Handler Define a command and handler using attributes:
// src/Command/ProcessOrderCommand.php
#[Command]
class ProcessOrderCommand {
public function __construct(public string $orderId) {}
}
// src/CommandHandler/ProcessOrderCommandHandler.php
#[CommandHandler]
class ProcessOrderCommandHandler {
public function __construct(private OrderRepository $orderRepo) {}
#[Transactional]
public function __invoke(ProcessOrderCommand $command) {
$order = $this->orderRepo->find($command->orderId);
// Business logic here
}
}
Dispatching Commands Use Symfony’s Messenger component to dispatch:
$this->messageBus->dispatch(new ProcessOrderCommand('order-123'));
src/Ecotone/SymfonyBundle/Resources/config/services.yaml (Default bundle configuration).tests/ in the Ecotone-Dev repo for real-world examples.#[Command] and #[CommandHandler] for write operations.
#[Command]
class UpdateUserEmailCommand { ... }
#[CommandHandler]
class UpdateUserEmailHandler {
public function __invoke(UpdateUserEmailCommand $command) { ... }
}
#[Query] and #[QueryHandler] for read operations.
#[Query]
class GetUserByEmailQuery { ... }
#[QueryHandler]
class GetUserByEmailHandler {
public function __invoke(GetUserByEmailQuery $query) { ... }
}
$user = $this->queryBus->ask(new GetUserByEmailQuery('user@example.com'));
#[AggregateRoot] and #[EventSourced].
#[AggregateRoot]
#[EventSourced]
class Order {
public function apply(OrderCreatedEvent $event) { ... }
public function apply(OrderPaidEvent $event) { ... }
}
#[Event].
#[Event]
class OrderCreatedEvent { ... }
#[EventHandler] to react to events.
#[EventHandler]
class NotifyOrderCreatedHandler {
public function __invoke(OrderCreatedEvent $event) { ... }
}
#[Saga] and #[SagaStep].
#[Saga]
class OrderFulfillmentSaga {
#[SagaStep]
public function handleOrderCreated(OrderCreatedEvent $event) { ... }
#[SagaStep]
public function handlePaymentConfirmed(PaymentConfirmedEvent $event) { ... }
}
#[Workflow] and #[WorkflowStep] for complex state transitions.
#[Workflow]
class OrderWorkflow {
#[WorkflowStep]
public function validateOrder(Order $order) { ... }
#[WorkflowStep]
public function processPayment(Order $order) { ... }
}
config/packages/messenger.yaml:
framework:
messenger:
transports:
async: '%env(MESSENGER_TRANSPORT_DSN)%'
#[Transactional] for automatic Doctrine transactions:
#[CommandHandler]
class ProcessOrderHandler {
#[Transactional]
public function __invoke(ProcessOrderCommand $command) { ... }
}
ecotone:
outbox:
enabled: true
table_name: 'outbox_messages'
Attribute Conflicts
#[AsMessage]). Ecotone attributes are designed to be self-contained.#[Command], #[Event], etc.).Event Sourcing Snapshotting
#[Snapshot] to optimize:
#[AggregateRoot]
#[EventSourced]
#[Snapshot(every: 10)] // Take a snapshot every 10 events
class LargeAggregate { ... }
Saga Timeout Handling
config/packages/ecotone.yaml:
ecotone:
sagas:
timeout: 3600 # 1 hour in seconds
Query Bus vs. Command Bus
#[Query] as a command (e.g., using messageBus->dispatch() instead of queryBus->ask()).#[QueryHandler] with queryBus->ask() for read operations.Transactional Boundaries
#[Transactional] applies to the entire handler method. Avoid long-running transactions:
#[CommandHandler]
class LongRunningHandler {
#[Transactional] // Risk of timeout
public function __invoke(LongRunningCommand $command) {
// Offload heavy work to async tasks
}
}
Enable Debugging
Add to config/packages/ecotone.yaml:
ecotone:
debug: true
This logs dispatched messages, event sourcing snapshots, and saga steps.
Check Middleware
Ecotone uses middleware for features like retries, logging, and outbox. Inspect config/packages/ecotone.yaml for middleware configuration:
ecotone:
middleware:
- 'ecotone.middleware.retry'
- 'ecotone.middleware.outbox'
Event Sourcing Debugging
#[EventSourced] with debug: true to log applied events:
#[EventSourced(debug: true)]
class Order { ... }
Custom Middleware Create middleware to intercept messages:
use Ecotone\Middleware\Middleware;
class LoggingMiddleware implements Middleware {
public function handle(object $message, callable $next) {
// Log before
$result = $next($message);
// Log after
return $result;
}
}
Register in config/packages/ecotone.yaml:
ecotone:
middleware:
- 'App\Middleware\LoggingMiddleware'
Custom Event Stores
Extend EventStore interface to use a non-Doctrine database:
use Ecotone\EventSourcing\EventStore;
class CustomEventStore implements EventStore {
public function load(string $aggregateId): ?AggregateRoot { ... }
public function save(AggregateRoot $aggregate): void { ... }
}
Bind in Symfony’s services:
services:
Ecotone\EventSourcing\EventStoreInterface: '@App\CustomEventStore'
Dynamic Command/Query Resolution
Override CommandBus or QueryBus to add dynamic resolution logic:
use Ecotone\CommandHandling\CommandBus;
class CustomCommandBus implements CommandBus {
public function dispatch(object $command): void {
// Custom logic
}
}
Bind in config/services.yaml:
services:
Ecotone\CommandHandling\CommandBusInterface: '@App\CustomCommandBus'
php bin/console doctrine:migrations:diff
php bin/console doctrine:migrations:migrate
How can I help you explore Laravel packages today?