-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[FrameworkBundle] Add new "controller.service_arguments" tag to inject services into actions #21771
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
Conversation
2554653
to
5e84861
Compare
@nicolas-grekas made some excellent points regarding DX. I will not use it myself, but it's beneficial when it comes to lazy loading, which can fully replace the container usage, which I highly encourage!
I think that adding the ability to "inject" services into the action arguments, will add a layer of magic that makes it more complex in combination with autowiring. |
7438115
to
d1889dd
Compare
8579093
to
d33e043
Compare
Now with explicit wiring: And with tests. @iltar: except when there is only one action (e.g. That's why people inject the container instead in fact (and/or extend the base controller really). By injecting services into actions, we get laziness, and we ask people for writing a type hint instead of calling The alternatives we have that provide laziness are getter injection - and service subscribers. But these won't provide the same DX here (getter injection is nice when shipped via a trait, see #18193). All in all, this will provide a seamless DX to me, without making people write "bad" code anymore. |
da56af9
to
15f56e1
Compare
Will this make it possible to do something like /**
* @Route(service="...")
*/
class FooController {} |
@nicolas-grekas I'm not against it and your argument makes sense! |
I need to look at the actual implementation in more detail, but what is described here sounds pretty close to what I was hoping for in this implementation. I also like this bit:
What would I want to do if I wanted to have all actions mapped automatically and also map a specific service? Would it work like this?
... or even:
How would
All-in-all, I think I like this approach. I know you had some technical reasons why it might not be the best way to go. Were there any things you've done in this PR that you wish you could have done differently? Tentative 👍 from me for now. :) Super excited about this direction! |
15f56e1
to
e2be25e
Compare
Yes! You'd just need one more thing to have the DX you envision: enable autowiring on the definitions that have these tags. The "routing.controller" does not enable autowiring automatically (that'd be coupling), but it plays well with autowired definitions (that's composition).
That one would be redundant, only the second tag is needed.
As you know, I wondered the same. Thinking about this, it doesn't make much sense for
They vanished now that we have #21770, which serves as the infrastructure that this PR leverages. I'm really happy with the PR as is :) |
That makes me happy. :) I'm a bit lost on this:
Does this mean that if autowiring is defined for the controller in question then autowiring is used but if not then it isn't?
What about closures? Or is that something we don't need to worry about supporting in core? For example, Silex-style closures:
This might be well out of scope so I'm not going to worry about this too much. Mostly just curious if this would somehow be covered? |
yep
All of this is bound to Symfony's DIC (almost all the logic is in a compiler pass), so Silex would need its own wiring system. |
d94a2ab
to
2b6e005
Compare
PR updated. Everything now moved to When the tag is set, all public methods are now considered for generating a service-locator based argument-resolver. All public methods are considered for generating a service-locator: in fact, methods that are listed to be called at instantiation time (ie the one returned by So, we now have two passes:
There is a possibility that this may generate some unused service-locators, bound to public methods that are not used as actions. Yet, that should be rare and that'll be just dead code with no practical downside, especially compared to the huge +-side of not having to configure anything.
this PR provides nothing new on this topic: today also, if you add a bogus argument to an action (eg a bad type hint for a doctrine entity with the proper argument resolver configured), then the controller fails when it is called without earlier notice. There is no chance to make anything better here, at least that's not what this PR is about - entities or services or whatever.
valid concern, fixed! see description above.
not in my perfect world! I certainly do not want to have services come in when I did not ask for!
As stated, that's certainly not a problem to me, quite the contrary.
I really like that approach, thanks for pushing it to us. @weaverryan, I think I addressed all your concerns :) |
2b6e005
to
7beb45c
Compare
Instead of that. Why not promote one Action per class. Then everything works with constructor autowiring which is already in core |
7beb45c
to
0218537
Compare
"service" attribute renamed to "id" for consistency with existing practice. |
Awesome! So this new approach is very interesting - it removes several of the issues I was having earlier! Before we dive further into the details, the implementation makes me realize something: from a purely technical perspective, this feature could work without the controllers being services. If I told you that I wanted service auto-completion on my framework:
controllers_service_arguments:
AppBundle\Controller: true
# .. and you could probably have config to control specific args... I'm not proposing this is a superior syntax! It just strikes me as a bit unnatural that we're requiring the controllers to be services... when we don't really need that! In fact, one of my original comments was this:
You said you didn't do anything to address this... but you did! Thanks to this line in if ($id === $class) {
$controllers[$id.'::'.$r->name] = new Reference($argsId);
} It is now possible to register a controller as a service via the PSR-4 loader, but then refer to it using the non-service syntax. Yes: as long as your register the controller as a service with the correct tag, you are then allowed to use it NOT as a service, but enjoy the service argument injection. I can imagine this conversation:
But in reality, Person 1 is still not actually using their controller as a service. This whole situation is what made me realize that the requirement that the controllers be services is unnatural... simply because the feature itself doesn't require it! Even if everyone was creating all controllers as services, I still think the implementation shouldn't rely on this unrelated thing. So:
What are your thoughts about this? And thanks :) |
0218537
to
7a56616
Compare
7a56616
to
9afcc63
Compare
…t services into actions
9afcc63
to
9c6e672
Compare
Thank you @nicolas-grekas. |
…s" tag to inject services into actions (nicolas-grekas) This PR was merged into the 3.3-dev branch. Discussion ---------- [FrameworkBundle] Add new "controller.service_arguments" tag to inject services into actions | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | (no test yet) | Fixed tickets | - | License | MIT | Doc PR | - Talking with @simensen and @weaverryan, we wondered if we could leverage the `ArgumentResolver` mechanism to make it inject services on demand, using e.g. autowiring. ```php class PostController { public function indexAction(Request $request, PostRepository $postRepository) { // PostRepository comes from the container $postRepository->findAll(); // ... } } ``` This PR achieves that, using a new "controller.service_arguments" tag. Typically: ```yaml services: AppBundle\Controller\PostController: autowire: true tags: - name: controller.service_arguments ``` It also supports with explicit wiring (thus doesn't necessarily require autowiring if you don't want to use it): ```yaml services: AppBundle\Controller\PostController: tags: - name: controller.service_arguments action: fooAction argument: logger id: my_logger ``` ~~The attached diff is bigger than strictly required for now, until #21770 is merged.~~ Todo: - [x] rebase on top of #21770 when merged - [x] add tests - [x] add cleaning pass to remove empty service locators Commits ------- 9c6e672 [FrameworkBundle] Add new "controller.service_arguments" tag to inject services into actions
Happy the PR is merged, let's play with it now :) @weaverryan to answer your comment, having left some time to think about it:
The PR just needs a service locator that returns something for any string value of the "_controller" routing param. So yes this can work with, any such "_controller", not only those registered as services. I think it would be possible to implement this "controllers_service_arguments" config key yes. I just don't see the need to fight against having controllers-as-services, thus don't see the need to seek for any other configuration mean.
I don't see anything wrong with P2's answer. We need a "Px" that will need to answer something to P1, because P1 needs to opt-in. Being via service registration or another mean doesn't matter. I really look forward the RX (reviewer experience) where I don't need do find |
Just tested it on a project... and that works! It's deceptively simple and I will activate it in the default configuration for apps using Flex. Can't wait to share that with everyone. |
Docs issue added! symfony/symfony-docs#7672 |
…ing ControllerTrait (nicolas-grekas) This PR was merged into the 3.3-dev branch. Discussion ---------- [FrameworkBundle] Introduce AbstractController, replacing ControllerTrait | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no (master only) | Deprecations? | yes | Tests pass? | yes | Fixed tickets | - | License | MIT | Doc PR | - Basically reverts and replaces #18193. Instead of using getter injection to provide our controller helpers, let's leverage the new `ServiceSubscriberInterface` (see #21708). This is what the proposed `AbstractController` class provides. So, instead of extending `Controller`, this would encourage extending `AbstractController`. This provides almost the same experience, but makes the container private, thus not usable by userland (this safeguard was already provided by `ControllerTrait`). I did not deprecate `Controller`, but I think we should. Now that we also have "controller.service_arguments" (see #21771), we have everything in place to encourage *not* using the container in controllers directly anymore. My target in doing so is removing getter injection, which won't have any use case in core anymore. The wiring for this could be: ```yaml services: _instanceof: Symfony\Bundle\FrameworkBundle\Controller\AbstractController: calls: [ [ setContainer, [ '@container' ] ] ] tags: [ container.service_subscriber ] ```` But this is optional, because we don't really need to inject a scoped service locator: injecting the real container is fine also, since everything is private. And this is done automatically on ControllerResolver. Commits ------- a93f059 [FrameworkBundle] Introduce AbstractController, replacing ControllerTrait
|
||
public function process(ContainerBuilder $container) | ||
{ | ||
if (false === $container->hasDefinition($this->resolverServiceId)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not the other way around? If some tags are found then register the resolver.
Because currently the resolver is asked to resolve before DefaultValueResolver even if there is no controller tagged.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd say because "less dynamic" is better. The extra check you're talking about is not a perf issue: it is really fast - and happens once per action (you don't call that many actions for constructing one response, do you?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and happens once per action
Isn't it once per argument of an action, and sub-action(s)? Although I'm fine with it.
In symfony4, will you remove |
Talking with @simensen and @weaverryan, we wondered if we could leverage the
ArgumentResolver
mechanism to make it inject services on demand, using e.g. autowiring.This PR achieves that, using a new "controller.service_arguments" tag. Typically:
It also supports with explicit wiring (thus doesn't necessarily require autowiring if you don't want to use it):
The attached diff is bigger than strictly required for now, until #21770 is merged.Todo: