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 Bundle Laravel Package

micro-module/tactician-bundle

MicroModule TacticianBundle integrates the Tactician command bus into Symfony applications, helping you configure commands, handlers, and middleware with less boilerplate and structure command-driven business logic cleanly.

View on GitHub
Deep Wiki
Context7

Getting Started

⚠️ Reality check first: micro-module/tactician-bundle has 0 stars and 0 dependents. It is not the official integration — that's league/tactician-bundle. Before adopting it, diff its config tree and middleware wiring against league/tactician-bundle; if they match, prefer the maintained package. The guidance below assumes the API mirrors the standard Tactician/Symfony conventions (command bus service tactician.commandbus, handler tag tactician.handler, middleware tag tactician.middleware).

  1. Install

    composer require micro-module/tactician-bundle
    

    For Symfony Flex projects the recipe (if any) registers the bundle automatically. If not, add it to config/bundles.php:

    return [
        // ...
        MicroModule\TacticianBundle\TacticianBundle::class => ['all' => true],
    ];
    
  2. First place to look: the generated/example config at config/packages/tactician.yaml. Confirm the default command bus exists:

    tactician:
        commandbus:
            default:
                middleware:
                    - tactician.middleware.locking   # optional
                    - tactician.middleware.command_handler
    
  3. Verify boot

    bin/console debug:container tactician.commandbus
    bin/console debug:container --tag=tactician.handler
    

    If the second command lists nothing, your handlers aren't being tagged yet (see Gotchas).

  4. First use case — a classic write operation:

    final class RegisterUser
    {
        public function __construct(
            public readonly string $email,
            public readonly string $plainPassword,
        ) {}
    }
    
    final class RegisterUserHandler
    {
        public function __construct(private UserRepository $users, private PasswordHasher $hasher) {}
    
        public function __invoke(RegisterUser $command): void
        {
            $this->users->save(new User($command->email, $this->hasher->hash($command->plainPassword)));
        }
    }
    

    Register the handler with the tag (or rely on autoconfiguration if the bundle provides it):

    App\Application\User\RegisterUserHandler:
        tags: [{ name: tactician.handler, command: App\Application\User\RegisterUser }]
    
  5. Dispatch from a controller — inject CommandBus (the interface, not the concrete class):

    public function __invoke(Request $request, CommandBus $bus): Response
    {
        $bus->handle(new RegisterUser($request->get('email'), $request->get('password')));
        return new Response('', 201);
    }
    

Implementation Patterns

Tag by command, not by interface. Each handler is a plain callable/invokable service. The command attribute on the tag tells the bus which command class it handles. This is the single most important wiring detail — miss it and you get a runtime No handler for command error.

Use one bus per bounded context, not per command. Multiple named buses (default, sync, async) let you attach different middleware stacks — e.g. a logger/transaction middleware on the synchronous bus, a queued transport on the async one. Inject the specific bus via a named alias if your app grows.

Middleware is the extension surface. Every cross-cutting concern — transactions, logging, validation, authorization — belongs in a middleware, not in handlers:

final class TransactionMiddleware implements Middleware
{
    public function __construct(private EntityManagerInterface $em) {}

    public function execute(object $command, callable $next): object
    {
        $this->em->beginTransaction();
        try {
            $result = $next($command);
            $this->em->flush();
            $this->em->commit();
            return $result;
        } catch (\Throwable $e) {
            $this->em->rollback();
            throw $e;
        }
    }
}

Tag it tactician.middleware, then list it before tactician.middleware.command_handler in the config. Order is the pipeline order.

Combine with Symfony's Validator via a ValidationMiddleware that validates the command object. Commands are DTOs, so Symfony constraints (@Assert\Email, etc.) work directly on them — this keeps validation declarative and out of the handler.

Dispatch from controllers or from other services, never both deeply nested in one request. Nesting command buses is possible ($bus->handle() from inside a handler) but it breaks transaction and middleware assumptions. Prefer a single top-level dispatch and let the handler orchestrate its own collaborators.

Test at two levels: unit-test handlers directly (they're plain classes — no container needed), and integration-test the bus wiring with KernelTestCase asserting debug:container shows the handler tagged. For the latter, a smoke test that dispatches one command and asserts side effects catches most misconfiguration.

Return values: Tactician's handle() returns whatever the final middleware returns — usually the handler's return value or null. If you need a result (e.g. an ID), have the handler return it; otherwise treat commands as fire-and-forget. Do not assume a value is present.

Gotchas and Tips

  • Autoconfiguration is not guaranteed. If handlers aren't auto-tagged after composer require, you'll see No handler for command [X]. Diagnose with bin/console debug:container --tag=tactician.handler and wire explicitly, or add an autoconfigure rule in services.yaml for your handler namespace.
  • The tag command attribute must be the FQCN, not the short name or the handler class name. Copy-paste errors here are the #1 cause of runtime failures.
  • Middleware ordering bites silently. A command_handler middleware placed before a transaction middleware means transactions wrap nothing. Read the config array top-to-bottom as execution order.
  • Handlers must be stateless and idempotent-friendly. The bus doesn't guarantee one dispatch per request; retries (if a retry middleware is present) will re-invoke the handler. Don't cache request-scoped state on the handler.
  • Config is cached. After editing tactician.yaml, run bin/console cache:clear in dev — a stale container will keep old middleware even after a code change.
  • No built-in async. This bundle is synchronous only. If you need queues, that's a middleware addition (e.g. Bernard/Redis) — plan for it explicitly rather than assuming it's provided.
  • Debugging without middleware: add a small logging middleware early in the stack that dumps get_class($command). In league/tactician-bundle there's tactician.middleware.logger; confirm whether this fork ships an equivalent — if not, write the 10-line one yourself.
  • Version compatibility risk. With zero dependents and zero stars, verify by inspecting composer.json in the repo that the constraint on league/tactician and the Symfony framework-bundle range matches your app (e.g. ^6.0 || ^7.0). If it pins an old Symfony major, you'll hit a resolver conflict at install and should switch to the official package.
  • Consider migration path. If you do adopt it, keep the CommandBus interface (League\Tactician\CommandBus) type-hinted in your code, not the concrete bus class — that makes swapping this bundle for league/tactician-bundle later a config-only change.
  • Extension point worth knowing: the bundle's DI extension usually exposes a compiler pass to auto-register handler tags from a namespace. Look in the bundle's DependencyInjection/ folder for a ...CompilerPass and whether it's registered in the bundle build() — if it exists, you can skip hand-tagging every handler.
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.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core