-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
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
[FrameworkBundle] Split abstract Controller class into traits #16863
Conversation
Alternative approach http://www.whitewashing.de/2013/06/27/extending_symfony2__controller_utilities.html |
Making the controller a trait is what I suggested in #12595 (comment) But maybe we should also split the traits by topic like |
There is another approach I want to discuss since several time but I did not have the time to blog about it. It's a derivate of/similar to the ADR pattern, applied to Symfony.
In fact, it's already doable to use such system with Symfony. We do it in API Platform master. Here is one action for instance: https://github.com/dunglas/DunglasApiBundle/blob/master/Action/PostCollectionAction.php In this implementation I also rely a lof on kernel events to be able to plug and play some sort of logic (persistence, validation...) in a decoupled manner but it's out of scope. |
@Tobion Splitting the trait is what I've suggested as a next step in my initial post. It absolutely makes sense to set the good old @dunglas I like your idea, but I wouldn't see it as a direct alternative to this approach. Maybe, we should discuss it in a different ticket. The idea behind the |
@dunglas I really like the idea of having a single action per class, but registering them as services will immensely increase the container size and services list. Or this is no real problem? |
@sstok It needs to be benchmarked but even if you have 200 controllers in your app I'm not sure it will make a big difference from a performance point of view. |
In my opinion, this proposal hurts DX (developer experience) very seriously for Symfony newcomers. Implementing the interface and using a trait looks confusing and splitting the trait into "microtraits" looks overkill to me. |
@javiereguiluz I take this concern very serious, because DX (especially for unexperienced developers) is imho the most important reason, why we have that First of all, splitting the trait would not hurt DX here, since we would still have the At the moment, we still need the A trait cannot add an interface to a class, so we need to add it manually at the moment. Maybe we can find a better way than relying on the interface to autowire the container to the controller? |
1a245bb
to
ff7cfe8
Compare
Why not keeping the |
@Ener-Getick My intention was not to create too many ways of doing the same thing. But maybe both, the |
👍 for both way |
And what do you think about renaming the trait to something which would assume there are sub traits like |
I think this PR is a very good idea, turning many things into traits is good because I see many newcomers in PHP and almost all of them don't know anything about traits (they're more focused on the "awesome short-array syntax like in JS" than the awesome features like traits, variadic functions or generators). It sounds very DX to me so I agree 👍 @dunglas You made me discover the ADR pattern and even if it looks very good (and much better than MVC to me), I don't know if we could use it for Symfony 3. Maybe for Symfony 4 actually, but it's not for now (and has to be discussed by the core team). Still, @derrabus In your PR you should change this :
Because it's a deprecation actually :) |
@Pierstoval Done, thanks. :-) |
I'm sceptical on this. Traits are mixins, which are supposed to provide implementation of additional interfaces, e.g. |
@Pierstoval You can already use it even in Symfony 2 (I think it is even possible in the 2.0.0 release):
Btw, this listener is the way SensioFrameworkExtraBundle and FOSRestBundle are already handling this (the View class of FOSRestBundle is similar to the concept defined in ADR) |
@stof Thanks a lot for the enlightenment! |
What are the advantages of this approach?
Actually, I don't see any reason why it _shouldn't_ be a class. Could someone explain why trait is a better choice for this kind of functionality? |
@tommiv a class can have multiple traits but only one parent. So a controller can extend |
@Ener-Getick is this really necessary? Yes, if you inherit |
@tommiv I'm in favor of having a class (implementing the |
Because the question was raised why we should favor traits over an abstract base class: The Controller class does not define a type. It even does not define a public interface (apart from the container-awareness which is not a functionality specific to the controller). It does not contain any real functionality either. It is more or less a collection of glue code snippets making some components easier to use. In fact, you do not have to extend Using type inheritance for code reuse is bad in gerneral. It violates the Liskov substitution principle and in languages with single inheritance, it locks you into a fixed inheritance chain. If you have more than one piece of code that you want to reuse, you will create either a huge Adam class for everything or deep and complicated inheritance trees. The alternative is class composition, but this strategy creates the need to copy&paste boilerplates of glue code like the code we find inside the Before traits, php did not have a way to avoid duplication of glue code other than inhertitance. With traits being available, we have a way more lightweight mechanism of distributing glue code. And we can build smaller reusable packages instead of one big class. The removal of the Why do we have that It is helpful for rapid prototyping. If I don't care much about the quality of the code because I'm going to throw it away anyway, this class enables me to get the job done very quickly. As @javiereguiluz noted, the class lowers the entrance barrier for beginners. Someone who has never seen a Symfony application could easily start to build one. Just extend the And of course, you don't need to know what a trait is and what that |
@derrabus Thanks for the explanation. It really makes sense now. |
This is what I'd like to do to go on with this PR:
If, after splitting it, we still need a |
@derrabus I see your point. It makes sense, but I think we should go in a little different way:
|
@kozlice I really like the good idea of splitting traits, but when not extending the default controller, it would "force" the user to implement/use a |
@kozlice I don't disagree with you about the first and second bullet point of your list. But I don't see the point in introducing a marker interface. Right now, you can use any plain php class as a controller. Changing that would be a BC break without any benefit. |
104634b
to
fd1566a
Compare
… support (dunglas) This PR was merged into the 3.2-dev branch. Discussion ---------- [DependencyInjection] Autowiring: add setter injection support | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | n/a | License | MIT | Doc PR | todo Add support for setter injection in the Dependency Injection Component. Setter injection should be avoided when possible. However, there is some legitimate use cases for it. This PR follows a proposal of @weaverryan to ease using [DunglasActionBundle](https://github.com/dunglas/DunglasActionBundle) with #16863. The basic idea is to include a setter for the required service in the trait and let the DependencyInjection Component autowire the dependency using the setter. This way, a newcomer can use the trait without having to create or modify the constructor of the action. /cc @derrabus Commits ------- a0d7cbe [DependencyInjection] Autowiring: add setter injection support
What needs to be done here to move this forward? I can help if you need. |
@TomasVotruba I can always rebase the PR against the current master, if there is interest in this feature. Continuously keeping it mergable is painful because the But right now, I don't feel like the core team is supporting my idea. |
@derrabus Thanks for update. I have the same feelings. What a pity. Maybe writing article about this issue could spread the idea. |
So what about As right now, we have tens of controllers injecting the same services over and over again (plus additional deps). Making me seriously considering switching back to the controller base class, only for convenience. Or adding the utils class it in user-land. |
@ro0NL I like to have my own
It does not make sense to me to have one service that get injected all the container to implements some services shortcuts. We "tolerate" it from the controller because it ease starting to develop Symfony application, a simple app can be just a config file and some controller actions, in such a case it's "convenient" to get any service you need from there being "container aware". |
Not my approach :) class ControllerUtils {
public function __construct(UrlGeneratorInterface $urlGenerator/**, etc. */) { }
public function redirectToRoute() { }
// etc.
}
The service definition is provided by the framework (compare it to the |
Actually, we should not put the different parameters in the constructor, but in This could be good also to have a specific |
Not sure i follow.. but i propose a very low-level solution use SF\ControllerUtils;
use My\Dep;
class MyController {
public function __construct(Dep $dep, ControllerUtils $controllerUtils) {}
// profit
} services:
my_controller:
class: MyController
arguments: ['@my_dep', '@controller.utils'] |
This could be a simple abstract definition. services:
my_controller:
class: MyController
parent: controller.utils
calls:
- ['setMyDep', ['@my_dep']] |
This is precisely what I was thinking about, and using |
Yeah but the down side is that makes classes definition mutable... This kind of setters should return the old value IMO, to be consistent and able to retrieve their original state, like handlers do. |
I favor a solution that doesnt require setter-injection, nor a base class, nor a trait. |
I would like to try this feature in practice before merging, since it is great change that may go wrong. So I've implemented these traits for Controllers in bundle:
Feel free to try it and write your experience and what was good and bad. It might help to discuss this PR in more real-world way. |
Personally, I like the ControllerTrait, but I'm skeptical about the smaller traits. |
Have you tried it in practice? What are the results? |
Closing in favor of #18193 |
…t (dunglas) This PR was squashed before being merged into the 3.3-dev branch (closes #18193). Discussion ---------- [FrameworkBundle] Introduce autowirable ControllerTrait | Q | A | | --- | --- | | Branch | master | | Bug fix? | no | | New feature? | yes | | BC breaks? | no | | Deprecations? | no | | Tests pass? | yes | | Fixed tickets | #16863 | | License | MIT | | Doc PR | todo | This is the missing part of the new controller system and it's fully BC with the old one. Used together with the existing autowiring system, #17608 and [DunglasActionBundle](https://github.com/dunglas/DunglasActionBundle) it permits to inject explicit dependencies in controllers with 0 line of config. It's a great DX improvement for Symfony. It also has a lot of another advantages including enabling to reuse controller accros frameworks and make them easier to test. See https://dunglas.fr/2016/01/dunglasactionbundle-symfony-controllers-redesigned/ for all arguments. Magic methods of old controllers are still available if you use this new trait in actions. For instance, the [`BlogController::newAction`](https://github.com/symfony/symfony-demo/blob/master/src/AppBundle/Controller/Admin/BlogController.php#L70) form the `symfony-demo` can now looks like: ``` php namespace AppBundle\Action\Admin; use AppBundle\Form\PostType; use AppBundle\Utils\Slugger; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Bundle\FrameworkBundle\Controller\ControllerTrait; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Form\Extension\Core\Type\SubmitType; class NewAction { use ControllerTrait; private $slugger; public function __construct(Slugger $slugger) { $this->slugger = $slugger; } /** * @route("/new", name="admin_post_new") */ public function __invoke(Request $request) { $post = new Post(); $post->setAuthorEmail($this->getUser()->getEmail()); $form = $this->createForm(PostType::class, $post)->add('saveAndCreateNew', SubmitType::class); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $post->setSlug($this->slugger->slugify($post->getTitle())); $entityManager = $this->getDoctrine()->getManager(); $entityManager->persist($post); $entityManager->flush(); $this->addFlash('success', 'post.created_successfully'); if ($form->get('saveAndCreateNew')->isClicked()) { return $this->redirectToRoute('admin_post_new'); } return $this->redirectToRoute('admin_post_index'); } return $this->render('admin/blog/new.html.twig', array( 'post' => $post, 'form' => $form->createView(), )); } } ``` As you can see, there is no need to register the `slugger` service in `services.yml` anymore and the dependency is explicitly injected. In fact the container is not injected in controllers anymore. Convenience methods still work if the `ControllerTrait` is used (of course it's not mandatory). Here I've made the choice to use an invokable class but this is 100% optional, a class can still contain several actions if wanted. Annotations like `@Route` still work too. The old `abstract` controller isn't deprecated. There is no valid reason to deprecate it IMO. People liking using the "old" way still can. Unless in #16863, there is only one trait. This trait/class is basically a bunch of proxy methods to help newcomers. If you want to use only some methods, or want explicit dependencies (better), just inject the service you need in the constructor and don't use the trait. I'll create open a PR on the standard edition soon to include ActionBundle and provide a dev version of the standard edition to be able to play with this new system. I'll also backport tests added to the old controller test in 2.3+. **Edit:** It now uses getter injection to benefit from lazy service loading by default. Commits ------- 1f2521e [FrameworkBundle] Introduce autowirable ControllerTrait
in order to respect ADR pattern see http://pmjones.io/adr/ see symfony/symfony#16863 (comment)
in order to respect ADR pattern see http://pmjones.io/adr/ see symfony/symfony#16863 (comment)
The Idea
This PR came to my mind during the SymfonyCon hack day. Following the
ContainerAware
class that has been transformed into a trait, I'd like to do the same with FrameworkBundle'sController
class. As far as I can tell, there is no reason anymore whyController
has to be a class (see this comment on why I would favor a trait over an abstract class here).The current
Controller
class covers many different topics. It contains utility methods for security, rendering, routing and more. Before traits, it was not possible to separate those topics. Now, we could split the class into multiple focused traits. This would increase reusability and enable usage outside of a controller context, for instance inside an event listener.Like it is currently done in the
Controller
class, the traits won't contain real functionality. Instead, they will provide shortcut functions to ease the integration of various Symfony components (for instance Routing and HttpFoundation in the examples below).Examples
The following example shows an event listener using the
redirectToRoute
method that can be found in theController
class.In this example, we use the method inside a controller that is registered as a service and therefore should not depend on the container.
BC Breaks
The traits will introduce protected fields and methods to manage their dependencies. If a class extending
Controller
already contains methods and properties with the same name, this change might introduce conflicts.