-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[RFC] Attribute-based Interception for Service Methods #59730
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
AOP finds extensive application in the Java ecosystem. Taking into account the current trends and the evolutionary trajectory of Java, the proxy - based approach appears to be the most viable and efficient solution. |
The idea sounds great. Nevertheless, the proxy approach have some other limitations: this cannot be done with final class or final methods, and I don't think this proxy stuff can be done thanks to PHP 8.4 proxy mechanism. If using VarDumper component's virtual proxies, another problem could be that it cannot "proxify" methods which might return |
Yes, that would be other limitations, and we could throw an exception at compile time to let devs know that the interceptors cannot be applied if the class or method is declared as final |
Although if the service is an interface it should viable as decorating the original service will be the only option we'll have, in that case, it doesn't matter if the class or method is final |
yeah it would be actually very nice if the AOP attributes could be added on interface methods 👍 |
I like this. It's inline with the direction we took with attributes which is to reduce boilerplate around infrastructural concerns, simplifying configuration to allow focusing on business logic. |
I do believe the PHP engine can improve the implementation by supporting hook calls for methods. I'm imagine something like this: set_hook_func(Foo::class, 'barMethod', function (Closure $func, mixed ...$args) {
// do something before or early return
$result = $func(...$args); // call original or not
// do something after
return $result;
}); allowing full control over the func call:
then a proxy class won't be necessary and all limitations regarding inheritance will be gone \o/ |
I found two functions that closely match the expected behavior via
but none of them allow you to call the original function dynamically, so they're limited to some specific use cases only. |
@yceruto On this matter, you might want to take a look at the OpenTelemetry PHP Extension, which exposes a hook to wrap any calls, to trace it: https://github.com/open-telemetry/opentelemetry-php-instrumentation?tab=readme-ov-file#usage. Still, it does not allow you to call the original function but you receive the related object, its params in the pre callback and the return value in the post callback. |
thanks @gaelreyrol for the link, yes the thing is that if only one hook is allowed per function, we'll need full control over the func call so it can be wrapped into a set of interceptors with different targets (log, cache, transaction, etc.) having pre/post hooks only will cover some cases, but still limited (e.g. not possible to skip the function call for caching interceptor, or manipulate the arguments before the call or the return value after) |
we currently know exactly which methods will request interceptors, thanks to set_hook_func(Foo::class, 'barMethod', function (Closure $func, mixed ...$args) use ($interceptors) {
return $interceptors->call($func, $args);
}); that way, devs won't have to deal with this high-level function but can work directly with their own interceptor adapter. |
@ro0NL do you mean between the generated proxy and the targeted service? yes, if that's what you're referring to. Otherwise, no, it doesn't solve the whole problem as you need many interceptors for various methods and each method might be wrapped by multiple interceptors, and they shouldn't know about each other |
Uh oh!
There was an error while loading. Please reload this page.
This is a post-mortem research of #58076 to understand the outcomes, what worked, and what didn't, with the goal of finding actionable insights.
The topic focuses on AOP paradigm, in simple words: how to separate common extra features—like logging, caching, or handling transactions—from the code in your service methods. Let's picture what I mean exactly:
The What
(Logging method calls example)
Instead of doing this everywhere:
I want to do this:
acting
#[Log]
as a join point (marker), indicating that an aspect (interceptor) should wrap the method execution:Consider other practical examples like caching the execution of slow, resource-intensive methods or managing database transactions (e.g. using Doctrine
$em->wrapInTransaction()
). This way, it lets you add extra behavior that aren't crucial to the business logic without cluttering the core code.The How
#58076 proposed implementing this in a new component, but I now believe it should be part of the DI component instead, since the main issue here is "how to intercept the method call" when
MyBusinessService::execute()
is invoked.Initially, I proposed a
CallableWrapper
to wrap the method before it's called, like a Controller listener or Messenger middleware where a callable invocation is controlled. However, this approach would restrict the feature to controllers or message handlers instead of applying it universally.I'm now considering that an auto-generated proxy service is a better approach, yet it's more complex to implement. Suppose another service uses our
MyBusinessService
:imaging now that the DI component generates a proxy for
MyBusinessService
, like this:and it's the one injected into
OtherService
as a decorator of theMyBusinessService
service. If the concrete service is injected using an interface, the auto-generated proxy should be different but can still be achieved using the decoration pattern.Another possible solution -> #59730 (comment)
Known Limitations
This proxy approach will work if the method is called from another service (which is the most common case we aim to solve here). However, calling the method from another class excluded from the DI scope will not trigger any interceptor (as expected).
Are there any other limitations I might not be aware of?
Questions
The big questions are:
related issues: #47184, #57079, #58076
references for similar approach:
Cheers!
The text was updated successfully, but these errors were encountered: