Controllers in Symfony applications often extend the AbstractController base class
to use convenient shortcuts like render(), redirectToRoute(), addFlash(),
and more. Many projects do this because the base controller keeps things simple
and productive. But if you prefer to write framework-agnostic controllers,
Symfony 7.4 introduces a new feature that makes this easier than ever.
Introducing ControllerHelper
Symfony 7.4 introduces a new class called ControllerHelper. It exposes all
the helper methods from AbstractController as standalone public methods.
This allows you to use those helpers without extending the base controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
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 show(int $id): Response
{
if (!$id) {
return ($this->redirectToRoute)('product_list');
}
return ($this->render)('product/show.html.twig', ['product_id' => $id]);
}
}
Instead of inheriting from AbstractController, this example uses the
AutowireMethodOf attribute to inject only the specific helpers needed to render
templates and redirect responses. This gives you precise control, better testability,
and smaller dependency graphs.
Working with Interfaces
Closures are great, but Symfony 7.4 goes even further. You can use interfaces to describe helper method signatures and inject them directly. This gives you static analysis and autocompletion in your IDE, without adding any boilerplate:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
interface RenderInterface
{
// this must match the signature of the render() helper
public function __invoke(string $view, array $parameters = [], ?Response $response = null): Response;
}
class MyController
{
public function __construct(
#[AutowireMethodOf(ControllerHelper::class)]
private RenderInterface $render,
) {
}
// ...
}
Most applications should continue using AbstractController as usual. It's
simple, expressive, and requires no extra setup. The new ControllerHelper is
meant for advanced software design that favor decoupled code.
Note that while this post highlights ControllerHelper, the same strategy is
applicable e.g. to Doctrine repositories: instead of injecting the full repository service,
you can inject single query functions with the same #[AutowireMethodOf] attribute.
This is one I really struggle to understand. The closure option breaks static analysis, and overall it's harder to use (no editor autocompletion, having to use parentheses to encapsulate the variable). The second option requires me to define my own interfaces while needing to pay attention that signatures match exactly (and if they change, static analysis won't be able to detect it), while requiring to repeat
#[AutowireMethodOf(ControllerHelper::class)]everywhere in order to use it...So what's the point? In the example just either use
Twig\Environmentdirectly, or if for some reason you want to encapsulate some logic, define your own class/interface with class implementing it (you're already doing it in the second option) and in there you do whatever you want, so in the controller you can just inject that without having to bother with the attribute everywhere.I followed the PR on Github, and I was (still am) quite puzzled by the supposed benefits of this. There were a lot of valid issues raised in the discussion, but it was ended abruptly with a "thank you for the discussion, I'll merge this regardless of everything being said". It feels like a way to push for the use of attributes even where it doesn't quite make sense 🤷♂️
May I ask why this entry misses the usual links to the contributor and the PR?
@Massimiliano that was a mistake on my side. It's fixed now. Thanks!
@David this is providing an option to empower users who value decoupling and DI granularity over pure convenience. Defining you own interfaces is what DDD/clean arch recommends. If you start with matching the Symfony interfaces and they evolve, then you'll have the choice: either follow the lead - maybe the changes make sense for your app also - or define an adapter. Having this choice open is part of the benefits. Note that signatures are quite stable in Symfony so that concern is mostly an edge case. For static analysis, it should be possible to have the lint:container command check for potential mismatches. Help wanted to make it happen!
Great! Composition over inheritance is always better for code reuse. The solution with the interface looks great.