For the complete documentation index, see llms.txt. This page is also available as Markdown.

Closures as Expressions

Type-safe Closures as alternative to expression language in PHP attributes

PHP 8.5 introduced Closures in constant expressions, which makes it possible to define Closures directly inside Attributes. Ecotone builds on top of that: every Attribute that provides an expression property accepts a Closure as an alternative to the Symfony Expression Language string.

// Expression Language
#[Delayed(expression: 'payload.dueDate')]

// Closure
#[Delayed(expression: static function (#[Payload] OrderWasPlaced $event): \DateTimeInterface {
    return $event->dueDate;
})]

While Expression Language strings are compact, Closures give you:

  • Type safety and static analysis - PHPStan/Psalm verify the code, misspelled properties fail before deployment, not in production

  • IDE support - autocompletion, go-to-definition and find-usages just work

  • Refactoring safety - renaming a property or method updates the Closure automatically

  • No new syntax to learn - it is plain PHP instead of a separate expression dialect

How Closure parameters are resolved

The Closure is executed like a regular Message Handler - its parameters are resolved from the currently handled Message using the same Parameter Converters you already know:

#[CommandHandler('order.place')]
public function placeOrder(
    PlaceOrder $command,
    #[Header('token', expression: static function (
        #[Header('token')] string $token,
        #[Reference] TokenService $tokenService,
    ): string {
        return $tokenService->normalize($token);
    })] string $token,
): void {}

Inside the Closure you may use:

  • #[Payload] - payload of the Message (with Conversion applied, e.g. deserialized Command)

  • #[Header('name')] / #[Headers] - specific header or all headers

  • #[Reference] - Service from Dependency Container

  • #[ConfigurationVariable] - configuration parameter

  • Message type hint - the whole Message

  • Default Converters - first parameter defaults to payload, class type hints resolve Services from the container

All of them can be combined within single Closure:

And thanks to Default Converters, simple cases need no attributes at all - the first parameter receives the payload and class type hints resolve Services:

Context variables bound by parameter name

Expression Language exposes special variables like value or service. For Closures, those are bound by parameter name, so you can keep the same semantics without extra attributes:

  • $value in #[Header] - receives the given header value

  • $value in #[Payload] and #[Fetch] - receives the Message payload

  • $service in #[Reference] - receives the referenced Service

For Database Business Methods there is no Message involved, so Closure parameters bind by name to the Business Method parameters instead.

Use cases

Deriving Message Handler parameters

Transform payload or headers before they reach your handler - Method Invocation:

Delaying Messages and Time to Live

Compute delay or expiration from the Message itself - Delaying Messages, Time to Live:

Enriching Message Headers

Add dynamically computed headers when Message is sent to asynchronous channel:

Fetching Aggregates

Resolve Aggregate identifiers with full type safety - Instant Fetch Aggregate:

Deduplication

Derive deduplication keys from Message data - Idempotent Consumer:

Tenant resolution

Derive the tenant from inbound Messages - Multi-Tenancy:

Database Business Methods

For Dbal Parameters there is no Message at evaluation time, therefore Closure parameters are bound by name to the Business Method parameters (and $payload for parameter level DbalParameter):

Good to know

  • Closures must be static and self-contained - PHP requires Closures inside Attributes to be declared static and they cannot capture variables (use) or $this

  • No nested Closure expressions - parameters of a Closure expression may use Parameter Converters with Expression Language strings (e.g. #[Header('name', expression: 'value.trim()')]), but not another Closure expression

  • Works with compiled containers - Ecotone resolves the Closure's parameter converters during container compilation and re-reads the Closure from its Attribute declaration at runtime, so production setups with dumped container cache are fully supported

  • Closures in your own custom Attributes (without expression semantics) are supported out of the box in Ecotone Free - the Enterprise licence applies to expression properties of Ecotone's Attributes

Last updated

Was this helpful?