Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: article blog #1

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
304 changes: 304 additions & 0 deletions blog-article.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
# Modernisez vos applications Symfony avec Tailwind et Twig Components

Symfony est depuis longtemps un incontournable pour développer des applications web PHP. Mais qu’en est-il du frontend ? Grâce à Tailwind CSS et aux Twig Components, vous pouvez transformer vos interfaces en un rien de temps tout en restant organisé et maintenable. Dans cet article, découvrez comment combiner ces trois outils pour créer des applications Symfony à la fois modernes et élégantes.

## Prérequis

Une application Symfony >= 6.3 et Twig >= 3.0. Voir la documentation officielle : [Installing & Setting up the Symfony Framework (Symfony Docs)](https://symfony.com/doc/current/setup.html).
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pour les prérequis pas totalement certain des versions minimum
je vais devoir verifier ca

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ça me semble cohérent en tout cas mais oui vaut mieux double check


Dans cet article, nous utiliserons une app Symfony 7.1 avec Symfony AssetMapper. Pas besoin d'installer Webpack Encore (bien que totalement compatible), ce qui simplifie l'installation.

## Installation

```bash
composer require symfony/asset-mapper symfony/twig-bundle symfony/ux-twig-component
```

Ceci installe les dépendances nécessaires pour utiliser les Twig Components.

Ensuite, il faut installer Tailwind CSS :

```bash
composer require symfonycasts/tailwind-bundle tales-from-a-dev/twig-tailwind-extra
php bin/console tailwind:init
```

Le fichier `tailwind.config.js` sera créé à la racine de votre projet. Une bonne configuration de Tailwind est essentielle pour une utilisation optimale.

### Conseil sur les conventions de nommage

Dans notre équipe, lors d'une refonte graphique avec des maquettes Figma, nous n’avions pas défini de convention claire pour les variables de style. Résultat : des classes en `Camel-Case` (`bg-Grey-Light`), peu adaptées à Tailwind. Après discussion, nous avons opté pour une convention en `kebab-case` (`bg-grey-light`), respectant les conventions Tailwind et facilitant la maintenance.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

quelle équipe ? La notion d'équipe n'a pas été introduite, tu ne présentes pas un projet client ici

Aussi, je ne comprends pas pourquoi figma est évoqué, pourquoi cet outil mérite-t-il sa place ici ? Il est lié à votre problématique de nommage ?


## Utilisation

Incluez le CSS de Tailwind dans le template de base :

```twig
{# templates/base.html.twig #}
{% block stylesheets %}
<link rel="stylesheet" href="{{ asset('styles/app.css') }}">
{% endblock %}
```

En développement, utilisez :

```bash
php bin/console tailwind:build --watch
```

Plus d'infos dans la [documentation officielle du bundle Tailwind.](https://symfony.com/bundles/TailwindBundle/current/index.html)

```twig
{# templates/default.html.twig #}
{% extends 'base.html.twig' %}
{% block body %}
<div class="container">
<h1 class="text-3xl font-bold">Accueil</h1>
<p class="text-gray-500">Bienvenue chez KNPlabs !</p>
</div>
{% endblock %}
```

## Configuration avancée

Tailwind est extrêmement flexible. Vous pouvez personnaliser les couleurs, polices, tailles, etc., dans le fichier `tailwind.config.js`. Par exemple, définissez la classe `container` pour centrer le contenu et ajouter du padding différents selon les tailles d’écran :

```js
theme: {
container: {
center: true,
padding: {
DEFAULT: '1rem',
lg: '2rem',
},
},
},
```

Ajoutez des couleurs personnalisées, raduis, etc. dans la clé `extend` :
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

radius


```js
extend: {
colors: {
background: "#ffffff",
foreground: "#020817",
primary: {
DEFAULT: "#0f172a",
foreground: "#f8fafc",
},
secondary: {
DEFAULT: "#f1f5f9",
foreground: "#0f172a",
},
},
borderRadius: {
lg: "0.6rem",
md: "0.4rem",
},
},
```

Vous pouvez retrouver une configuration plus complète dans notre [repo de démonstration](https://github.com/KnpLabs/symfony-tailwind-twig-components/blob/main/tailwind.config.js).

## Twig Components

Twig Components, un composant Symfony, permet de créer des composants réutilisables dans vos templates Twig. Ces composants peuvent être :

- Anonymes (Anonymous Twig Component) : fichiers Twig simples dédiés à l’UI.
- Avec logique métier (Live Twig Component) : classes PHP pour des composants plus complexes, avec un template Twig associé.

### Anonymous Twig Component et approche CVA

Avec Twig Components, vous pouvez gérer les variantes de style CSS grâce à **CVA** (*Class Variant Authority*).

Le CVA centralise la gestion des classes CSS conditionnelles, rendant vos composants modulaires et faciles à maintenir. Initialement popularisé dans le monde JavaScript, il est désormais disponible dans Twig !

Voici un exemple de création d’un bouton avec variantes :

```twig
{# templates/components/button.html.twig #}

{% props as = 'button', variant = 'default', size = 'default' %}

{% set buttonVariants = cva({
base: 'inline-flex items-center justify-center gap-2 w-fit whitespace-nowrap rounded-md text-sm font-medium transition-colors',
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
},
size: {
default: 'h-10 px-4 py-2',
lg: 'h-12 rounded-md px-8',
}
},
defaultVariants: {
variant: 'default',
size: 'default',
}
}) %}

<{{ as }} {{ attributes.without('class') }}
class="{{ buttonVariants.apply({variant, size}, attributes.render('class'))|tailwind_merge }}"
>
{% block content '' %}
</{{ as }}>
```

Voici notre composant bouton qui accepte trois props : `as`, `variant`, et `size`. Cette approche de composant dite "polymorphic" permet de rendre par défaut le composant comme une balise `button`, mais vous pouvez le changer en `a`, `div`, ou autre en passant la prop `as`.

#### Usage

```twig
<twig:button as="a" href="https://knplabs.com" size="lg">Découvrir KNPlabs</twig:button>
```

#### Rendu HTML

```html
<a href="https://knplabs.com" class="inline-flex items-center justify-center gap-2 w-fit whitespace-nowrap rounded-md text-sm font-medium transition-colors bg-primary text-primary-foreground hover:bg-primary/90 h-12 px-8">
Découvrir KNPlabs
</a>
```

*Retrouvez un autre cas d'utilisation pour les headings sur notre [repo de démonstration](https://github.com/KnpLabs/symfony-tailwind-twig-components/blob/main/templates/components/heading.html.twig)*

##### Bonus: Tailwind Merge

Le filtre `tailwind_merge` permet de fusionner les classes CSS de manière intelligente. Par exemple, si vous surchager votre composant avec d'autre classes Tailwind en doublon ou en conflict, `tailwind_merge` les fusionnera et appliquera le style surchager en dernier. Cela évite l'utilisation pas très propre du `!` devant vos classes Tailwind.

```twig
<twig:button as="a" href="https://knplabs.com" size="lg" class="bg-red-500">Rouge KNPlabs</twig:button>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l'exemple n'a pas l'air d'illustré le filtre en question ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

J’ai un style par défaut avec une classe bg-primary, et ici je surcharge avec ma classe bg-red-500. Dans le résultat final, la classe bg-primary n’apparaît plus, donc les conflits entre les classes ont bien été résolus. Peut-être que l’exemple n’est pas assez explicite, ou peut-être même inutile pour cet article, je ne sais pas trop ? 🤷‍♂️

```

Rendu HTML:

```html
<a href="https://knplabs.com" class="inline-flex items-center justify-center gap-2 w-fit whitespace-nowrap rounded-md text-sm font-medium transition-colors h-12 px-8 bg-red-500">
Rouge KNPlabs
</a>
```

### Live Twig Components

Comme je le disais, vous pouvez aussi créer des composants avec des classes PHP, pour des cas plus complexes nécessitant une certaine logique métier.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

c'est peut etre un abus de language de parler de "logique métier" ici ? Ça permet d'ajouter de la logique, métier ou non.


Prenons un exemple très simple de formatage d'un prix. Imaginons que votre contrôleur vous renvoie un prix, et que vous voulez l'afficher en euros. Par exemple, `12.34` devrait être affiché comme `12,34 €`.

Vous pouvez créer un Live Twig Component. Ce composant acceptera des propriétés `price` et `locale`, et expose une méthode `formatPrice`.

```php
<?php

namespace App\Twig\Components;

use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
use NumberFormatter;

#[AsTwigComponent('product:price')]
final class Price
{
public float $price;
public string $locale = 'fr_FR';

public function formatPrice(): string
{
$formatter = new NumberFormatter($this->locale, NumberFormatter::CURRENCY);
return $formatter->formatCurrency($this->price, 'EUR');
}
}
```

Dans cet exemple, le attributes `#[AsTwigComponent('product:price')]` définit le nom du composant, qui sera automatiquement lié au fichier `templates/components/product/price.html.twig`. Voici à quoi pourrait ressembler le template Twig :

```twig
<span {{ attributes.without('class') }}
class="w-fit bg-gray-300 rounded-full px-3 py-1 text-sm font-semibold text-gray-900 {{ attributes.render('class')|tailwind_merge }}"
>
{{ this.formatPrice }}
</span>
```

Dans ce template, la méthode `formatPrice` est appelée via `this.formatPrice`, permettant ainsi d'afficher le prix formaté de manière propre et réutilisable dans votre application.

#### Pourquoi utiliser cette approche ?

Avec cette approche, vous pouvez :

- **Séparer la logique métier du rendu** : La méthode `formatPrice` encapsule la logique de formatage.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

je ne sais pas, implémenté dans un live component, je n'ai pas vraiment la sensation que la "logique est séparer du rendu", ça me semble assez couplé au contraire. Je suppose que tu veux dire qu'elle ne fait pas partie du template ? C'est déjà ce que font les filtres twig non ?

Voici comment sont présentés les live components dans la doc symfony :

Live components builds on top of the [TwigComponent](https://symfony.com/bundles/ux-twig-component/current/index.html) library to give you the power to automatically update your Twig components on the frontend as the user interacts with them.

C'est présenté comme une solution pour ajouter de l'interactivité sur un composant, c'est assez loin de la description que tu en donnes ici.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

y'a pas une confusion, ce que tu décris ce sont des twig components non, pas des live components ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Effectivement, c’est peut-être un abus de dire que c’est un Live Twig Component juste parce qu'il a un petite logique. Je me suis sans doute un peu égaré entre les Anonymous Components, les Live Components, et les simples Twig Components sans nom particulié

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Par contre, quand je parle de séparation entre le rendu et la logique, je veux dire que d’un côté, tu as ton template avec le style, etc., et de l’autre, ta classe PHP qui gère la logique. Contrairement à certaines utilisations des extensions Twig qui retournent directement une string contenant des balises HTML. C’est pour illustrer ce point que je donne cet exemple.

- **Tirer parti de la puissance de PHP** : Vous avez accès à toutes les fonctionnalités de PHP pour construire vos composants, comme ici avec `NumberFormatter`.

Fini les Twig Extension qui retournent du HTML. Grâce à cette méthode, vous obtenez des composants puissants, clairs et réutilisables.

#### Exemple d'utilisation

Voici comment intégrer ce composant dans un template Twig :

```twig
<twig:product:price price="19.99" />
```

Rendu HTML :

```html
<span class="w-fit bg-gray-300 rounded-full px-3 py-1 text-sm font-semibold text-gray-900">
19,99 €
</span>
```

##### Bonus: Testez vos composants

Vous pouvez tester vos composants Twig avec PHPUnit d'une simplicité deconcertente. Voici un exemple de test pour notre composant `Price`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Vous pouvez tester vos composants Twig avec PHPUnit d'une simplicité deconcertente. Voici un exemple de test pour notre composant `Price`.
Vous pouvez tester vos composants Twig avec PHPUnit avec une simplicité déconcertante. Voici un exemple de test pour notre composant `Price`.


```php
<?php

namespace App\Tests\Twig\Components;

use App\Twig\Components\Price;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\UX\TwigComponent\Test\InteractsWithTwigComponents;

class PriceTest extends KernelTestCase
{
use InteractsWithTwigComponents;

public function testComponentMount(): void
{
$component = $this->mountTwigComponent(
name: Price::class,
data: ['price' => 99.99, 'locale' => 'fr_FR'],
);

$this->assertInstanceOf(Price::class, $component);
$this->assertSame(99.99, $component->price);
$this->assertSame('fr_FR', $component->locale);
}

public function testComponentRenders(): void
{
$rendered = $this->renderTwigComponent(
name: Price::class,
data: ['price' => 99.99, 'locale' => 'fr_FR'],
);

$this->assertStringContainsString('99,99 €', $rendered); // Notez l'espace insécable entre le montant et le symbole de l'euro
}
}
```

Et voilà rien de plus compliqué que ca pour tester nos composants Twig. On test le mount du composant et on test le rendu du composant, tout simplement.

## Limites

- **Lisibilité du Code** : Twig peut parfois rendre le code plus verbeux, surtout avec des composants complexes ou lorsque plusieurs niveaux de composants sont imbriqués. La syntaxe de Twig n'est pas toujours la plus concise et peut rendre le DOM final difficile à appréhender, surtout pour les développeurs non familiers avec cette approche. Par exemple, l'utilisation de directives Twig à l’intérieur des attributs HTML pour manipuler des classes, des styles, etc., peut rapidement rendre le code difficile à lire.

- **Séparation de la Logique et de la Présentation** : Lorsqu'on utilises à la fois des Twig Components et des classes PHP pour définir la logique, cela peut entraîner une certaine confusion. Par exemple, un composant Twig peut appeler une méthode définie par sa classe PHP, ce qui peut rendre la traçabilité du flux logique un peu floue, surtout pour des développeurs qui ne sont pas familiers avec ce type d'architecture.
ErwannRousseau marked this conversation as resolved.
Show resolved Hide resolved

- **Utilisation de Twig Extension vs Twig Components** : Twig permet créer des extensions ce qui peut simplifier certaines tâches. Additionné avec des Twig Components, Anonymous ou/et Live, cela peut créer un soucis d'harmonisation dans votre codebase.

## Conclusion

L’utilisation de Twig Components avec Tailwind CSS est une approche puissante et moderne pour développer le frontend d'un monolithe Symfony. Certes, cette méthodologie a ses limites, comme la lisibilité parfois complexe du DOM, dû aux parfoit nombreuses classes Tailwind et à Twig qui peut parfois rendre le code plus verbeux, ou bien la nécessité de trouver un équilibre entre logique PHP et Twig. Cependant, ces défis peuvent être surmontés avec une bonne organisation, un découpage rigoureux des composants et des conventions claires.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dû aux parfoit

parfois
dût ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ce que je veux dire, c’est que ce n’est pas systématique d’utiliser une multitude de classes Tailwind pour styliser une balise, ce qui pourrait alourdir le HTML.


Selon notre d'expérience, nous sommes pleinement satisfaits de cette approche. Elle nous a permis d'intégrer nos maquettes de manière de plus en plus rapide et efficace grâce à une bibliothèque de composants que nous avons enrichie au fil du projet. Cette productivité accrue n’aurait pas été possible sans Tailwind CSS, qui simplifie et accélère grandement le stylage de nos composants et du reste de notre interface. En somme, cette combinaison s'est révélée être un excellent choix pour nos besoins.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suite à notre d'expérience, nous sommes pleinement satisfaits de cette approche.

tournure un peu étrange ? Nous sommes pleinement satisfaits de notre expèrience de cette approche ?

Cette productivité accrue n’aurait pas été possible sans Tailwind CSS

faut peut-etre pas s'enflammer, imho il vaudrait mieux parler d'une aide/d'un gain de productivité apporté par Tailwind

2 changes: 1 addition & 1 deletion src/Twig/Components/Price.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ final class Price
public float $price;
public string $locale = 'en_US';

public function getFormatPrice(): string
public function formatPrice(): string
{
$formatter = new NumberFormatter($this->locale, NumberFormatter::CURRENCY);
return $formatter->formatCurrency($this->price, 'USD');
Expand Down
1 change: 0 additions & 1 deletion tailwind.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
"./assets/**/*.js",
"./templates/**/*.html.twig",
Expand Down
6 changes: 3 additions & 3 deletions templates/base.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
<meta charset="UTF-8">
<title>Tailwind + Twig Components</title>
<link rel="icon" href="{{ asset('icons/knp.svg') }}">
{% block javascripts %}
{% block importmap %}{{ importmap('app') }}{% endblock %}
{% block stylesheets %}
<link rel="stylesheet" href="{{ asset('styles/app.css') }}">
{% endblock %}
</head>
<body class="container bg-background min-h-dvh">
<body class="container bg-background min-h-dvh lg">
<main>
{% block body %}{% endblock %}
</main>
Expand Down
Loading