Thanks to visit codestin.com
Credit goes to github.com

Skip to content

DI child containers #29075

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

Closed
Bilge opened this issue Nov 3, 2018 · 14 comments
Closed

DI child containers #29075

Bilge opened this issue Nov 3, 2018 · 14 comments

Comments

@Bilge
Copy link
Contributor

Bilge commented Nov 3, 2018

Symfony supports the concept of just a single DI container for the entire application. If the configuration file becomes too big it can be split into separate files with includes, but it does not support isolating separate groups of services in separate containers. This is true despite the fact that the documentation explains how to create containers from scratch with configuration files.

It may be desirable in some applications to provide sub-containers that contain services of just one type for scenarios where container injection is desirable or required, but one does not want to inject the main container with all services. This is already possible by defining a ContainerInterface service that loads a custom configuration file as shown in the aforementioned documentation section. However, even though only services defined in the sub-container can be retrieved from that container, some of the services in the sub-container may have dependencies on services in the main container. For this use-case, it is important to establish a parent/child relationship between the sub-container and the main Symfony container in which the child container defers to the parent for dependencies it cannot fulfil.

Zend Framework already supports separate containers for different groupings of services, such as controllers and forms, but it's a little less trivial in Symfony due to the compiler steps. We cannot simply override get() to defer to the parent container for missing service definitions because compilation will fail before we can ever make a get() call. The compiler must be made aware of the parent container and either copy in or link to the definitions it needs from the parent according to the dependency graph.

@jvasseur
Copy link
Contributor

jvasseur commented Nov 3, 2018

You can already have a sub-container that has access to only a subset of the main container with service locators.

It's not exactly a child container since it only proxies a subset of services of the main container (but it can make publicly available services that are private in the main container), but I believe it can handle all use cases for child containers.

@Bilge
Copy link
Contributor Author

Bilge commented Nov 4, 2018

@jvasseur Whilst that is interesting, it's very limited because each service has to be defined one-by-one in the service locator. That is highly restrictive compared to creating a real DI container that can be configured with glob resources to mass include all services matching a certain file pattern.

@jvasseur
Copy link
Contributor

jvasseur commented Nov 4, 2018

@Bilge you can use glob resources or defaults in a specific file to add a tag to your services that you want to be in the sub-container then use a compile pass to collect all services with that tag and create a service locator with all those services. This is what is done in the framework for form types for example: https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Form/DependencyInjection/FormPass.php#L63-L84.

@Bilge
Copy link
Contributor Author

Bilge commented Nov 4, 2018

Thanks @jvasseur. I had presumed I would need to do as much but the code example will surely be helpful. I am not exactly sure which bits I need to replicate, however. It seems that code is probably doing a lot more than I would need to do. Do I just need to copy definitions from the container into the service locator? Documentation on this topic is quite scarce.

@Tobion
Copy link
Contributor

Tobion commented Nov 4, 2018

ServiceLocatorTagPass::register($container, $servicesMap); returns the new service locator. You can then inject that locator to the services that need it. This is what is done with $definition->replaceArgument(0, $this->processFormTypes($container)); in the example above

@nicolas-grekas
Copy link
Member

nicolas-grekas commented Nov 4, 2018

There's also another way to create several containers: multi-kernel apps. That's basically what a kernel is: a scope for a container. Many apps use these since years.

@Bilge
Copy link
Contributor Author

Bilge commented Nov 4, 2018

@nicolas-grekas That doesn't seem like a very intuitive solution, and poses more questions than it answers. Specifically, how would one get services in one kernel that depend on services in another kernel to work together and without being optimized away by the compiler?

@jakzal
Copy link
Contributor

jakzal commented Nov 5, 2018

It may be desirable in some applications to provide sub-containers that contain services of just one type for scenarios where container injection is desirable or required, but one does not want to inject the main container with all services.

I'm still missing a use case here. Could you think of a real life scenario where you'd need a "sub-container"? I thought service locators cover our bases.

@Bilge
Copy link
Contributor Author

