Skip to content

feat: inject attributes declared on handler into closure expressions (Enterprise)#685

Merged
dgafka merged 1 commit into
mainfrom
attribute-injection-in-closure-expressions
Jul 20, 2026
Merged

feat: inject attributes declared on handler into closure expressions (Enterprise)#685
dgafka merged 1 commit into
mainfrom
attribute-injection-in-closure-expressions

Conversation

@dgafka

@dgafka dgafka commented Jul 20, 2026

Copy link
Copy Markdown
Member

Why is this change proposed?

Closure expressions can resolve #[Header], #[Payload], #[Reference] and friends, but not an Attribute declared on the handler they sit on. A closure parameter type hinted with such an Attribute was treated as the payload, or looked up as a service in the container, and failed with a misleading error.

This left configuration and behaviour unable to sit together: an Attribute can declare policy next to the endpoint, but the expression deciding how to act on that policy could not read it. The same closure had to be duplicated per variation, or the policy hardcoded into the closure.

Description of Changes

  • Closure expression parameters type hinted with an Attribute are now resolved against the owning method first, then the owning class.
  • Reuses the existing AttributeBuilder / AttributeReference mechanism, so the attribute is resolved lazily through reflection and survives the dumped container (covered cold and warm).
  • No new licence gate — VerifyEnterpriseLicenceForClosureExpressions already covers anything reached through a closure expression.
  • Adds ConsoleCommandTenantPropagationTest, covering tenant header propagation into #[ConsoleCommand] and onward into a Command Bus sub-flow.

Example

Configuration is declared once as an Attribute, and a single shared closure adapts to it:

final class OrderService
{
    #[DedupPolicy(scope: 'customer')]
    #[Deduplicated(expression: static function (
        DedupPolicy $policy,
        #[Header('customerId')] string $customerId,
        #[Header('orderId')] string $orderId,
    ): string {
        return $policy->scope === 'customer' ? $customerId : $orderId;
    })]
    #[CommandHandler('order.process')]
    public function process(): void
    {
        // deduplicated per customer
    }
}

Changing scope to 'order' switches deduplication to per order, with the closure left untouched.

Pull Request Contribution Terms

  • I have read and agree to the contribution terms outlined in CONTRIBUTING.

Drafted with /superpowers:test-driven-development, /superpowers:verification-before-completion, /ecotone-contributor.

…(Enterprise)

Closure expressions could resolve #[Header], #[Payload], #[Reference] and
friends, but not an Attribute declared on the owning handler. Such a
parameter fell through to payload conversion or a container lookup,
failing with a misleading error.

Resolve attribute typed closure parameters against the owning method,
then the owning class, reusing AttributeBuilder so resolution stays lazy
and survives the dumped container.

Also adds coverage for propagating tenant header into #[ConsoleCommand].
@dgafka
dgafka merged commit 4936d3c into main Jul 20, 2026
11 of 13 checks passed
@dgafka
dgafka deleted the attribute-injection-in-closure-expressions branch July 20, 2026 19:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant