A little context
I’ll place myself in the context of a Symfony application. We are a team of 8 developers: 6 dedicated backend Symfony developers and 2 frontend developers, including one who is a junior.
This shows that our team has a strong inclination towards backend development. The first thing not to do is to underestimate the frontend. Don’t think that frontend development is easy; on the contrary, it requires expertise - an expertise that our backend developers lack.
Our goal, therefore, will be to take as much weight off the shoulders of our frontend developers as possible, without forcing backend developers to step out of their comfort zone.
The application I’ll use for this example is as follows:
We have a set of radios, multiple pages with different radios, and if you click on any of them, then you can listen to a live stream of that radio.
Let’s start working!
So, we start by making the first page and everything goes well. But as we proceed to the next one, we encounter our first problem. Our frontend developers simply reused the code from the first page on the next. They immediately encounter this error:
This is an error we’re well acquainted with. We forgot to pass a variable to our Controller. Solving it is simple, but this error illustrates a deeper problem. Our frontend developer is dependent on our backend developers. Each time they make a new page, they have to ask the other developers to prepare a controller with the variables they need. We want to make our frontend independent. We aim to advance the project with as little friction between our backend and frontend devs as possible.
Component Architecture
For that, I propose that we cater to the frontend developers by using the component architecture. I won’t go back over what component architecture is, but I invite you to read my article on this topic if you haven’t already (https://dev.to/webmamba/how-to-integrate-component-architecture-into-symfony-4bjb).
So, we start by breaking down our page into many small components. Our list of radios will be a RadioList component composed of several Radio components. These Radio components are themselves made up of smaller components. To do all this, the frontend developer does not need us at all. They write anonymous component, component only made off a template:
RadioCard
<div class=" transition ease-in-out delay-150 hover:-translate-y-1 hover:scale-110 duration-300 aspect-square relative overflow-hidden rounded-lg bg-white/50 shadow-lg transition duration-300 ease-in-out hover:shadow-2xl p-4"
> {% block content %} {% endblock %}
</div>
RadioCardImage
{% props logo, name %} <div class="relative overflow-hidden border-slate-500/50 border rounded aspect-square"> <div> <img class="w-full h-full object-cover self-center" src="{{ logo }}" alt="{{ name }}" /> </div>
</div>
Radio
{% props radio %} <twig:RadioCard> <twig:RadioCardBackground logo="{{ radio.logo }}" radio="{{ radio.name }}" color="{{ radio.color }}"/> <twig:RadioCardImage logo="{{ radio.logo }}" name="{{ radio.name }}" /> <div class="absolute bottom-2 right-2"> <twig:PlayerButton/> </div>
</twig:RadioCard>
I repeat, to make this list of components, the frontend did not need to write any PHP. They do all this work with complete autonomy.
Backend dev it’s your time!
Once all these components are made to integrate our list of radios, the backend developers now only have to make a RadioList component, which has the following class:
<?php namespace App\Twig\Components; use App\Repository\RadioRepository;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; #[AsTwigComponent]
final class RadioList
{ public ?string $filter = null; public function __construct( private readonly RadioRepository $radioRepository ) {} public function getRadios(): array { if ($this->filter === null) { return $this->radioRepository->findAll(); } return $this->radioRepository->findByGenre($this->filter); }
}
But where it becomes really interesting is for the template, which will now only be this. All the work will have been pre-done by our frontend developers.
{% for radio in this.getRadios %} <twig:Radio :radio="radio"/>
{% endfor %}
This is great, we now have a working component able to communicate with our database, that anyone can reuse on every page. And all that while sticking to what we are good at, PHP code.
CVA
But we need to go a bit further. For example, for this site, we will need to set up an alert system. The informational alerts will be at the top left in blue, errors at the bottom in bold red, etc. We don’t want a component for each variation; we want modular components.
For this in Symfony, we can use CVAs (Component Variant Attributes). Here’s how it works:
{% props color, size, position %} {% set alert = cva({ base: 'min-w-96 absolute rounded-lg shadow-md bg-slate-200 text-white z-50', variants: { color: { 'blue': 'bg-blue-800 shadow-blue-800', 'red': 'bg-red-800 shadow-red-800', 'green': 'bg-green-800 shadow-green-800', }, size: { 'sm': 'p-2 mb-2 text-sm', 'lg': 'p-4 mb-4text-lg', }, position: { 'top-left': 'top-4 left-4', 'top-right': 'top-4 right-4', 'bottom-left': 'bottom-4 left-4', 'bottom-right': 'bottom-4 right-4', } }
}) %} <div class="{{ alert.apply({color, size, position})|tailwind_merge }}" role="alert"> {% block content %} {% endblock %}
</div>
We create our Alert component. At the start of this file, we’ll use the cva function. To this function, we’ll pass a key array, the first key of which will be ‘base’ and will take as its value all the CSS classes that will be present in all variations of our components. Next, we’ll define our variants. So here, variants on color, size, or position. Then, we’ll apply our cva to the div of our alert.
And just like that, we’ve created a component with a multitude of possible variations.
So, quite naturally, we’ve been able to create a set of components of different sizes, with different variations, from which our developers can draw to implement new features. We have the beginnings of a design system. Working in this way allows our front-end and back-end developers to work without stepping on each other’s toes, all while staying on a common codebase. Implementing a new feature is now picking between few components from our design system, we are dangerous!
Thank you for reading, see you soon for an article on how to document this design system!