Bilge commented Nov 5, 2018

@jakzal To my understanding, a service locator is a container with a subset of the parent container's services (that's not its standard definition, but is an intended use-case of service locators within the Symfony framework). Therefore, a service locator is a sub-container, and seems to be perfectly suited to my purposes.

I'll try implementing it as described in this ticket tomorrow. However, even if it does work, it's worth noting that this use-case is pretty poorly documented at present. It does not seem easy or intuitive to find and use ServiceLocatorTagPass::register, were my attention not guided by the experts herein.

@Bilge
Copy link
Contributor Author

Bilge commented Nov 7, 2018

I was able to implement this successfully and didn't even need ServiceLocatorTagPass::register. Since all I wanted to do was create a Service Locator and populate it with a list of services matching a glob file pattern, I created the usual config to define the glob services and the Service Locator. The only difficulty was adding the glob services to the Service Locator, since this cannot be done with just config.

By tagging the glob services and adding a compiler pass to augment the Service Locator's constructor injection definition, together with $container->findTaggedServiceIds(), this was all I needed to do to satisfy the sub-container use case.

It's unfortunate this cannot be done entirely in config, however, as it increases the complexity somewhat. Perhaps in future there might be support for referencing tagged services in the config.

Thanks to all participants for your help and support. 👏

@Bilge Bilge closed this as completed Nov 7, 2018
@nicolas-grekas
Copy link
Member

@Bilge if you're up for a doc PR, many will be able to follow your lead here :)

@Bilge
Copy link
Contributor Author

Bilge commented Nov 8, 2018

@nicolas-grekas That would be interesting. Upon re-reading the doc, I now see there is a mention of ServiceLocatorTagPass::register. But not only did I not end up using it, and not only does it not really explain why you need to use it, but it seems this statement is patently misleading:

and will automatically share identical locators amongst all the services referencing them

When I read that, I started to worry that not using the static register() method would recreate the service locator every time it's referenced. But a simple KernelTest would seem to indicate that's not the case. I've since deleted it but it looked something like this:

$a = self::$container->get('my_locator');
$b = self::$container->get('my_locator');

self::assertSame($a, $b);

Since this test passed I assumed I do not need to use register() to have the same Service Locator shared between its dependents.

Notwithstanding I don't know the RST format, like I said at the beginning, it's interesting because it seems you originally wrote that piece of documentation but I can't see how it would segue into my implementation because it seems diametrically opposed to what you're currently recommending to people wanting to implement Service Locators!


Edit: After reading the register() code more carefully, it seems this creates the entire Service Locator definition and adds it to the Container for you. More to the point, it seems this is to support a purely compiler-based definition of the Service Locator and its services, which would even require you to use the reference it returns to programmatically alter the definitions of those services that wish to receive the SL. This is in direct contrast to my approach, because I define as much as possible in config and just do the service injection in the compiler pass, allowing you to define and reference the Service Locator and its services in config like normal.

I've only been using Symfony for a few weeks now and I don't fully understand the impact or drawbacks of my approach. Specifically, I notice one key difference is that you "memoize" all the service references in your implementation by wrapping them in ServiceClosureArgument. I don't know what that does or why it's needed, but I noticed that not all of my Service Locators seem to have working lazy-load implementations. In my app, I define two SLs with two sets of tagged resources in exactly the same way, and one of them has a list of closures (lazy-loaded) services in its factories list and the other has concrete instances and I am unable to figure out why, but I'm surely doing something wrong! I don't mind trying to write some documentation, but it would be like the blind leading the blind.

@Bilge Bilge reopened this Nov 8, 2018
@nicolas-grekas
Copy link
Member

nicolas-grekas commented Nov 15, 2018

I'm not sure to understand all the details here so feel free to share some code/example app.
About ServiceLocatorTagPass::register(), what it does is dedup same locator definitions (it's about dedup at compile time - not runtime)

@nicolas-grekas
Copy link
Member

I'm closing here as this overlaps with #25707 and #28992 + not sure how actionable this is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants