A typical Controller in Symfony may look like this :
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class HelloController extends AbstractController
{
#[Route(
path: '/hello/{name}',
name: 'app_hello',
requirements: ['name' => '[a-zA-Z-]+'],
methods: ['GET']
)]
public function index(string $name = 'Adrien'): Response
{
$form = $this->createForm(SomeFormType::class);
return $this->render('hello.html.twig', [
'name' => $name,
'form' => $form,
]);
}
}
It is perfectly fine of course but there are a few things we can improve.
What are the dependencies ?
By calling $this->render(...)
, it hides what are the dependencies. Indeed we did not provide anything through the usual ways : __construct
, setter
, properties
(See documentation)
So what are we using and how do I have access to it ?
Taking a look at the AbstractController::render(...)
method here we can see it is using the twig
service. But it is not declared as a dependency so how does it work ?
It is another feature called "Service Subscriber", which AFAIK is only used by controllers. Basically it is a "Service Locator" where the class defines itself which are its dependencies.
Besides twig
there are (ATM) 9 additional dependencies declared this way which are (source) :
public static function getSubscribedServices(): array
{
return [
'router' => '?'.RouterInterface::class,
'request_stack' => '?'.RequestStack::class,
'http_kernel' => '?'.HttpKernelInterface::class,
'serializer' => '?'.SerializerInterface::class,
'security.authorization_checker' => '?'.AuthorizationCheckerInterface::class,
'twig' => '?'.Environment::class,
'form.factory' => '?'.FormFactoryInterface::class,
'security.token_storage' => '?'.TokenStorageInterface::class,
'security.csrf.token_manager' => '?'.CsrfTokenManagerInterface::class,
'parameter_bag' => '?'.ContainerBagInterface::class,
];
}
It makes the dependencies very blurry IMO as we have to dig into the class to discover what is already accessible from the $this->container->get(...)
.
How does the Request $request
work ?
My argument here could seem a bit off, but I found that a lot of symfony developers doesn't know anymore that to be able to reference the Request
class within a controller method, you need your controller class to be tagged with controller.service_arguments
.
Part of why I think people don't know that is because by extending the AbstractController
+ using autoconfigure: true
it will automatically add it. I know it is supposed to help and it does. But if you understand that by leveling up your team on symfony core functionnalities, they will become more and more productive then they might miss this.
Same argument could be applied to any service that is "autoconfigured" but
- In general it refers to an Interface instead of an Abstract class
- It doesn't alter your class with unwanted / unneeded dependencies.
It hides what it really does
⚡️ Quizz : what changed in the render method in 6.2 ?
➡️➡️➡️ The way Form
is handled.
Here is the explanation : normally if you need to pass on a form from a controller to a template (twig), then you learned to do it this way (prior to 6.2) :
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class HelloController extends AbstractController
{
#[Route(
path: '/hello/{name}',
name: 'app_hello',
requirements: ['name' => '[a-zA-Z-]+'],
methods: ['GET']
)]
public function index(string $name = 'Adrien'): Response
{
$form = $this->createForm(SomeFormType::class);
return $this->render('hello.html.twig', [
'name' => $name,
'form' => $form->createView(),
]);
}
}
now you can change it like so :
- 'form' => $form->createView(),
+ 'form' => $form,
Easier right ? But is it because twig changed the way it handles forms ? Not at all ! Taking a look at this code (source)
foreach ($parameters as $k => $v) {
if ($v instanceof FormInterface) {
$parameters[$k] = $v->createView();
}
}
from the AbstractController
it clearly shows that it iterates over all the parameters you gave to do it for you. Meaning that if you go from this AbstractController
to a lighter one like I'll show later, then you have to change this yourself otherwise it won't work.
It may break your application
Maybe you already have built some simple HTTP API using return $this->json(...)
? But how does it work ? Take a look at the code (source)
protected function json(
mixed $data,
int $status = 200,
array $headers = [],
array $context = []
): JsonResponse {
if ($this->container->has('serializer')) {
$json = $this->container->get('serializer')->serialize($data, 'json', array_merge([
'json_encode_options' => JsonResponse::DEFAULT_ENCODING_OPTIONS,
], $context));
return new JsonResponse($json, $status, $headers, true);
}
return new JsonResponse($data, $status, $headers);
}
The issue is that if you didn't had the Serializer
component installed then the encoding of your data will be handled by the JsonResponse
class which does json_encode
. But at the moment you install the Seralizer
component then the behaviour changes and the output may differ as well.
What is the alternative ?
Here is what I propose :
-use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Component\Form\FormFactoryInterface;
+use Twig\Environment;
-class HelloController extends AbstractController
+class HelloController
{
+ public function __construct(
+ private readonly Environment $twig,
+ private readonly FormFactoryInterface $formFactory,
+ ) {
+ }
#[Route(
path: '/hello/{name}',
name: 'app_hello',
requirements: ['name' => '[a-zA-Z-]+'],
methods: ['GET']
)]
public function index(string $name = 'Adrien'): Response
{
- $form = $this->createForm(SomeFormType::class);
+ $form = $this->formFactory->create(SomeFormType::class);
- return $this->render('hello.html.twig', [
+ return $this->twig->render('hello.html.twig', [
'name' => $name,
- 'form' => $form,
+ 'form' => $form->createView(),
]);
}
}
and of course don't forget to update your services.yaml
configuration accordingly :
services:
_defaults:
autowire: true
autoconfigure: true
App\Controller\:
- resource: '../src/Controller/'
+ resource: '../src/Controller/**/*Controller.php'
+ tags: [{ name: 'controller.service_arguments' }]
I don't think this alternative is more complicated than what symfony propose. Of course this was a very simple case. I recommend to always have a look at the AbstractController
because there are also very useful tips like this one which sets the 422
status code if a form has been submitted but is not valid.
Conclusion
There is nothing wrong using the AbstractController
if you are building a very simple app that doesn't require much maintenance.
But if you are building something that requires improved readability (== improved maintenance), you should consider moving away from the AbstractController
to have explicit configurations.
Top comments (0)