Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Laravel Package

ecotone/laravel

Ecotone Laravel brings message-driven architecture to Laravel: command, query and event buses, CQRS, event sourcing, sagas and the outbox pattern, wired into the container, queues and Eloquent. Build decoupled, resilient apps with attributes instead of boilerplate.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal path from zero to a working handler

composer require ecotone/laravel
php artisan vendor:publish --provider="Ecotone\Laravel\EcotoneProvider"

The service provider is auto-discovered — no config/app.php edit. Publishing yields config/ecotone.php.

Where to look first, in order:

  1. config/ecotone.php — this is the file that matters. It controls the namespace(s) scanned for handlers, the service name used to resolve the gateway, serializer settings, and channel/consumer definitions. Almost every "my handler isn't firing" problem traces back here.
  2. vendor/ecotone/laravel/src/EcotoneProvider.php — short, and it is the honest map of what Laravel services are bridged into Ecotone (container, DB connection, config, possibly queue). Read it before you assume a feature is wired.
  3. The Ecotone core docs (ecotoneframework.com), not the Laravel adapter docs. The adapter is thin; 90% of the concepts — attributes, gateways, channels, aggregates, sagas, outbox — are core Ecotone semantics. The Laravel repo mostly tells you where to put things.

First use case — a command bus with no service wiring

The whole point of the package is that you do not write bindings or a route table. Drop an attribute on a class inside a scanned namespace and it's registered:

// app/Order/PlaceOrder.php
final class PlaceOrder
{
    public function __construct(
        public readonly string $orderId,
        public readonly int $amount,
    ) {}
}
// app/Order/OrderService.php
namespace App\Order;

use Ecotone\Modelling\Attribute\CommandHandler;

final class OrderService
{
    #[CommandHandler]
    public function place(PlaceOrder $command): void
    {
        // Eloquent persist, dispatch events, whatever
    }
}

Dispatch from a controller, artisan command, or queued job:

use Ecotone\Laravel\Facades\Ecotone;

Ecotone::sendCommand(new PlaceOrder('ord-1', 2500));

Verify before you build anything else:

php artisan ecotone:list           # every discovered command/query/event endpoint
php artisan ecotone:run <channel>  # run an async consumer

If your handler appears in ecotone:list, you're wired. If it doesn't, fix the namespace scan in config/ecotone.php — do not go debugging the handler itself. ecotone:list is your fastest feedback loop and you should run it after every refactor that moves a class.

Implementation Patterns

One mental model: three buses, three shapes. Commands and queries are point-to-point (exactly one handler; queries return a value, commands typically void/ack). Events are pub/sub (zero or many handlers, no return value). Keep the call sites disciplined:

Ecotone::sendCommand(new PlaceOrder(...));   // write
$dto = Ecotone::sendQuery(new GetOrder($id)); // read
Ecotone::publishEvent(new OrderPlaced(...));  // fan-out side effects

The clean layering that keeps this maintainable in a Laravel app: controller validates HTTP input and only dispatches a command → command handler does the domain work → event handlers do side effects (mail, projection updates, third-party calls). This mirrors Laravel's FormRequest/Job/Listener split but with explicit bus semantics, and it's what makes handlers unit-testable without HTTP.

Constructor injection works because handlers resolve through the container. Ecotone instantiates handlers, but the Laravel adapter resolves them out of Laravel's container, so you can inject repositories, Http clients, LoggerInterface, config-backed services — no app() calls inside handlers:

final class OrderReadModel
{
    public function __construct(private OrderRepository $orders) {}

    #[QueryHandler('order.get')]
    public function get(GetOrder $query): OrderView
    {
        return $this->orders->project($query->id);
    }
}

