Weave Code
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

Simple Bus Query Bus Laravel Package

ajgl/simple-bus-query-bus

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Install the package:
    composer require ajgl/simple-bus-query-bus
    
  2. Define a query class (e.g., app/Domain/Queries/GetUserById.php):
    namespace App\Domain\Queries;
    
    class GetUserById
    {
        public function __construct(public int $id) {}
    }
    
  3. Create a handler (e.g., 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);
        }
    }
    
  4. Configure the query bus in a service provider (e.g., 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;
        });
    }
    
  5. Use the query bus in a controller or service:
    use App\Domain\Queries\GetUserById;
    
    public function show(UserQueryBus $queryBus)
    {
        $query = new GetUserById(1);
        $user = null;
        $queryBus->handle($query, $user);
        return $user;
    }
    

First Use Case

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);

Implementation Patterns

Core Workflow

  1. Define Queries: Create DTO-like classes for read operations (e.g., FindProductBySku, GetUserOrders).
  2. Implement Handlers: Each query has a single handler (e.g., FindProductBySkuHandler).
  3. Register Handlers: Map queries to handlers in the CallableMap.
  4. Resolve Handlers: Use NameBasedMessageHandlerResolver with a MessageNameResolver (e.g., ClassBasedNameResolver).
  5. Handle Queries: Pass queries to the bus with a result variable to capture the return value.

Integration Tips

  • Laravel Service Container: Use the container as the service locator for dependency injection:
    $serviceLocator = function ($serviceId) {
        return app($serviceId);
    };
    
  • Middleware Stack: Add query-specific middleware (e.g., caching, logging):
    $queryBus->appendMiddleware(new CacheQueryResultsMiddleware());
    $queryBus->appendMiddleware(new LogQueryExecutionMiddleware());
    
  • Named Queries: Implement NamedMessage for custom query names:
    class SearchProducts implements NamedMessage
    {
        public static function messageName(): string
        {
            return 'search_products';
        }
    }
    
  • Lazy Loading: Use callable strings or arrays for handlers to defer instantiation:
    $queryHandlers = [
        'App\Domain\Queries\SearchProducts' => 'App\Domain\Handlers\SearchProductsHandler',
    ];
    

Common Patterns

  1. Query Result Caching:
    $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;
        }
    });
    
  2. Validation Middleware:
    $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);
        }
    });
    
  3. Query Logging:
    $queryBus->appendMiddleware(new class implements Middleware {
        public function handle($message, callable $next)
        {
            \Log::info("Handling query: " . get_class($message));
            return $next($message);
        }
    });
    

Gotchas and Tips

Pitfalls

  1. Handler Resolution Order:
    • Ensure DelegatesToMessageHandlerAndCatchReturnMiddleware is the last middleware in the stack. Otherwise, the return value won’t be captured.
    • Example of incorrect order:
      // Wrong: Return value won't be captured
      $queryBus->appendMiddleware(new LoggingMiddleware());
      $queryBus->appendMiddleware(new DelegatesToMessageHandlerAndCatchReturnMiddleware(...));
      
  2. Circular Dependencies:
    • Avoid circular references between handlers and queries (e.g., a handler injecting the query bus).
    • Use interfaces for dependencies to enable mocking in tests.
  3. Result Variable Scope:
    • The result variable must be passed by reference to capture the return value:
      // Wrong: No reference
      $queryBus->handle($query, $result);
      
      // Correct: Pass by reference
      $queryBus->handle($query, $result);
      
  4. Handler Instantiation:
    • If using service locators, ensure all dependencies are registered in the container. Unresolvable services will throw exceptions during handler resolution.

Debugging Tips

  1. Handler Not Found:
    • Verify the query class name matches the key in CallableMap.
    • Check if the MessageNameResolver is correctly resolving the query name.
    • Enable debug logging for the bus:
      $queryBus->appendMiddleware(new class implements Middleware {
          public function handle($message, callable $next)
          {
              \Log::debug("Handling message: " . get_class($message));
              return $next($message);
          }
      });
      
  2. Middleware Not Triggering:
    • Ensure middleware is appended before DelegatesToMessageHandlerAndCatchReturnMiddleware if it should run before handling.
    • Use tap() to inspect the message bus stack:
      $queryBus->tap(function ($bus) {
          \Log::debug("Middleware stack:", $bus->middleware());
      });
      
  3. Performance Issues:
    • Profile handler resolution with Xdebug or Tideways to identify slow handlers.
    • Avoid eager-loading handlers by using callable strings or lazy resolvers.

Extension Points

  1. Custom Message Name Resolvers:
    • Implement SimpleBus\Message\Name\MessageNameResolver for custom naming logic (e.g., based on query attributes).
  2. Dynamic Handler Registration:
    • Use SimpleBus\Message\CallableResolver\CallableResolver to dynamically resolve handlers (e.g., from a database).
  3. Query Bus Decorators:
    • Wrap the bus to add cross-cutting concerns (e.g., rate limiting):
      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);
          }
      }
      
  4. Query Result Transformers:
    • Add middleware to transform results (e.g., serialize to JSON):
      $queryBus->appendMiddleware(new class implements Middleware {
          public function handle($message, callable $next)
          {
              $result = $next($message);
              return json_encode($result);
          }
      });
      

Configuration Quirks

  1. Service Locator Overrides:
    • If using Laravel’s container, ensure no conflicting bindings exist for handler classes.
    • Use when() in the service provider to conditionally bind handlers:
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.
codifyo/ts-generator-bundle
andydefer/laravel-cluster
testo/fiber
mintobit/jobqueue
a4sex/maintenance-bundle
a4sex/entity-date-update
a4sex/client-identifier
a4sex/base-utilites
a4sex/key-value-storage
a4sex/micro-status
chilldev/dependency-injection-extra
datinglibre/datinglibre-app-api
biberltd/corebundle
bricre/symfony-bundle-test
biberltd/logbundle
dominium/http-adapter-bundle
dominium/google-analytics
a4sex/auto-clean-entity
christhompsontldr/laravel-inky
spatie/mailcoach-vapor