Symfony : why I stopped using the AbstractController
Tags: symfony, php
Published: 2023-02-23 13:23:51

---
title: Symfony : why I stopped using the AbstractController
published: true
date: 2023-01-24 09:35:48 UTC
tags: symfony,php
canonical_url: https://blog.roc-it.tech/symfony-why-i-stopped-using-the-abstractcontroller/
---

 ![Symfony : why I stopped using the AbstractController](https://images.unsplash.com/photo-1599507593548-0187ac4043c6?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxMTc3M3wwfDF8c2VhcmNofDN8fHBocHxlbnwwfHx8fDE2NzQ1NTQzNzk&ixlib=rb-4.0.3&q=80&w=2000)

A typical Controller in Symfony may look like this :

```php
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](https://symfony.com/doc/6.2/service_container/autowiring.html#autowiring-other-methods-e-g-setters-and-public-typed-properties))

So what are we using and how do I have access to it ?

Taking a look at the `AbstractController::render(...)` method [here](https://github.com/symfony/symfony/blob/6.2/src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php#L251) 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 <abbr title="as far as I know">AFAIK</abbr> is only used by controllers. Basically it is a "Service Locator" where the class defines itself which are its dependencies.

Besides `twig` there are (<abbr title="at the moment">ATM</abbr>) 9 additional dependencies declared this way which are ([source](https://github.com/symfony/symfony/blob/6.2/src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php#L85)) :

```php
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 <abbr title="in my opinion">IMO</abbr> 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

1. In general it refers to an Interface instead of an Abstract class
2. 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) :

```php
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 :

```diff
- '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](https://github.com/symfony/symfony/blob/6.2/src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php#L236))

```php
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](https://github.com/symfony/symfony/blob/6.2/src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php#L150))

```php
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 :

```diff
-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 :

```diff
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](https://github.com/symfony/symfony/blob/6.2/src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php#L259) 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.