Named routing keys for anything long-lived. Unnamed handlers (#[CommandHandler]) work fine but tie the routing key to the class name. Naming them (#[CommandHandler('order.place')]) gives you a stable contract and a place to route asynchronously. Prefer names when the endpoint will be triggered by more than one caller or will outlive the current class name.

Async via channels. Synchronous by default; put a #[Asynchronous('channelName')]-style attribute on the handler (or route at the gateway) to hand it to an Ecotone channel. Then run consumers with php artisan ecotone:run <channel> under a process supervisor. This is a separate operational process from php artisan queue:work — see gotchas.

Aggregates for DDD, not for CRUD. Use #[Aggregate] + #[CommandHandler] on the aggregate's factory/mutator methods when you have real invariants and state transitions that belong together. For a plain service call, plain #[CommandHandler] on a service class is simpler and better. Do not force aggregates onto straightforward flows.

Sagas / process managers for multi-step flows. This is genuine value with no first-class Laravel equivalent — order fulfilment, retry chains, onboarding sequences. Model the process as a saga with #[Saga]-style handlers driven by events, and keep the saga deciding, not doing — it emits commands rather than calling services directly.

Testing. Handlers are plain classes, so instantiate and call them directly for unit tests. For integration tests, resolve the gateway from the container (app(Ecotone::SERVICE_NAME) or the facade) and assert the handler ran, or assert on the resulting read model / outbox rows. Avoid asserting on internal Ecotone message objects.

Strangler-fig integration tip. Leave existing controllers and jobs alone. Route new writes for one bounded context through the command bus, keep Eloquent read models where they are, and only move the outbox/projection layer last. This keeps blast radius small if you decide to back out.

Gotchas and Tips

Silent handler non-registration is the failure mode you will hit. If the namespace in config/ecotone.php doesn't match a handler's PSR-4 path, or a file wasn't autoload-dumped, messages are dispatched into the void with no exception. Always run php artisan ecotone:list after moving classes, and consider a startup assertion (e.g., a health-check command that asserts a known routing key exists) so it can't pass silently in staging.

Two runtimes, two supervisors. ecotone:run is not queue:work. If your async handlers aren't processing, check that the Ecotone consumer process is actually running under supervisor separately from (or in addition to) your Horizon/queue workers. They do not share a process. Add both to your deploy manifest — this is the number-one "works locally, dead in staging" issue.

Run composer dump-autoload immediately after any namespace move. Attribute scanning is autoload-driven. ecotone:list will look empty until the autoloader sees the file, and the failure is silent.

Outbox / event-sourcing migrations. The adapter is thin; whether event store and outbox tables are auto-published as migrations varies by setup and version. If you're using the outbox or event sourcing, verify the schema exists in database/migrations before assuming the feature works. Prefer running migrations you can inspect rather than relying on auto-publish.

Transactions and the outbox. The critical correctness property is whether domain state and its event land in the same transaction. Confirm the adapter uses Laravel's DB connection and transaction manager — if it doesn't, you will get partial commits (state saved, event lost, or vice versa). Test this explicitly: force an exception after the write and check whether an orphaned outbox row or orphaned state remains.

Idempotency is your responsibility. Async delivery is at-least-once. Design every event handler to be re-runnable. The quickest way to prove this: run a consumer, kill it mid-handler, restart it, and check the side effects weren't duplicated.

Poison messages block consumers. There's no universal dead-letter safety net you can assume — build a handler-level try/catch that logs to a failure channel (or a failed_messages table/queue) so one bad message can't stall a channel indefinitely.

Serialization quirks. Commands/queries are serialized between processes on async paths. readonly promoted constructor props are fine; closures, resources, Eloquent models, and Collection objects are not. Pass primitives or small DTOs, load models inside the handler by ID.

Octane/Swoole caveats. Long-lived workers hold the container across requests. Verify that the Ecotone gateway and handler registry are safe under Octane; if you see stale handler state or container leaks, run synchronous commands outside Octane or open an issue. Treat Octane as unverified until tested.

Version-lock both the adapter and core. Pin ecotone/ecotone and ecotone/laravel explicitly, and treat the adapter's Composer constraints as the ceiling for your next Laravel upgrade. That ceiling is where upgrade lag bites in year two. Keep a fork branch warm if the adapter falls behind.

License and adoption are real gates, not paperwork. NOASSERTION is not a license grant; get legal sign-off before merging to production. With ~22 stars and zero dependents, you will be the first to encounter edge cases — budget a senior champion and treat failures as design inputs, not bugs to report upstream and forget.

Extend at the boundary, not inside. Wrap the Ecotone facade behind a small internal interface (CommandBus, EventBus) in your own domain layer. When (not if) you need to swap the adapter, pin to a fork, or revert a bounded context, you change one binding instead of hundreds of call sites.

Debugging checklist when a command "does nothing": 1) ecotone:list shows the handler? 2) If async, is the consumer running? 3) Is the command class serializable? 4) Did an exception get swallowed by a handler wrapper? Add structured logging inside the handler's first line — you'll know instantly whether the message reached the handler or died before it.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package