Ecotone — The enterprise architecture layer for Laravel and Symfony
Ecotone extends your existing Laravel and Symfony application with the enterprise architecture layer
One Composer package adds CQRS, Event Sourcing, Workflows, and production resilience to your codebase. No framework change. No base classes. Just PHP attributes on your existing code.
composer require ecotone/laravel # or ecotone/symfony-bundle
See what it looks like
That's the entire setup. No bus configuration. No handler registration. No retry config. No serialization wiring. Ecotone reads your attributes and handles the rest:
Command and Query Bus — wired automatically from your #[CommandHandler] and #[QueryHandler] attributes
Event routing — NotificationService subscribes to OrderWasPlaced without any manual wiring
Test exactly the flow you care about
Extract a specific flow and test it in isolation — only the services you need:
Only OrderService is loaded. No notifications, no other handlers — just the flow you're verifying.
Now bring in the full async flow. Enable an in-memory channel and run it within the same test process:
->run('notifications') processes messages from the in-memory queue — right in the same process. The async handler executes deterministically, no timing issues, no polling, no external broker.
The key: swap the in-memory channel for , , or to test what runs in production — the test stays the same. Ecotone runs the consumer within the same process, so switching transports never changes how you test. The ease of in-memory testing no matter what backs your production system.
What changes in your daily work
Business logic is the only code you write
No command bus configuration. No handler registration. No message serialization setup. You write a PHP class with an attribute, and Ecotone wires the bus, the routing, the serialization, and the async transport. Your code stays focused on what your application actually does — your domain.
Going async never means rewriting handlers
Add #[Asynchronous('channel')] to any handler. The handler code stays identical. Switch from synchronous to to to by changing one line of configuration. Your business logic never knows the difference.
Failed messages don't disappear
Every failed message is captured in a . You see what failed, the full exception, and the original message. with one command. And can be combined with inbuilt Outbox pattern to ensure full consistency. No more silent failures. No more guessing what happened to that order at 3am.
Complex workflows live in one place
A multi-step business process — order placement, payment, shipping, notification — doesn't need to be scattered across event listeners, cron jobs, and database flags. Ecotone gives you for stateful workflows, for linear pipelines, and for declarative process control. The entire business flow is readable in one class.
Your codebase tells the story of your business
When a new developer opens your code, they see PlaceOrder, OrderWasPlaced, ShipOrder — not AbstractMessageBusHandlerFactory. Ecotone keeps your domain clean: no base classes to extend, no framework interfaces to implement, no infrastructure leaking into your business logic. Just with attributes that declare their intent.
AI-ready by design
Ecotone's declarative, attribute-based architecture is inherently friendly to AI code generators. When your AI assistant works with Ecotone code, two things happen:
Less context needed, less code generated. A command handler with #[CommandHandler] and #[Asynchronous('orders')] tells the full story in two attributes — no bus configuration files, no handler registration, no retry setup to feed into the AI's context window. The input is smaller because there's less infrastructure to read, and the output is smaller because there's less boilerplate to generate. That means lower token cost, faster iteration cycles, and more accurate results.
AI that knows Ecotone. Your AI assistant can work with Ecotone out of the box:
— Ready-to-use skills that teach any coding agent how to correctly write handlers, aggregates, sagas, projections, tests, and more. Install with one command and your AI generates idiomatic Ecotone code from the start.
— Direct access to Ecotone documentation for any AI assistant that supports Model Context Protocol — Claude Code, Cursor, Windsurf, GitHub Copilot, and others.
— AI-optimized documentation files that give any LLM instant context about Ecotone's API and patterns.
Testing that AI can actually run. Ecotone's runs async flows in the same process — even complex workflows with sagas and projections can be tested with ->sendCommand() and ->run(). Your coding agent writes and verifies tests without needing to set up external infrastructure or guess at test utilities.
Declarative configuration that any coding agent can follow and reproduce. Testing support that lets it verify even the most advanced flows. Less guessing, no hallucinating — just confident iteration.
The full capability set
Capability
What it gives you
Learn more
The enterprise gap in PHP, closed
Every mature ecosystem has an enterprise architecture layer on top of its web framework:
Ecosystem
Web Framework
Enterprise Architecture Layer
Ecotone is built on the same foundation — — that powers Spring Integration, NServiceBus, and Apache Camel. In active development since 2017 and used in production by teams running multi-tenant, event-sourced systems at scale, Ecotone brings the same patterns that run banking, logistics, and telecom systems in Java and .NET to PHP.
This isn't about PHP catching up. It's about your team using proven architecture patterns — with the development speed that PHP gives you — without giving up the ecosystem you already know.
Start with your framework
Laravel — Laravel's queue runs jobs, not business processes. Stop stitching Spatie + Laravel Workflow + Bus::chain + DIY outbox. Ecotone replaces the patchwork with one attribute-driven toolkit: aggregates with auto-published events, piped workflows, sagas, snapshots, transactional outbox — testable in-process, running on the queues you already have.
composer require ecotone/laravel
→ ·
Symfony — Symfony Messenger handles dispatch. For aggregates, sagas, or event sourcing the usual path is bolting on a separate event sourcing library, rolling your own outbox, and writing dedup middleware per handler. Ecotone replaces the patchwork with one attribute-driven toolkit: aggregates, sagas, event sourcing, piped workflows, transactional outbox, and per-handler failure isolation so one failing listener doesn't double-charge customers on retry. Pure POPOs, Bundle auto-config, your Messenger transports preserved.
composer require ecotone/symfony-bundle
→ ·
Any PHP framework — Ecotone Lite works with any PSR-11 compatible container.
composer require ecotone/lite-application
→
Try it in one handler. You don't need to migrate your application. Install Ecotone, add an attribute to one handler, and see what happens. If you like what you see, add more. If you don't — remove the package. Zero commitment.
— Setup guide for any framework
— Send your first command in 5 minutes
— Build a complete messaging flow step by step
The full CQRS, Event Sourcing, and Workflow feature set is under the Apache 2.0 License. are available for teams that need advanced scaling, distributed bus with service map, orchestrators, and production-grade Kafka integration.
Join — ask questions and share what you're building.
Async execution — #[Asynchronous('notifications')] routes to RabbitMQ, SQS, Kafka, or DBAL — your choice of transport
Failure isolation — each event handler gets its own copy of the message, so one handler's failure never blocks another
Retries and dead letter — failed messages retry automatically, permanently failed ones go to a dead letter queue you can inspect and replay
class OrderService
{
#[CommandHandler]
public function placeOrder(PlaceOrder $command, EventBus $eventBus): void
{
// your business logic
$eventBus->publish(new OrderWasPlaced($command->orderId));
}
#[QueryHandler('order.getStatus')]
public function getStatus(string $orderId): string
{
return $this->orders[$orderId]->status;
}
}
class NotificationService
{
#[Asynchronous('notifications')]
#[EventHandler]
public function whenOrderPlaced(OrderWasPlaced $event, NotificationSender $sender): void
{
$sender->sendOrderConfirmation($event->orderId);
}
}