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
Resolver Pattern
somework/cqrs-bundle
cursor
symfony
ddd
event-bus
Messenger
cqrs
command-bus
query-bus

AbstractMessageTypeResolver + MessageTypeLocator implement hierarchy-aware service resolution. The resolution order is deterministic and intentionally more precise than Symfony's flat lookup:

Install
php artisan boost:add-skill somework/cqrs-bundle

Save this content to: .cursor/rules/resolver-pattern.md

# Message Type Resolver Pattern

## Architecture

`AbstractMessageTypeResolver` + `MessageTypeLocator` implement hierarchy-aware service resolution. The resolution order is deterministic and intentionally more precise than Symfony's flat lookup:

1. Exact message class match
2. Parent classes (walking up via `get_parent_class()`)
3. Interfaces of the class (including inherited ones), most specific first: sorted by interface depth, ties keep declaration order — the same order `DispatchModeDecider` uses
4. Type default / global default (handled in `resolveFallback()`)

## Creating a New Resolver

Extend `AbstractMessageTypeResolver` and implement two hooks:

- **`assertService(string $key, mixed $service): mixed`** — Validate the resolved service matches your expected type. Throw `Exception\LogicException` (a `\LogicException` that implements `CqrsException`) with `get_debug_type()` for clear diagnostics. The bundle's registrars pass plain references to `ServiceLocatorTagPass::register()`, so the locator returns the service itself, never a closure, and a new resolver need not handle closures. `MessageSerializerResolver` and `MessageMetadataProviderResolver` still call a closure they get, for locators built by hand (tests, applications).
- **`resolveFallback(object $message): mixed`** — Provide the default when no hierarchy match is found. Two variants exist in the codebase:
  - **Simple:** Return a stored default (see `RetryPolicyResolver`)
  - **Default key:** Look up the resolver's `DEFAULT_KEY` in the locator (see `MessageSerializerResolver`, `MessageMetadataProviderResolver`, `RateLimitResolver`). The registrar stores the per-type default, or the global default when the type has none, under that key; the resolver never walks two levels of defaults

Pass `DEFAULT_KEY` as an ignored key to `resolveService()` so the hierarchy walk never matches it.

## WeakMap Caching

`MessageTypeLocator` uses a static `WeakMap<ContainerInterface, ...>` so cache lifetime is tied to the container — automatic cleanup on kernel reboot or test teardown, no manual invalidation needed.

Cache key structure: `container → messageClass → ignoredSignature`; misses are cached as `null` too. Service locators are immutable, so the cache is never reset at runtime (there is no `kernel.reset` hook). The ignored signature is computed from sorted, deduped, null-byte-joined key names. When adding a new resolver that needs to exclude keys from hierarchy walk (like serializer/metadata exclude their default keys), pass them as `$ignoredKeys` to `resolveService()` — the cache handles this automatically.

On top of it, `AbstractMessageTypeResolver::resolveService()` keeps the resolved service per message class and ignored keys in an instance array (resolution runs on every dispatch; the array makes it about five times cheaper than the WeakMap lookup plus `ServiceLocator::get()`). Services are therefore resolved once per class: they must be stateless, and a service defined as not shared is reused. `MessageTransportResolver` does the same per class but skips values computed by closures (only found in locators built by hand), which are re-invoked on every call.

## MessageTransportResolver Exception

`MessageTransportResolver` does NOT extend `AbstractMessageTypeResolver` because it needs:
- Nullable return type (`?array` vs always-resolved service)
- Selective caching: static sources cached, closures re-invoked each call
- Complex normalization logic: `TransportRegistrar` stores each transport list as an `ArrayObject` service (a `Traversable`); strings, arrays and closures (with an optional container parameter, re-invoked on every call) are accepted from locators built by hand

If your new resolver needs nullable returns or dynamic sources, follow `MessageTransportResolver`'s standalone pattern. Otherwise, extend the abstract base.

## Corresponding Registrar

Every resolver needs a registrar in `src/DependencyInjection/Registration/` that builds its service locator from config. See `di-registrar-pattern.md` for registrar conventions.

Message Type Resolver Pattern

Architecture

AbstractMessageTypeResolver + MessageTypeLocator implement hierarchy-aware service resolution. The resolution order is deterministic and intentionally more precise than Symfony's flat lookup:

  1. Exact message class match
  2. Parent classes (walking up via get_parent_class())
  3. Interfaces of the class (including inherited ones), most specific first: sorted by interface depth, ties keep declaration order — the same order DispatchModeDecider uses
  4. Type default / global default (handled in resolveFallback())

Creating a New Resolver

Extend AbstractMessageTypeResolver and implement two hooks:

  • assertService(string $key, mixed $service): mixed — Validate the resolved service matches your expected type. Throw Exception\LogicException (a \LogicException that implements CqrsException) with get_debug_type() for clear diagnostics. The bundle's registrars pass plain references to ServiceLocatorTagPass::register(), so the locator returns the service itself, never a closure, and a new resolver need not handle closures. MessageSerializerResolver and MessageMetadataProviderResolver still call a closure they get, for locators built by hand (tests, applications).
  • resolveFallback(object $message): mixed — Provide the default when no hierarchy match is found. Two variants exist in the codebase:
    • Simple: Return a stored default (see RetryPolicyResolver)
    • Default key: Look up the resolver's DEFAULT_KEY in the locator (see MessageSerializerResolver, MessageMetadataProviderResolver, RateLimitResolver). The registrar stores the per-type default, or the global default when the type has none, under that key; the resolver never walks two levels of defaults

Pass DEFAULT_KEY as an ignored key to resolveService() so the hierarchy walk never matches it.

WeakMap Caching

MessageTypeLocator uses a static WeakMap<ContainerInterface, ...> so cache lifetime is tied to the container — automatic cleanup on kernel reboot or test teardown, no manual invalidation needed.

Cache key structure: container → messageClass → ignoredSignature; misses are cached as null too. Service locators are immutable, so the cache is never reset at runtime (there is no kernel.reset hook). The ignored signature is computed from sorted, deduped, null-byte-joined key names. When adding a new resolver that needs to exclude keys from hierarchy walk (like serializer/metadata exclude their default keys), pass them as $ignoredKeys to resolveService() — the cache handles this automatically.

On top of it, AbstractMessageTypeResolver::resolveService() keeps the resolved service per message class and ignored keys in an instance array (resolution runs on every dispatch; the array makes it about five times cheaper than the WeakMap lookup plus ServiceLocator::get()). Services are therefore resolved once per class: they must be stateless, and a service defined as not shared is reused. MessageTransportResolver does the same per class but skips values computed by closures (only found in locators built by hand), which are re-invoked on every call.

MessageTransportResolver Exception

MessageTransportResolver does NOT extend AbstractMessageTypeResolver because it needs:

  • Nullable return type (?array vs always-resolved service)
  • Selective caching: static sources cached, closures re-invoked each call
  • Complex normalization logic: TransportRegistrar stores each transport list as an ArrayObject service (a Traversable); strings, arrays and closures (with an optional container parameter, re-invoked on every call) are accepted from locators built by hand

If your new resolver needs nullable returns or dynamic sources, follow MessageTransportResolver's standalone pattern. Otherwise, extend the abstract base.

Corresponding Registrar

Every resolver needs a registrar in src/DependencyInjection/Registration/ that builds its service locator from config. See di-registrar-pattern.md for registrar conventions.

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.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib