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 π
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 )
- Report any error/typo.
- Report something that could be improved.
- Like and retweet!
- Follow me on Twitter Follow me on Twitter
- Subscribe to the RSS feed.
- Click on the More on Stackoverflow buttons to make me win "Announcer" badges π .
Thank you for reading! And see you soon on Strangebuzz! π
[π¬π§] 3rd blog post of the year: Β«A better ADR pattern for your Symfony controllersΒ» https://t.co/K1I7DNW27b Proofreading, comments, likes, and retweets are welcome! π Annual goal: 3/6 #symfony #php #adr #rad #MicroSymfony
— COil β (@C0il) November 4, 2024