A better ADR pattern for your Symfony controllers

Published on 2024-11-01 • Modified on 2024-11-01

This post shows various experiments and attempts around the ADR pattern applied to Symfony controllers. Let's go! 😎

Prerequisite

I assume you have at least a basic knowledge of Symfony and know what a controller is.

Configuration

  • PHP 8.4
  • Symfony 7.1

Introduction

In the #923 AWOS blog post, there was a blog post by Damien Alexandre from JoliCode: A Good Naming Convention for Routes, Controllers and Templates?". I have always been interested in best practices and conventions, so it kept my attention. I let you read the whole article, but one of the main topics is:

Can we use the controller FQCN as the route name? πŸ€”

Goal

This blog post has several interesting ideas, but let's test this on an actual Symfony project. Why not go further?
MicroSymfony is an open-source Symfony application template I have developed, and it already uses the ADR pattern; it is the perfect candidate to test this.

The ADR pattern

ADR stands for Β« Action Domain Responder Β»; in Symfony it can be implemented as invokable controllers. As explained in the documentation:

Controllers can also define a single action using the __invoke() method, which is a common practice when following the ADR pattern (Action-Domain-Responder).

Here is the snippet extracted from the documentation:

// src/Controller/Hello.php
namespace App\Controller;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

#[Route('/hello/{name}', name: 'hello')]
class Hello
{
    public function __invoke(string $name = 'World'): Response
    {
        return new Response(sprintf('Hello %s!', $name));
    }
}

Can't we improve this? πŸ€”

POC: use the controller FQCN as the route name

In the documentation, we see that the route is a simple string. It is hardcoded. Is there a convention with this string? We can put whatever we want. As the application grows, if there are many developers, each may use a different convention. How to avoid this?

When using the ADR pattern, each action is encapsulated in a controller class, and there is a unique method: the __invoke() one. That means the controller identifies the action, and its FQCN identifies each controller! Why not use the FQCN self::class as the route name? That's the point of the JoliCode article. The previous snippet becomes:

// src/Controller/Hello.php
namespace App\Controller;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

#[Route('/hello/{name}', name: self::class)]
class Hello
{
    public function __invoke(string $name = 'World'): Response
    {
        return new Response(sprintf('Hello %s!', $name));
    }
}

Let's check the route with the debug:router command:

$ bin/console debug:router "App\Controller\Hello"
+--------------+---------------------------------------------------------+
| Property     | Value                                                   |
+--------------+---------------------------------------------------------+
| Route Name   | App\Controller\Hello                                    |
| Path         | /hello/{name}                                           |
| Path Regex   | {^/hello(?:/(?P<name>[^/]++))?$}sDu                  |
| Host         | ANY                                                     |
| Host Regex   |                                                         |
| Scheme       | ANY                                                     |
| Method       | ANY                                                     |
| Requirements | NO CUSTOM                                               |
| Class        | Symfony\Component\Routing\Route                         |
| Defaults     | _controller: App\Controller\Hello()                     |
|              | name: World                                             |
| Options      | compiler_class: Symfony\Component\Routing\RouteCompiler |
|              | utf8: true                                              |
+--------------+---------------------------------------------------------+

As expected, the action's route name now takes the FQCN of its controller πŸŽ‰. So yes, we can use the FQCN as the route name. We avoid putting a hardcoded string and we can apply the convention on the other ADR actions.

Note that if you don't put a name for the route, Symfony automatically generates one as explained in this blog post . In this case, the route name would be app_hello__invoke. This feature was introduced in Symfony 6.4.

Ok, it works, but can we do further with this approach? Can't we use self::class in other places?

POC: use the controller FQCN as the Twig template path and name

When creating a controller, we can extend the Symfony AbstractController, which provides several useful functions. It provides the render() function to render a response from a Twig template. Let's switch on the MicroSymfony project and its HomeAction.
We now use the controller FQCN to render the Twig template:

declare(strict_types=1);

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\HttpKernel\Attribute\Cache;
use Symfony\Component\Routing\Attribute\Route;

/**
 * @see StaticActionTest
 */
#[AsController]
#[Cache(maxage: 3600, public: true)]
final class HomeAction extends AbstractController
{
    /**
     * Simple page with some content.
     */
    #[Route(path: '/', name: self::class)]
    public function __invoke(): Response
    {
        $readme = (string) file_get_contents(__DIR__.'/../../README.md');

        return $this->render(self::class.'.html.twig', ['readme' => $readme]);
    }
}

It works, but we have to modify the template path in the project; it is now:

templates
    App
        Controller
            HomeAction.html.twig

Note that there is some magic as the FQCN is App\Controller\HomeAction while the generated path is App/Controller/HomeAction. Symfony automatically converts anti-slashes into slashes because the path is normalized.

Can we do better? If we look at the controller code, there are remaining hardcoded strings. Can you spot them?

POC: use the controller FQCN as the route path

Let's have a look at another action, the one displaying the composer.json file of the project:

#[Route(path: '/composer', name: self::class)]
public function __invoke(): Response
{
    $composer = (string) file_get_contents(__DIR__.'/../../composer.json');

    return $this->render(self::class.'.html.twig', ['composer' => $composer]);
}

In the #Route attribute of the __invoke() function, let's remove the hardcoded string /composer of the path: value by the FQCN class. The code becomes:

#[Route(path: self::class, name: self::class)]
public function __invoke(): Response
{
    $composer = (string) file_get_contents(__DIR__.'/../../composer.json');

    return $this->render(self::class.'.html.twig', ['composer' => $composer]);
}

Again, yes, we can do it! The URL of the page becomes:

https://127.0.0.1:8001/App\Controller\ComposerAction

The URL is ugly 😱 but it works! Should we use this? Not on a public website because SEO is important. But for an internal website or tool, why not? What is nice is that you know where the code is located when developing, just with the URL.

Test it!

You can test this in less than one minute; just type (composer & the Symfony binary are required):

composer create-project strangebuzz/microsymfony && cd microsymfony && make start && open https://127.0.0.1:8000

Don't hesitate to create a PR if you spot errors or something that could be improved. πŸ™

Conclusion

I use this new pattern in the MicroSymfony project (except for the route path part), and I introduced a new Twig helper path(ctrl_fqcn('HomeAction')) to generate URLs without having to write the full controllers' FQCN in the Twig templates
(which is quite ugly because you have to use App\\Controller\\ComposerAction 😱).
Let's try this and see if it works well on larger projects. At least, we know that it is possible. WDYT? πŸ™‚

That's it! I hope you like it. Check out the links below to have additional information related to the post. As always, feedback, likes and retweets are welcome. (see the box below) See you! COil. 😊

  GitHub   Read the doc   Read the doc  More on the web

They gave feedback and helped me to fix errors and typos in this article; many thanks to BernardNgandu πŸ‘

  Work with me!


Call to action

Did you like this post? You can help me back in several ways: (use the Tweet on the right to comment or to contact me )

Thank you for reading! And see you soon on Strangebuzz! πŸ˜‰

COil