-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[DI] Support generating interface-bound proxies #20656
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
To me this is sounds like a good idea. 👍 |
I'd love to see it 👍 Btw. shouldn't |
This should not be in the argument configuration, as this would mean that injecting the same service in 2 different places would inject different objects (depending on whether they are proxied or no), which can creates nasty behaviors (as the DIC currently enforces that getting a shared service always return the same instance). Regarding the implementation, I'm not sure we should get rid of ProxyManager. There are a bunch of nasty cases regarding proxy generation (handling code generation, handling all features of method signatures, etc) and I don't see any benefit reimplementing many part of the library while it is well-tested. |
@stof totally agreed, on both points made. |
I think proxy generation, interface bound or not, are far from trivial so I would prefer to keep everything in ProxyManager instead. This IMO is a missing feature from ProxyManager, not Symfony itself (although Symfony will need to change a bit its integration for this new feature). Regarding where to enable it, I think this should be done in the service definition, it's a peculiar way of registering a service, not injecting it. So I would go with: <service id="foo" class="Ns\FooClass" proxy="true" /> to keep the current behaviour, and if the user wants to, he can proxy against a specific interface instead: <service id="foo" class="Ns\FooClass" proxy="App\Interface" /> Maybe having a proxy for two interfaces is doable, so we could have: <service id="foo" class="Ns\FooClass" proxy="true">
<proxy>
<argument type="string">App\FooInterface</argument>
<argument type="string">App\BarInterface</argument>
<proxy>
</service> But I think we can re-discuss about that once this feature has landed in ProxyManager. |
As discussed at the SymfonyCon, it is true that an interface bound proxy is way simpler (approximate code): interface FooInterface
{
public function foo(): string;
}
class FooProxyClass implements FooInterface
{
private $container;
private $serviceId;
private $service;
public function foo(): string
{
if (null === $this->service) {
$this->service = $this->container->get($this->serviceId);
}
return $this->service->foo();
}
}
$proxy = Closure::bind(
function (ContainerInterface $container, string $serviceId) {
$this->container = $container;
$this->serviceId = $serviceId;
return $this;
},
new FooProxyClass(),
'FooProxyClass'
)($container, $serviceId); It might still be easier to have a dependency on Zend/Code to generate the proxy though. I'm still less convinced about the argument proxy and would prefer to split this RFC in two to treat them separately, but I'm +1 for the first one. There is also the question of if this could be handled in ProxyManager and be more generic (not coupled to Symfony DIC) via PSR-11. |
Because we decided to not have a hard requirement on ProxyManager (and I'm really fine with this), lazy services are second zone citizen right now. About your example proxy, it looks nice :) There is one missing case, which is fluid interfaces. $service = $this->container->get($id);
$result = $service->foo();
return $result === $service ? $this : $result; |
Of course, we should also deal with references (as arg and as return values). |
I guess this should also allow generating proxies based on abstract classes? |
Possibly, but I would add the support for it in a second iteration. |
I would like to support third comment/feature by @pamil. This feature can be desirable, at this moment, this feature would help us with our current project. Namely, we are using Go! Aop for handling cross cutting concerns (mainly as implementation of decorator). Now, Aop bundle allows us to inject symfony services in aspects, which makes it very powerful tool. However, we have noticed that service injected in aspect can not be, nor it can depend on, a service which class has been weaved with Aop engine. Not to get into too much explaining, it goes something like -> first service container is initialized, then aspect container, then aspects as services, then other services. If aspect requires service, that service is initialized prior to aspect, where we get into a circular dependency issue. There are two approaches now with which we can handle this situation:
We are using approach nb. 2. However, we are not found of that approach either, because, for vendor's services we have to do extra work in order to make that service lazy. We use a lot of aspects, they are registered and initialized with the application, but majority is not used during Request-Response lifecycle. Therefore, in terms of execution optimization, but mostly having in mind developer experience, ability to specify that injection of specific service should be lazy would increase our productivity. For us, solving cyclic dependency issue would be as simply as modifying from:
to:
and that would make our developer experience smooth. We do not have any objection to sacrifice some performances or arguable "good, by the book, practice" on account of our productivity and achieved developer experience. Thank you. |
Well, marking a service as proxy at the level of the injection breaks a rule of the current system: the injection of |
@stof Proxy is just a wrapper around of same service instance for shared service right? Each proxy would use same service instance - it would not be a same proxy, but underlying service would be the same, or I have misinterpreted proposed idea? |
On top of that, if the rule is: "if there is a shared service A that can be lazy loaded with proxy AA, there can be only one instance of service A and only one instance of its proxy AA" -> I don't think that that can be classified as hard problem to solve. As service container manages shared services, it can manage shared proxies, right? |
Well, the issue proposes 2 implementations. What I'm saying is that the first proposal fits well in the existing system (it is actually just a different way of implementing the existing proxy, by proxying only an interface rather than the whole class). The second one requires changing one of the invariants we have in the component (and requires adding complexity in the component for that) |
Just a DX note: it must be possible to disable proxying on the argument side. imagine a service user like this: class Builder {
private $service;
public function __construct(ServiceInterface $service) {
$this->service = $service;
}
public function method() {
if($this->service instanceof SomeConcreteImplementationThatAllowsUsageOptimizations) {
// efficient solution using special methods of conrete impl
return;
}
// the long way
}
} if the injected service is defined by a 3rd party, you must override the whole definition, which in the worst case requires a custom compiler pass (if the service is dynamically created). edit: i know there are other ways around this, like adding a 2nd optional injection against the concrete service, but i bet they all have their drawbacks |
Not really a big fan of that @backbone87 tbh. If you don't want the service to be proxied it's simpler to either override the service definition or alter it via a Compiler pass. If it's just for that specific case, then maybe we could indeed provide a |
I'm probably a bit naïve there, I've just red this, and came with one idea which allows to "opt out" the proxy, avoid name collision (the proxy having the same name than the service), and would work nicely with autowiring : Assuming
Also, the service Ns\FooInterface could either implement ONLY Ns\FooInterface, or every interface of the Ns\FooClass. One other way of configuring this kind of behaviour would be that |
See #27697 for an implementation (not everything discussed/proposed, but still the main idea.) |
…ith "lazy: Some\ProxifiedInterface" (nicolas-grekas) This PR was merged into the 4.2-dev branch. Discussion ---------- [ProxyManagerBridge][DI] allow proxifying interfaces with "lazy: Some\ProxifiedInterface" | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #20656 | License | MIT | Doc PR | - By adding `<tag name="proxy" interface="Some\ProxifiedInterface" />` to your service definitions, this PR allows generating interface-based proxies. This would allow two things: - generating lazy proxies for final classes - wrapping a service into such proxy to forbid using the methods that are on a specific implementation but not on some interface - the proxy acting as a safe guard. The generated proxies are always lazy, so that lazy=true/false makes no difference. As a shortcut, you can also use `lazy: Some\ProxifiedInterface` to do the same (yaml example this time.) Commits ------- 1d9f1d1 [ProxyManagerBridge][DI] allow proxifying interfaces with "lazy: Some\ProxifiedInterface"
… lazy services (tucksaun) This PR was merged into the 4.4 branch. Discussion ---------- [DependencyInjection] Document proxifying interfaces for lazy services Symfony 4.2 introduced the possibility of lazy load services using final classes by proxyfying specific interfaces (symfony/symfony#20656), but this has not been documented yet. Targeting 4.4 because 4.4 is the oldest maintained branch, but should I target 4.2 as this was introduced in 4.2? Fix #10295 Commits ------- f86b976 [DI] Document proxifying interfaces for lazy services
May it be useful to make the container able to generate interface-bound proxies?
I see 3 benefits to do so:
There are two places where this could be enabled, depending on who should have this responsibility:
in the proxyfied-service definition
Assuming
Ns\FooClass implements Ns\FooInterface
, this could be eg. in XML:so that:
lazyness could be activated when the "lazy" attribute is enabled, or be always enabled.
"proxy" attribute being true, all interfaces implemented by the service class would be proxyfied.
We could also imagine a
proxy
tag that explicitly tells which interfaces should be proxyfied amongst all the implemented one.in the dependency injecting definition
In XML, this could be eg.:
so that inside bar:
lazyness could be activated when a "lazy" attribute is enabled, or be always enabled.
In that case, I don't think we should allow proxying several interface at once.
WDYT? (ping @theofidry, maybe you'd like to give it a try?)
The text was updated successfully, but these errors were encountered: