feat: support closures in attributes and closure expressions (Enterprise)#678
Merged
Conversation
…ise) Fixes #649 by lazily re-reading attributes via reflection when they cannot be serialized into the container. Adds Enterprise support for passing Closures to every attribute expression property, executed with full message-handler parameter resolution (#[Payload], #[Header], #[Reference], ...).
…d closures ExpressionEvaluationService gains evaluateWithMessage() and evaluateWithContext(), dispatching to Symfony expressions or ClosureExpressionEvaluator under the hood. Additional context variables (value, service, business method parameters) bind to closure parameters by name. All expression call sites now use the single entrypoint, removing per-site closure branching and evaluator dependencies.
AttributeDeclaration::resolveClosure dumps a recipe recreating the Closure from its attribute declaration, mirroring Symfony's Closure::fromCallable approach. ClosureExpressionInvokerCompiler resolves all closure parameter converters at container build time reusing standard converter builders, so handler-parameter and Fetch closure expressions run without runtime reflection. Runtime evaluator remains for interceptor-injected attributes, DbalParameter context binding and nested closure expressions.
…olution ClosureExpressionInvokerCompiler is now the only place deciding how closure parameters resolve. The runtime ClosureExpressionEvaluator consumes the same resolver definitions through a small definition interpreter instead of duplicating the attribute-to-converter mapping, shrinking its dependencies to LicenceDecider and the container. Nested closure expressions flow through the shared plan via compileForRuntimeResolution, and Fetch with closure expression on closure parameters fails fast with a clear configuration error.
VerifyEnterpriseLicenceForClosureExpressions compiler pass walks container definitions and throws LicensingException during bootstrap when closure expressions are used without Enterprise licence - detecting compiled closure recipes, attribute definitions carrying closures and attribute references. Runtime licence checks are removed from ClosureExpressionInvoker and ClosureExpressionEvaluator, aligning with how other Enterprise features fail fast at configuration time.
Nested closure expressions (closure expression attributes on closure parameters) are disallowed with clear ConfigurationException, making compilation infallible and removing the runtime-only resolution fallback. ClosureExpressionEvaluator is removed entirely - every closure expression now compiles into container definitions. Interceptor-injected attributes (Delayed, AddHeader, TimeToLive, Deduplicated, WithTenantResolver) receive pre-compiled ClosureExpressionInvoker through parameters marked with InvokerFor attribute, resolved per intercepted endpoint at build time. DbalParameter closures compile into ContextClosureExpressionInvoker bound by business method parameter names. ExpressionEvaluationService returns to string-only expressions, as closures never reach it anymore.
… expression program AttributeExpressionExecutor replaces ClosureExpressionInvoker and the attribute injection in interceptors - it carries the intercepted attribute together with its compiled expression program (closure parameter resolvers or Expression Language string) exposing execute(message). Interceptors declare single ExecutorFor parameter per attribute instead of attribute and invoker pairs, and string expressions in interceptors are now compiled through the same executor, removing string-vs-closure branching at runtime. AttributeDeclaration::resolveClosure recipe becomes obsolete, as executors derive the closure from the attribute they carry.
…ed expression execution
…ation of DbalParameter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why is this change proposed?
PHP 8.5 allows closures inside attributes, but using one in any Ecotone attribute crashed container compilation with
Serialization of 'Closure' is not allowed(fixes #649). This PR fixes that, and builds on it to let everyexpressionproperty accept aClosureas a type-safe, refactorable, IDE-friendly alternative to Symfony expression strings (Ecotone Enterprise).Description of Changes
AttributeDefinitionnow carries anAttributeDeclaration(declaring class/method/parameter). When an attribute cannot be serialized, the container re-reads it via reflection at runtime — works with both the in-memory and the dumped/cached container.expressionproperty acceptsstring|Closure:#[Payload],#[Header],#[Reference],#[Fetch],#[AddHeader],#[Delayed],#[TimeToLive],#[Deduplicated],#[DbalParameter],#[WithTenantResolver].ClosureExpressionEvaluatorexecutes the closure like a message handler — closure parameters resolve via#[Payload](with conversion),#[Header],#[Headers],#[Reference],#[ConfigurationVariable],Messagetype-hints, and first-parameter-defaults-to-payload.DbalParameterclosures map parameters by name to the same context string expressions see.LicensingException(covered by tests for each path).Usage examples
Use cases
Flow
sequenceDiagram participant Compile as Container Compilation participant Dumped as Dumped Container participant Evaluator as ClosureExpressionEvaluator participant Handler Compile->>Compile: attribute contains Closure - cannot serialize Compile->>Dumped: store AttributeDeclaration (class/method/parameter) Note over Dumped: runtime boot Dumped->>Dumped: re-read attribute via reflection - live Closure Dumped->>Evaluator: evaluate(Closure, Message) Evaluator->>Evaluator: resolve closure parameters (Payload, Header, Reference, ...) Evaluator->>Handler: converted argument / enriched headerPull Request Contribution Terms