brammm/tactishun

A simple PHP 8 command bus implementation

v0.3.0 2025-08-12 18:57 UTC

README

PHP Version Latest Version on Packagist Software License Build status

A lightweight command bus implementation for PHP 8.3+ projects, designed to provide a straightforward approach to hexagonal architecture and CQRS patterns.

Features

  • Simple command bus pattern implementation
  • PSR-11 container integration
  • Extensible middleware system
  • Zero configuration required to get started
  • Full PHPStan coverage

Installation

Install via Composer:

composer require brammm/tactishun

Quickstart

use Brammm\Tactishun\CommandBus;
use Brammm\Tactishun\CommandHandler\CommandHandler;
use Brammm\Tactishun\HandledBy;

// 1. Create your command
#[HandledBy(SendWelcomeMailHandler::class)]
final readonly class SendWelcomeMail
{
    public function __construct(
        public UserId $userId,
    ) {}
}

// 2. Implement the handler
/** @implements CommandHandler<SendWelcomeMail> */
final class SendWelcomeMailHandler implements CommandHandler
{
    public function handle(object $command): void
    {
        // Fetch user from $command->userId and send welcome email
    }
}

// 3. Set up the command bus
$container = new Container(); // Any PSR-11 compatible container
$commandBus = new CommandBus($container);

// 4. Dispatch commands
$commandBus->handle(new SendWelcomeMail($user->id));

Usage

Handling multiple commands with a single command handler

It's possible to have multiple commands handled by a single command handler. Simply add the HandledBy attribute with the same handler to multiple command classes.

For convenience, you can extend from the MultipleCommandsHandler (instead of using the CommandHandler interface). It forwards the command to separate handleCommandName methods.

use Brammm\Tactishun\CommandHandler\MultipleCommandsHandler;
use Brammm\Tactishun\HandledBy;

#[HandledBy(UserCommandHandler::class)]
final readonly class RegisterUser
{
}

#[HandledBy(UserCommandHandler::class)]
final readonly class DeactivateUser
{
}

/** @extends MultipleCommandsHandler<RegisterUser|DeactivateUser> */
final class UserCommandHandler extends MultipleCommandsHandler
{
    public function handleRegisterUser(RegisterUser $command): void
    {
    }

    public function handleDeactivateUser(DeactivateUser $command): void
    {
    }
}

Extending functionality through middleware

Extend functionality with middleware that wraps command execution:

final readonly class LoggingMiddleware implements Middleware
{
    public function __construct(
        private LoggerInterface $logger
    ) {}

    public function process(object $command, CommandHandler $commandHandler): void
    {
        $commandName = get_class($command);
        $this->logger->info("Executing command: {$commandName}");

        $startTime = microtime(true);
        $commandHandler->handle($command);
        $executionTime = microtime(true) - $startTime;

        $this->logger->info("Command {$commandName} completed in {$executionTime}ms");
    }
}

// Register middleware
$commandBus->add(new LoggingMiddleware($logger));

Middleware are added First In, First Out.

Through Middleware, it is possible to provide async queued functionality. No Middleware to facilitate this are shipped with this package at the moment.

Custom CommandHandlerResolvers

By default, Tactishun uses the AttributeCommandHandlerResolver to find handlers via the HandledBy attribute. This allows the zero-configuration setup. If you'd rather not use that attribute or have a different solution in mind, you can provide a custom resolver as the second parameter:

class ClassMapResolver implements CommandHandlerResolver
{
    private array $handlerMap = [
        SendWelcomeMail::class => SendWelcomeMailHandler::class,
        ProcessPayment::class => ProcessPaymentHandler::class,
    ];

    public function resolve(object $command): string
    {
        $commandClass = get_class($command);
        return $this->handlerMap[$commandClass] ?? throw new HandlerNotFound($commandClass);
    }
}

$commandBus = new CommandBus($container, new ClassMapResolver());

Note that command handler resolvers must return a class-string for a command handler that the container implementation can then resolve to an instance of that handler.

Inspiration

This library draws inspiration from: