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

Tactician Command Events Laravel Package

micro-module/tactician-command-events

View on GitHub
Deep Wiki
Context7

Getting Started

What this package is for

micro-module/tactician-command-events sits alongside league/tactician and gives you a way to surface events that happen as a side effect of handling a command — without coupling your command handlers to a global event dispatcher. The typical shape is:

  1. Your command handler mutates an aggregate and records domain events.
  2. Tactician's middleware pipeline runs.
  3. This package's middleware takes those recorded events and hands them to a PSR-14 (or Symfony EventDispatcherInterface) dispatcher once the command has succeeded.

Minimal setup

composer require micro-module/tactician-command-events

Then, before writing any application code, open the repository and read two things in this order:

  1. README.md — for the exact middleware class name and constructor signature (this is the piece most likely to differ from what you'd assume).
  2. src/ — specifically the middleware class and any EventRecorder/EventCollector interface it ships. Everything else in your integration depends on whether that interface is yours to implement or provided by the package.

Wire the middleware into the bus. The canonical Tactician shape:

use League\Tactician\CommandBus;
use League\Tactician\Handler\CommandHandlerMiddleware;
use MicroModule\TacticianCommandEvents\...; // verify namespace from src/

$bus = new CommandBus([
    $commandEventsMiddleware,          // ← this package
    $commandHandlerMiddleware,
]);

Your first use case

Pick a single command with one obvious side effect — e.g. RegisterUser producing a UserRegistered event that triggers a welcome email. Get that one path working end-to-end with a fake dispatcher that just records events into an array. Do not start by wiring the real mailer; you want to confirm ordering and error behaviour before you involve I/O.


Implementation Patterns

Pattern 1 — Handler returns events, middleware dispatches them

If the package's handler contract is "return an array of event objects," your handler stays pure and easy to unit test:

final class RegisterUserHandler
{
    public function __construct(private UserRepository $users) {}

    public function handle(RegisterUser $command): array
    {
        $user = User::register($command->email);
        $this->users->save($user);

        return $user->releaseEvents(); // package-defined contract — confirm
    }
}

The middleware then loops over the returned events and calls dispatch() on each. The advantage: no service locator, no static recorder, and a handler that is trivially testable without any dispatcher at all.

Pattern 2 — Recorded events on the command or on an injected collector

If instead the package exposes an event-recording service (something injected into handlers, or into aggregates), the middleware flushes the recorder after $next($command) returns successfully. This is the pattern used by most "domain events + command bus" integrations. The critical detail to verify in src/ is where the flush happens relative to the handler call: before, after, or inside a try/finally.

Pattern 3 — One middleware, many subscribers

Rather than branching on event type inside the middleware, register ordinary listeners with your PSR-14 dispatcher and keep the middleware dumb:

$dispatcher->listen(UserRegistered::class, SendWelcomeEmail::class);
$dispatcher->listen(UserRegistered::class, ProvisionTenant::class);

Keep the middleware as a pure transport concern. If you find yourself adding if ($event instanceof ...) to a listener, that's a listener-collection smell, not a middleware problem.

Wiring into a container

  • Symfony: register the middleware as a service, tag it, and inject event_dispatcher. Note that Symfony's dispatcher takes an event name string as the first argument, whereas PSR-14 takes the object. Confirm which interface this package typehints against — that single typehint determines how you wire it.
  • Laravel: Tactician is not native here; you'll usually bind CommandBus in a service provider and hand it Illuminate\Contracts\Events\Dispatcher wrapped in a small adapter implementing PSR-14 if needed.
  • Plain PHP / no framework: pass a League\Event\EventDispatcher or a hand-rolled PSR-14 implementation. This is the least surprising path and the easiest to debug.

Pattern 4 — Dispatch after the transaction commits

If commands are wrapped in a DB transaction (typically another Tactician middleware placed outside this one), make sure the event middleware sits outside the transaction middleware too, so events are only dispatched on commit. Ordering:

new CommandBus([
    $commandEventsMiddleware,     // outermost — fires only after commit
    $transactionMiddleware,
    $commandHandlerMiddleware,
]);

Pattern 5 — Testing

Use a collecting dispatcher and assert on the recorded events:

$dispatcher = new class implements EventDispatcherInterface {
    public array $events = [];
    public function dispatch(object $event): object { $this->events[] = $event; return $event; }
};

// handle the command, then:
self::assertInstanceOf(UserRegistered::class, $dispatcher->events[0]);

This gives you an assertion on behaviour (what the command announced) rather than on implementation (which repository method was called).


Gotchas and Tips

Verify before you trust. This package has 0 stars, 0 dependents, and its last release was May 2021. There is effectively no community usage to lean on. Read src/ end to end — it is almost certainly small enough to read in ten minutes — before you build on it. Treat the README as a hint, not as a contract.

PHP version compatibility is the first blocker. A 2021 release predates widespread PHP 8.2/8.3 adoption. Check composer.json's php constraint and run composer why-not php 8.3 (or just try installing). If it's pinned to ^7.4 || ^8.0, expect deprecation notices from implicit nullable parameters and possibly dynamic property creation. Forking and bumping the constraint is usually trivial; that is also the moment to decide whether you'd rather vendor the ~100 lines yourself.

Middleware order is the whole ballgame. Events dispatched before the transaction middleware commits will fire on a rolled-back change. Events dispatched inside a try/catch that swallows handler exceptions will fire on failure. The most common production bug with this class of package is "we sent the email but the transaction rolled back." Explicitly decide and test:

  • Does the middleware fire events when the handler throws? (Check for try/finally vs. plain sequential code.)
  • Does a listener throwing an exception roll back the command? If the event middleware sits inside the transaction boundary, yes — and that's usually what you want, but it means a flaky webhook can fail a valid command.

Listener exceptions and partial dispatch. If you dispatch five events and the third listener throws, events 4–5 never fire. There is no built-in retry or outbox here. For anything that crosses a process boundary (email, HTTP webhook, queue publish), don't dispatch inline — write to an outbox table inside the transaction and let a separate worker publish. This package gives you the hook; it does not give you delivery guarantees.

Nested commands. If a listener or handler dispatches another command onto the same bus, and the middleware holds any per-request state (a single recorder, a "currently dispatching" flag), you can get event leakage between commands or infinite recursion. Check whether the middleware is stateless per-invocation. If it isn't, and you need nesting, instantiate a fresh recorder per top-level command.

No opinion on event shape. Packages in this space rarely ship a base event class. That's good — but it means there's no occurredAt, no eventId, no serialisation hook. If you'll ever replay or queue these events, define your own DomainEvent interface with an ID and timestamp now, and make every event implement it. Retrofitting IDs onto live events is miserable.

Debugging. Put a logging decorator around the dispatcher rather than inside listeners:

final class LoggingDispatcher implements EventDispatcherInterface
{
    public function dispatch(object $event): object
    {
        $this->logger->debug('event', ['class' => $event::class]);
        return $this->inner->dispatch($event);
    }
}

If events "aren't firing," the failure is almost always one of: (a) the middleware isn't in the bus array, (b) the handler's return value is being discarded because the handler was resolved through a different path, or (c) the bus was constructed in two places and you instrumented the wrong one.

Extension points. The clean upgrades, in order of preference: (1) decorate the dispatcher; (2) decorate the middleware with your own that calls $this->inner->execute($command, $next) and adds behaviour around it; (3) fork the package. Given the maintenance signal, (1) and (2) keep you upgrade-safe; (3) is realistic here but costs you upstream fixes you were unlikely to get anyway.

The honest recommendation. For a greenfield project, consider implementing the event-dispatch middleware inline — it's typically 30–60 lines, and you get exact control over transaction ordering and failure semantics, which is precisely where this package's opacity will hurt you. Adopt the package when you want a familiar name in the pipeline and are willing to read its source; avoid it when your events trigger anything you can't afford to send twice or send on a rolled-back transaction.

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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin