Skip to content

[DependencyInjection] Make it easy to get a service from its FQCN #18300

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
wants to merge 1 commit into from

Conversation

GuilhemN
Copy link
Contributor

Q A
Branch? master
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets #18268
License MIT
Doc PR

Alternative approach to #18268

This PR would allow people to use a new syntax to get their services by class name:

$container->getOfType(MyClass::class); // MyClass service, throws an exception if several services
$container->getAllOfType(Command::class); // All commands registered

This is particularly useful internally (I updated the AutowiringPass to show an example). We can also imagine automatically determine the service corresponding to a controller.
This could also be useful in third party bundles which could, for example, determine easily if a service already exists for a class (this is needed in DunglasActionBundle).

/cc @hason @Nicofuma @dunglas

@GuilhemN GuilhemN changed the title [DependencyInjection] Add new methods on the Container to get a service from its FQCN [WIP][DependencyInjection] Add new methods on the Container to get a service from its FQCN Mar 24, 2016
@GuilhemN GuilhemN changed the title [WIP][DependencyInjection] Add new methods on the Container to get a service from its FQCN [WIP][DependencyInjection] Add new methods to the Container to get a service from its FQCN Mar 24, 2016
*
* @return array the type map
*/
public function populateAvailableTypes()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public in order to optimize the classes using this feature (populate the types only once).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we make $this->typeMap null by default (it actually is already) and then in getServiceIdsOfType, call this private function if null === $this->typeMap. Basically, this class should be able to manage the fact that all the types should be populated once, at the beginning of getServiceIdsOfType.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not exactly true, this method has to be called each time the class of a definition is updated.
What do you think of the last commit using \SplObserver and \SplSubject to remove this need ?

@linaori
Copy link
Contributor

linaori commented Mar 25, 2016

We can also imagine automatically determine the service corresponding to a controller.

This is one of the cases where I actually think this would be very beneficial. The SensioFrameworkBundle could then add support for @Controller() which would simply check for ->has(get_class($controller)). This would remove the "tight coupling" between class and service.

Would that means the "class" key can be omitted if class_exists($serviceName)?

services:
    Foo\Bar: ~
    Bar\Baz: ~
/** 
 * @Controller() 
 */
class Foo\Bar
{
    public function __construct(Bar\Baz $baz) {}
}

Note that my wish is to rename the @Route in the extra bundle to @Controller, not possible yet.

@GuilhemN
Copy link
Contributor Author

@iltar actually, the call would be ->getServicesIdOfType(get_class($controller)) (except if we had a new method hasOfType ?)

And I like your idea but we have to investigate a bit more to see if it wouldn't create BC breaks/weird behaviour.

@GuilhemN
Copy link
Contributor Author

anyone else review about this ? I'd like to know if this would be accepted or not before finalizing it (or if you prefer #18268).

@dunglas
Copy link
Member

dunglas commented Mar 25, 2016

I think it's a nice improvement.

@xabbuh
Copy link
Member

xabbuh commented Mar 26, 2016

Adding these methods to the ContainerBuilder class might be useful to make some compiler passes easier to implement. However, I am not convinced that we should add them to the Container class too. I guess that this would make people tend to use the container in a way they shouldn't do.

@GuilhemN
Copy link
Contributor Author

@xabbuh this is useful to find services implementing a class at runtime, especially for controllers.

@linaori
Copy link
Contributor

linaori commented Mar 26, 2016

@Ener-Getick and what if you make it like this:

services:
    Foo\Bar: ~
# same as, except that the key is the class name in the first example
services:
    foo.bar:
        class: Foo\Bar

That means you can do $container->get(Bar::class); which should be validated compile time.

@GuilhemN
Copy link
Contributor Author

@iltar this is not the issue solved by this PR, maybe I wasn't clear in its description.
The goal here is to create a link between services and classes.
For example if you have these services:

services:
    app.foo:
        class: Foo

    app.bar:
        class: Bar # extends Foo

You can use this:

$container->getServiceIdsOfType(Foo::class);
// will return ['app.foo', 'app.bar']

$container->getServiceIdsOfType(Bar::class);
// will return ['app.bar']

IMO this should not be used in a traditional app but it is useful for compiler passes or for classes needing to be very flexible (like the ControllerResolver).

@GuilhemN
Copy link
Contributor Author

@xabbuh IMO we should keep at least getServiceIdsOfType in the Container, I agree that the other methods could introduce bad practices...

@linaori
Copy link
Contributor

linaori commented Mar 26, 2016

Regarding the Controller Resolver, I'm working on deprecating the current method by introducing a factory (similar to the argument resolver).


foreach ($reflectionClass->getInterfaces() as $reflectionInterface) {
$this->set($reflectionInterface->name, $id);
$this->autowiringTypes[$type] = $id;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use the autowiring types in this feature ? Maybe should it be renamed then ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I know what you mean?

@GuilhemN
Copy link
Contributor Author

@xabbuh I removed the new methods of the Container for now ; I think we'll see later if we really need them by testing this feature.

@javiereguiluz
Copy link
Member

I'm 👎 about this feature. It adds a new way to do what already can be done and I don't feel the new way is better.

First, as explained by @Ener-Getick, this will throw exceptions in some cases because services don't have a one-to-one correspondence to classes (e.g. Doctrine repositories defined as services share the same class).

Second, this is more complicated to explain. E.g.

Before: if you want to get the main Doctrine service to access the different entity managers, just get doctrine service:

$this->get('doctrine')->...

After:

use Symfony\Bridge\Doctrine\RegistryInterface;

$this->get(RegistryInterface::class)->...

What's a registry? Why the interface? Why it doesn't include the Doctrine word anywhere? Why the ::class when I want to get a service? How can I discover that to get doctrine I need to use RegistryInterface?

@fabpot
Copy link
Member

fabpot commented Mar 29, 2016

I have the same feeling as @Ener-Getick. Since the landing of auto-wiring, I'm not comfortable with all the features that are proposed, they are quite far from the overall philosophy of the DI component, which has always favored explicitness.

I'm also 👎

@GuilhemN
Copy link
Contributor Author

@javiereguiluz @fabpot and what do you think about the proposition of @xabbuh which is mainly meant to be used internally (only available at compilation)?
This is in fact only a move of the logic from the AutowirePass to the ContainerBuilder.

An example of how to use it:

$container->register('foo', 'Foo');
$container->register('bar', 'Bar'); // subclass of Foo

$container->getServiceIdsOfType('Foo');
// will return ['foo', 'bar'] 

$container->getServiceIdsOfType('Bar');
// will return ['foo'] 

It would only be possible to get the service ids of a certain type that way not their instance.

*
* @return string[]
*/
public function getServiceIdsOfType($class, $reload = true)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see a use for the $reload parameter here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The controller definitions can be changed at any time so the type map has to be reloaded each time we use it.
This parameter is here to prevent this reload in case we are sure it is useless.

@GuilhemN GuilhemN force-pushed the FQCN branch 3 times, most recently from 0ed266c to 019ea01 Compare March 29, 2016 17:39
@GuilhemN
Copy link
Contributor Author

The last commit uses \SplObserver and \SplSubject but it seems to me to be too much complex for the initial goal...
Maybe it would be easier/better to only create an utility class as initially proposed by @dunglas.

$serviceIds = $this->container->getServiceIdsOfType($typeHint->name);
if (1 === count($serviceIds)) {
$value = new Reference($serviceIds[0]);
} elseif (0 === count($serviceIds)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead you could use empty($serviceIds).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@GuilhemN GuilhemN changed the title [WIP][DependencyInjection] Add new methods to the Container to get a service from its FQCN [WIP][DependencyInjection] Make it easy to get a service from its FQCN Mar 30, 2016
@GuilhemN
Copy link
Contributor Author

After thinking, I created the utility class @dunglas proposed. This is much easier to maintain and to understand as we don't have to worry about definitions changes.
I doesn't like really much its name (I called it ServiceTypeHelper), maybe someone has a better idea?

@GuilhemN GuilhemN changed the title [WIP][DependencyInjection] Make it easy to get a service from its FQCN [DependencyInjection] Make it easy to get a service from its FQCN Mar 30, 2016
@hason
Copy link
Contributor

hason commented Mar 30, 2016

@Ener-Getick The instance of ServiceTypeHelper could be shared. I want to use it in #18268 (or in a library based on #18268).

@GuilhemN
Copy link
Contributor Author

@hason it's complex to share it as we have to reload the map each time a definition changes which is complicated to know (you can see my first try).
This is much easier to create the map each time we need it (and could be optimized with a static variable registering all the types corresponding to a class).

@linaori
Copy link
Contributor

linaori commented Apr 5, 2016

Something else that might be useful with something like this, is resolving the validator that belongs to a constraint. Right now you have to add a service id, but this could in theory be easy to resolve based on the class name, similar to form types.

@GuilhemN
Copy link
Contributor Author

GuilhemN commented Apr 5, 2016

@iltar only if we allow this to be used at runtime (the current state is a class only available when compiling).

@GuilhemN
Copy link
Contributor Author

Closing for now as it needs changes to work with the last changes.

@GuilhemN GuilhemN closed this May 19, 2016
@TomasVotruba
Copy link
Contributor

This would be really great, as it's autowiring by types is already present in the core.

@GuilhemN
Copy link
Contributor Author

GuilhemN commented Sep 22, 2016

@TomasVotruba i agree but that's a big task and we have several ways to do it that all have benefits and disadvantages. I don't think i'll have the time to take a look at it again soon but maybe in a month or two i'll be able to reopen a pr :)

@TomasVotruba
Copy link
Contributor

TomasVotruba commented Sep 22, 2016

@Ener-Getick Thank you for your persistence and work on this 👍. It's very difficult to make a mind-shift from named services to typed services.

I'm starting something similar via external bundles. So it's optional and easy to integrate and test proof of concept:

@GuilhemN
Copy link
Contributor Author

For you first example, you can take a look at DunglasActionBundle which does more or less the same thing but also applied to other part of the framework (commands, event subscribers, etc), the other one looks a bit redundant with the other one (and a bit more hacky).
Anyway, having a map translating classes to services name would be awesome in this context :)

@TomasVotruba
Copy link
Contributor

TomasVotruba commented Sep 22, 2016

I know that bundle, but it does too much. I really need only the autowiring on controllers. Also didn't work on first use.

The ActionAutowire is less clean, yet on half way to constructor injection. Rather see explanation here http://www.tomasvotruba.cz/blog/2016/09/19/how-to-get-more-than-request-in-controller-action/

@hason
Copy link
Contributor

hason commented Sep 30, 2016

@TomasVotruba @Ener-Getick You can use https://github.com/symfonette/class-named-services library.

@GuilhemN
Copy link
Contributor Author

@hason this pr is for compile time whereas yours is for runtime, so that's not the same kind of use :)

@TomasVotruba
Copy link
Contributor

@GuilhemN Btw, do you have your utility class somewhere available as package? Would love to try it.

@GuilhemN
Copy link
Contributor Author

@TomasVotruba no, but you can extract it from this PR if you want.

@TomasVotruba
Copy link
Contributor

@GuilhemN Sure, just prefer reusable open-source code.

@GuilhemN
Copy link
Contributor Author

@TomasVotruba i understand, i'll reopen this pr for 3.3 when 3.2 will be released.

@GuilhemN
Copy link
Contributor Author

see #20940

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

Successfully merging this pull request may close these issues.