-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[FrameworkBundle] Split abstract Controller class into traits #16863
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
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. |
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)
…from `AbstractController` as a standalone service (nicolas-grekas) This PR was merged into the 7.4 branch. Discussion ---------- [FrameworkBundle] Add `ControllerHelper`; the helpers from `AbstractController` as a standalone service | Q | A | ------------- | --- | Branch? | 7.4 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | - | License | MIT This PR is a follow up of #16863 by `@derrabus` almost 10 years ago 😱, which was seeking for a solution to reuse helpers provided by `AbstractController` that'd be free from coupling by inheritance and that'd allow for more granular cherry-picking of the desired helpers. At the time, granular traits were proposed as the reusability unit. In this PR, I'm proposing to add a `ControllerHelper` class that'd allow achieving both goals using functions as the reusability unit instead. To achieve true decoupling and granular helper injection, one would have to use the `#[AutowireMethodOf]` attribute (see #54016). Here is the chain of thoughts and concepts that underpin the proposal. It should be noted that this reasoning should be read as an example that could be extended to any helper-like class, e.g it fits perfectly for cherry-picking query functions from entity repositories. So, here is the chain for controllers: 1. The Problem: The Monolithic Base Class Symfony's `AbstractController` offers a convenient set of helper methods for common controller tasks. However, by relying on inheritance, our controllers become tightly coupled to the framework. This can make them more difficult to test in isolation and provides them with a broad set of methods, even when only a few are needed. 2. The Initial Goal: Reusability without Inheritance The long-standing goal has been to decouple controllers from this base class while retaining easy access to its valuable helper methods. The ideal solution would allow for a more granular, "à la carte" selection of these helpers. 3. The Path Not Taken: Granular Traits The original proposal in PR #16863 explored the use of granular traits (e.g., `RenderTrait`, `RedirectTrait`). This was a step towards greater modularity, allowing a developer to use only the necessary functionalities. However, a trait-based approach has its own set of challenges: - Implicit Dependencies: The services required by the traits (like the templating engine or the router) are not explicitly declared as dependencies of the controller. - A Different Form of Coupling: While avoiding vertical inheritance, traits introduce a form of horizontal coupling. 4. A More Modern Approach: The Injectable Helper Service This PR introduces a `ControllerHelper` service. This class extends `AbstractController` to leverage its existing, battle-tested logic but exposes all of its protected methods as public ones. This aligns with modern dependency injection principles, where services are explicitly injected rather than inherited. A controller can now inject the `ControllerHelper` and access the helper methods through it. 5. The Final Step: True Granularity with `#[AutowireMethodOf]` While injecting the entire `ControllerHelper` is a significant improvement, it still provides the controller with access to all helper methods. The introduction of the `#[AutowireMethodOf]` attribute (see #54016) is the final piece of the puzzle, enabling the ultimate goal of using individual functions as the unit of reuse. With `#[AutowireMethodOf]`, we can inject just the specific helper method we need as a callable: ```php class MyController { public function __construct( #[AutowireMethodOf(ControllerHelper::class)] private \Closure $render, #[AutowireMethodOf(ControllerHelper::class)] private \Closure $redirectToRoute, ) { } public function showProduct(int $id): Response { if (!$id) { return ($this->redirectToRoute)('product_list'); } return ($this->render)('product/show.html.twig', ['product_id' => $id]); } } ``` This solution provides numerous benefits: - Maximum Decoupling: The controller has no direct dependency on `AbstractController` or even the `ControllerHelper` class in its methods. It only depends on the injected callables. - Explicit and Granular Dependencies: The controller's constructor clearly and precisely declares the exact functions it needs to operate. - Improved Testability (less relevant for controllers but quite nice for dependents of e.g. entity repositories): Mocking the injected callables in unit tests is straightforward and clean. 6. Bonus: Auto-generated Adapters for Functional Interfaces For even better type-safety and application-level contracts, `#[AutowireMethodOf]` can generate adapters for functional interfaces. One can define an interface within their application's domain to achieve better type-coverage without any new coupling: ```php interface RenderInterface { public function __invoke(string $view, array $parameters = [], ?Response $response = null): Response; } ``` Then update the previous controller example to use this interface: ```diff #[AutowireMethodOf(ControllerHelper::class)] - private \Closure $render, + private RenderInterface $render, ```` Symfony's DI container will automatically create an adapter that implements `RenderInterface` and whose `__invoke` method calls the `render` method of the `ControllerHelper`. This gives full static analysis and autocompletion benefits with zero extra boilerplate code. This pull request, therefore, not only provides a solution to a long-standing desire for more reusable controller logic but does so in a way that is modern, flexible, and fully embraces the power of Symfony's dependency injection container (while still preserving really good usability when the DIC is not used, as is the case when unit testing.) Commits ------- c2020bb [FrameworkBundle] Add `ControllerHelper`; the helpers from AbstractController as a standalone service
…from `AbstractController` as a standalone service (nicolas-grekas) This PR was merged into the 7.4 branch. Discussion ---------- [FrameworkBundle] Add `ControllerHelper`; the helpers from `AbstractController` as a standalone service | Q | A | ------------- | --- | Branch? | 7.4 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | - | License | MIT This PR is a follow up of symfony/symfony#16863 by `@derrabus` almost 10 years ago 😱, which was seeking for a solution to reuse helpers provided by `AbstractController` that'd be free from coupling by inheritance and that'd allow for more granular cherry-picking of the desired helpers. At the time, granular traits were proposed as the reusability unit. In this PR, I'm proposing to add a `ControllerHelper` class that'd allow achieving both goals using functions as the reusability unit instead. To achieve true decoupling and granular helper injection, one would have to use the `#[AutowireMethodOf]` attribute (see symfony/symfony#54016). Here is the chain of thoughts and concepts that underpin the proposal. It should be noted that this reasoning should be read as an example that could be extended to any helper-like class, e.g it fits perfectly for cherry-picking query functions from entity repositories. So, here is the chain for controllers: 1. The Problem: The Monolithic Base Class Symfony's `AbstractController` offers a convenient set of helper methods for common controller tasks. However, by relying on inheritance, our controllers become tightly coupled to the framework. This can make them more difficult to test in isolation and provides them with a broad set of methods, even when only a few are needed. 2. The Initial Goal: Reusability without Inheritance The long-standing goal has been to decouple controllers from this base class while retaining easy access to its valuable helper methods. The ideal solution would allow for a more granular, "à la carte" selection of these helpers. 3. The Path Not Taken: Granular Traits The original proposal in PR #16863 explored the use of granular traits (e.g., `RenderTrait`, `RedirectTrait`). This was a step towards greater modularity, allowing a developer to use only the necessary functionalities. However, a trait-based approach has its own set of challenges: - Implicit Dependencies: The services required by the traits (like the templating engine or the router) are not explicitly declared as dependencies of the controller. - A Different Form of Coupling: While avoiding vertical inheritance, traits introduce a form of horizontal coupling. 4. A More Modern Approach: The Injectable Helper Service This PR introduces a `ControllerHelper` service. This class extends `AbstractController` to leverage its existing, battle-tested logic but exposes all of its protected methods as public ones. This aligns with modern dependency injection principles, where services are explicitly injected rather than inherited. A controller can now inject the `ControllerHelper` and access the helper methods through it. 5. The Final Step: True Granularity with `#[AutowireMethodOf]` While injecting the entire `ControllerHelper` is a significant improvement, it still provides the controller with access to all helper methods. The introduction of the `#[AutowireMethodOf]` attribute (see #54016) is the final piece of the puzzle, enabling the ultimate goal of using individual functions as the unit of reuse. With `#[AutowireMethodOf]`, we can inject just the specific helper method we need as a callable: ```php class MyController { public function __construct( #[AutowireMethodOf(ControllerHelper::class)] private \Closure $render, #[AutowireMethodOf(ControllerHelper::class)] private \Closure $redirectToRoute, ) { } public function showProduct(int $id): Response { if (!$id) { return ($this->redirectToRoute)('product_list'); } return ($this->render)('product/show.html.twig', ['product_id' => $id]); } } ``` This solution provides numerous benefits: - Maximum Decoupling: The controller has no direct dependency on `AbstractController` or even the `ControllerHelper` class in its methods. It only depends on the injected callables. - Explicit and Granular Dependencies: The controller's constructor clearly and precisely declares the exact functions it needs to operate. - Improved Testability (less relevant for controllers but quite nice for dependents of e.g. entity repositories): Mocking the injected callables in unit tests is straightforward and clean. 6. Bonus: Auto-generated Adapters for Functional Interfaces For even better type-safety and application-level contracts, `#[AutowireMethodOf]` can generate adapters for functional interfaces. One can define an interface within their application's domain to achieve better type-coverage without any new coupling: ```php interface RenderInterface { public function __invoke(string $view, array $parameters = [], ?Response $response = null): Response; } ``` Then update the previous controller example to use this interface: ```diff #[AutowireMethodOf(ControllerHelper::class)] - private \Closure $render, + private RenderInterface $render, ```` Symfony's DI container will automatically create an adapter that implements `RenderInterface` and whose `__invoke` method calls the `render` method of the `ControllerHelper`. This gives full static analysis and autocompletion benefits with zero extra boilerplate code. This pull request, therefore, not only provides a solution to a long-standing desire for more reusable controller logic but does so in a way that is modern, flexible, and fully embraces the power of Symfony's dependency injection container (while still preserving really good usability when the DIC is not used, as is the case when unit testing.) Commits ------- c2020bb3a6c [FrameworkBundle] Add `ControllerHelper`; the helpers from AbstractController as a standalone service
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.