Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions controller.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Copy link
Member

Choose a reason for hiding this comment

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

I think it's still logically coupled to ControllerHelper since the controller explicitly requests a method reference from it AutowireMethodOf(ControllerHelper::class)

the container wiring hides construction details, but conceptually MyController still depends on that specific class being available and compatible.

Copy link
Member

@yceruto yceruto Nov 3, 2025

Choose a reason for hiding this comment

The 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 AutowireMethodOf in every controller.

Copy link
Member

Choose a reason for hiding this comment

The 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.

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 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)

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

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

Let's merge this PR then. Thanks!

private RenderInterface $render,
Copy link
Member

@yceruto yceruto Nov 3, 2025

Choose a reason for hiding this comment

The 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 RenderInterface, but I would still need to remove/change the #[AutowireMethodOf(ControllerHelper::class)] line to make it work properly, isn't it? In this case, my code is not fully decoupled.

Copy link
Member

Choose a reason for hiding this comment

The 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
of course, if you want Symfony to autowire for you, you need to change the declaration.
But that's still not coupling: you can run the code without.

Copy link
Member

@yceruto yceruto Nov 3, 2025

Choose a reason for hiding this comment

The 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 RenderInterface in test env, since the DI attribute takes precedence over any external DI config.

Copy link
Member

@nicolas-grekas nicolas-grekas Nov 3, 2025

Choose a reason for hiding this comment

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

override this DI config in a functional test context

it must be possible - autowiring attributes are ignored when autowiring is diabled but also when a specific argument is explicitly configured.

) {
}

// ...
}

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
--------------

Expand Down