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
Closures as Expressions are available as part of Ecotone Enterprise and require PHP 8.5+.
Without an Enterprise licence, using a Closure expression fails fast during application bootstrap with a LicensingException.
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 parameterMessagetype hint - the whole MessageDefault 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:
$valuein#[Header]- receives the given header value$valuein#[Payload]and#[Fetch]- receives the Message payload$servicein#[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
staticand they cannot capture variables (use) or$thisNo 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 expressionWorks 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
expressionsemantics) are supported out of the box in Ecotone Free - the Enterprise licence applies toexpressionproperties of Ecotone's Attributes
Last updated
Was this helpful?