Synchronous in-process CQRS buses for Momo Framework. Provides a strictly typed interface for Command and Query dispatching within a single request cycle.
momo-framework/bus provides two lightweight, in-process buses for separating write and read operations in your application — following the CQRS pattern.
| Bus | Purpose | Returns |
|---|---|---|
CommandBus |
Write operations — create, update, delete | void |
QueryBus |
Read operations — fetch, list, search | mixed |
Both buses are always synchronous — handlers execute in the same process, in the same request cycle. For async processing, use momo-framework/queue.
Both buses enforce a one-handler-per-message contract. Registering a second handler for the same message class silently overwrites the first.
- PHP
>= 8.5 momo-framework/kernel
Auto-discovered by Momo Framework via PackageManifest — no manual registration needed.
composer require momo-framework/busHTTP / gRPC / CLI / GraphQL
│
▼
Controller / Resolver (delivery layer — knows about protocol)
│
│ new CreateOrderCommand(...)
▼
CommandBus (this package)
│
▼
CreateOrderHandler (application layer — knows about domain)
│
▼
Domain / Infrastructure (pure business logic)
Adding a new protocol (gRPC, GraphQL, CLI) means writing a new adapter that dispatches the same Command or Query. Handlers never change.
Define a command as a simple readonly value object:
final readonly class CreateOrderCommand implements CommandInterface
{
public function __construct(
public string $customerId,
public array $items,
) {}
}Define a handler that performs the operation:
final class CreateOrderHandler implements CommandHandlerInterface
{
public function __construct(
private readonly OrderRepository $orders,
private readonly QueueInterface $queue,
) {}
public function handle(CommandInterface $command): void
{
assert($command instanceof CreateOrderCommand);
$order = Order::create($command->customerId, $command->items);
$this->orders->save($order);
// Heavy work goes to the queue — bus stays synchronous
$this->queue->push(new SendOrderEmailJob($order->id));
}
}Dispatch from a controller:
$commandBus->dispatch(new CreateOrderCommand($customerId, $items));
// returns void — fire and forgetDefine a query:
final readonly class GetOrderQuery implements QueryInterface
{
public function __construct(
public string $orderId,
) {}
}Define a handler that returns data:
final class GetOrderHandler implements QueryHandlerInterface
{
public function __construct(
private readonly OrderRepository $orders,
) {}
public function handle(QueryInterface $query): mixed
{
assert($query instanceof GetOrderQuery);
return $this->orders->findById($query->orderId);
}
}Ask from a controller:
$order = $queryBus->ask(new GetOrderQuery($orderId));
// returns whatever the handler returnsfinal class ShopServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->singleton(CreateOrderHandler::class);
$this->singleton(GetOrderHandler::class);
}
public function boot(): void
{
$this->app->make(CommandBusInterface::class)
->register(CreateOrderCommand::class, $this->app->make(CreateOrderHandler::class));
$this->app->make(QueryBusInterface::class)
->register(GetOrderQuery::class, $this->app->make(GetOrderHandler::class));
}
}# install dependencies
composer install
# run tests
composer test
# run tests with coverage report (requires PCOV)
composer test:coverage
# static analysis — PHPStan level 10
composer stan
# code style check
composer lint
# code style fix
composer lint:fix
# rector — check for upgrades
composer rector:check
# run full CI pipeline locally
composer cicomposer ci
├── lint php-cs-fixer --dry-run
├── stan phpstan level 10
├── rector:check rector --dry-run
└── test phpunit