Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork5.3k
[FrameworkBundle] Document how to decouple controllers from Symfony#21553
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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.
Already on GitHub?Sign in to your account
Uh oh!
There was an error while loading.Please reload this page.
Changes fromall commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading.Please reload this page.
Jump to
Uh oh!
There was an error while loading.Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -951,6 +951,79 @@ This way, browsers can start downloading the assets immediately; like the | ||
| ``sendEarlyHints()`` method also returns the ``Response`` object, which you | ||
| must use to create the full response sent from the controller action. | ||
| Decoupling Controllers from Symfony | ||
| ----------------------------------- | ||
| Extending the :ref:`AbstractController base class <the-base-controller-class-services>` | ||
| simplifies controller development and is **recommended for most applications**. | ||
| However, some advanced users prefer to fully decouple your controllers from Symfony | ||
| (for example, to improve testability or to follow a more framework-agnostic design) | ||
| Symfony provides tools to help you do that. | ||
| To decouple controllers, Symfony exposes all the helpers from ``AbstractController`` | ||
| through another class called :class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\ControllerHelper`, | ||
| where each helper is available as a public method:: | ||
| use Symfony\Bundle\FrameworkBundle\Controller\ControllerHelper; | ||
| use Symfony\Component\DependencyInjection\Attribute\AutowireMethodOf; | ||
| use Symfony\Component\HttpFoundation\Response; | ||
| 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]); | ||
| } | ||
| } | ||
| You can inject the entire ``ControllerHelper`` class if you prefer, but using the | ||
| :ref:`AutowireMethodOf <autowiring_closures>` attribute as in the previous example, | ||
| lets you inject only the exact helpers you need, making your code more efficient. | ||
| Since ``#[AutowireMethodOf]`` also works with interfaces, you can define interfaces | ||
| for these helper methods:: | ||
| interface RenderInterface | ||
| { | ||
| // this is the signature of the render() helper | ||
| public function __invoke(string $view, array $parameters = [], ?Response $response = null): Response; | ||
| } | ||
| Then, update your controller to use the interface instead of a closure:: | ||
| use Symfony\Bundle\FrameworkBundle\Controller\ControllerHelper; | ||
| use Symfony\Component\DependencyInjection\Attribute\AutowireMethodOf; | ||
| class MyController | ||
| { | ||
| public function __construct( | ||
| #[AutowireMethodOf(ControllerHelper::class)] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. I think it's still logically coupled to the container wiring hides construction details, but conceptually MyController still depends on that specific class being available and compatible. Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. ideally, this wiring config should be defined globally in your project to eliminate the code coupling issue and remove the need to specify There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. It's not coupled at all. You can run this code without having the DI component nor the ControllerHelper class. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. I don't believe it's only about running this code (which is absolute right), but also about writing this code so it minimizes the ripple effect (the number of places you must modify when changing behavior), that's the kind of coupling I'm referring to. If adding an adapter forces edits in other parts of the code, you still have coupling (one that is about DI config in your php code) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Yes, it works without the framework or the DI component. My concern isn't about those cases (they're fine with this setup) I’m focused on achieving proper decoupling while still using the framework, especially in the context of functional testing. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. Let's merge this PR then. Thanks! | ||
| private RenderInterface $render, | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. example: let's say I want to replace this render implementation by a custom one, so I create a dedicated adapter for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. you don't have to. you can wire any other stuff explicitly anywhere else - test case, DI config, etc Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others.Learn more. (still on the Symfony context) I might be missing something, but based on what I've tested so far, it doesn't seem possible to override this DI config (without using a CP) in a functional test context (the most common for controllers) to use a different adapter for Member
| ||
| ) { | ||
| } | ||
| // ... | ||
| } | ||
| Using interfaces like in the previous example provides full static analysis and | ||
| autocompletion benefits with no extra boilerplate code. | ||
| .. versionadded:: 7.4 | ||
| The ``ControllerHelper`` class was introduced in Symfony 7.4. | ||
| Final Thoughts | ||
| -------------- | ||