composer require ajgl/simple-bus-query-bus
app/Domain/Queries/GetUserById.php):
namespace App\Domain\Queries;
class GetUserById
{
public function __construct(public int $id) {}
}
app/Domain/Handlers/GetUserByIdHandler.php):
namespace App\Domain\Handlers;
use App\Domain\Queries\GetUserById;
use App\Models\User;
class GetUserByIdHandler
{
public function handle(GetUserById $query): User
{
return User::findOrFail($query->id);
}
}
app/Providers/QueryBusServiceProvider.php):
use Ajgl\SimpleBus\Message\Bus\Middleware\CatchReturnMessageBusSupportingMiddleware;
use SimpleBus\Message\Bus\MessageBus;
use SimpleBus\Message\CallableResolver\CallableMap;
use SimpleBus\Message\CallableResolver\ServiceLocatorAwareCallableResolver;
use SimpleBus\Message\Handler\Resolver\NameBasedMessageHandlerResolver;
use SimpleBus\Message\Name\ClassBasedNameResolver;
public function register()
{
$this->app->singleton(MessageBus::class, function ($app) {
$queryBus = new MessageBus();
$queryBus->appendMiddleware(new CatchReturnMessageBusSupportingMiddleware());
$queryHandlers = [
GetUserById::class => $app->make(GetUserByIdHandler::class),
];
$queryHandlerMap = new CallableMap(
$queryHandlers,
new ServiceLocatorAwareCallableResolver($app)
);
$queryBus->appendMiddleware(
new DelegatesToMessageHandlerAndCatchReturnMiddleware(
new NameBasedMessageHandlerResolver(
new ClassBasedNameResolver(),
$queryHandlerMap
)
)
);
return $queryBus;
});
}
use App\Domain\Queries\GetUserById;
public function show(UserQueryBus $queryBus)
{
$query = new GetUserById(1);
$user = null;
$queryBus->handle($query, $user);
return $user;
}
Replace a direct Eloquent call in a controller with a query bus:
// Before (direct call)
$user = User::findOrFail($id);
// After (query bus)
$query = new GetUserById($id);
$user = null;
$queryBus->handle($query, $user);
FindProductBySku, GetUserOrders).FindProductBySkuHandler).CallableMap.NameBasedMessageHandlerResolver with a MessageNameResolver (e.g., ClassBasedNameResolver).$serviceLocator = function ($serviceId) {
return app($serviceId);
};
$queryBus->appendMiddleware(new CacheQueryResultsMiddleware());
$queryBus->appendMiddleware(new LogQueryExecutionMiddleware());
NamedMessage for custom query names:
class SearchProducts implements NamedMessage
{
public static function messageName(): string
{
return 'search_products';
}
}
$queryHandlers = [
'App\Domain\Queries\SearchProducts' => 'App\Domain\Handlers\SearchProductsHandler',
];
$queryBus->appendMiddleware(new class implements Middleware {
public function handle($message, callable $next)
{
$cacheKey = 'query_' . md5(get_class($message));
if (cache()->has($cacheKey)) {
return cache()->get($cacheKey);
}
$result = $next($message);
cache()->put($cacheKey, $result, now()->addMinutes(10));
return $result;
}
});
$queryBus->appendMiddleware(new class implements Middleware {
public function handle($message, callable $next)
{
if (!method_exists($message, 'validate')) {
throw new \RuntimeException('Query must implement validate()');
}
$message->validate();
return $next($message);
}
});
$queryBus->appendMiddleware(new class implements Middleware {
public function handle($message, callable $next)
{
\Log::info("Handling query: " . get_class($message));
return $next($message);
}
});
DelegatesToMessageHandlerAndCatchReturnMiddleware is the last middleware in the stack. Otherwise, the return value won’t be captured.// Wrong: Return value won't be captured
$queryBus->appendMiddleware(new LoggingMiddleware());
$queryBus->appendMiddleware(new DelegatesToMessageHandlerAndCatchReturnMiddleware(...));
// Wrong: No reference
$queryBus->handle($query, $result);
// Correct: Pass by reference
$queryBus->handle($query, $result);
CallableMap.MessageNameResolver is correctly resolving the query name.$queryBus->appendMiddleware(new class implements Middleware {
public function handle($message, callable $next)
{
\Log::debug("Handling message: " . get_class($message));
return $next($message);
}
});
DelegatesToMessageHandlerAndCatchReturnMiddleware if it should run before handling.tap() to inspect the message bus stack:
$queryBus->tap(function ($bus) {
\Log::debug("Middleware stack:", $bus->middleware());
});
Xdebug or Tideways to identify slow handlers.SimpleBus\Message\Name\MessageNameResolver for custom naming logic (e.g., based on query attributes).SimpleBus\Message\CallableResolver\CallableResolver to dynamically resolve handlers (e.g., from a database).class RateLimitedQueryBus implements MessageBus
{
public function handle($message, &$result)
{
if ($this->isRateLimited($message)) {
throw new \RuntimeException('Query rate limit exceeded');
}
$this->bus->handle($message, $result);
}
}
$queryBus->appendMiddleware(new class implements Middleware {
public function handle($message, callable $next)
{
$result = $next($message);
return json_encode($result);
}
});
when() in the service provider to conditionally bind handlers:How can I help you explore Laravel packages today?