Notre premier ArticleCard recevait l’article complet dans une seule propriété de type tableau. Cette approche permet de découvrir rapidement Twig Components, mais elle ne décrit pas précisément les données attendues et offre une seule apparence.
Dans ce chapitre, nous conservons le même composant et le même contrôleur. Nous améliorons uniquement son contrat et sa capacité de configuration avec trois notions : les props typées, le sac attributes et une variante visuelle.
Le code complet est disponible sur GitHub :
https://github.com/amine-betari/symfony-ux-lab
Le résultat
Démarrer le laboratoire puis ouvrir :
http://localhost:8480/articles
La première carte occupe toute la largeur avec la variante featured. Les deux suivantes utilisent l’apparence default. Les trois instances sont pourtant rendues par le même composant ArticleCard.
La limite du tableau unique
Notre première version déclarait une seule propriété :
#[AsTwigComponent]
final class ArticleCard
{
public array $article = [];
}
Puis la page transmettait le tableau complet :
<twig:ArticleCard :article="article" />
Cette classe indique seulement qu’elle accepte un tableau. Elle ne permet pas de savoir immédiatement si le composant attend un titre, un slug, une catégorie ou un temps de lecture.
Déclarer des props typées
Nous remplaçons le tableau par des propriétés explicites :
#[AsTwigComponent]
final class ArticleCard
{
public string $slug;
public string $title;
public string $excerpt;
public string $category;
public int $readingTime;
public string $icon;
public string $accent;
public string $variant = 'default';
}
La classe constitue maintenant un contrat lisible :
slug, title, excerpt, category, icon, accent → chaînes de caractères
readingTime → entier
variant → chaîne, default par défaut
Une propriété sans valeur par défaut est obligatoire lors du rendu. Le type PHP aide également à détecter une donnée incohérente plus tôt.
Fournir les props depuis la page
L’appel devient plus détaillé :
<twig:ArticleCard
:slug="article.slug"
:title="article.title"
:excerpt="article.excerpt"
:category="article.category"
:readingTime="article.readingTime"
:icon="article.icon"
:accent="article.accent"
/>
Le caractère : demande à Twig d’évaluer l’expression située à droite :
:title="article.title"
Sans :, le composant recevrait le texte littéral article.title au lieu du véritable titre.
Cette écriture est plus longue que le tableau unique, mais elle rend les dépendances du composant visibles à l’endroit où il est utilisé.
Ajouter une variante
Nous voulons mettre en valeur le premier article sans créer un second composant presque identique.
La boucle Twig connaît déjà la variable loop.first. Elle peut donc choisir la variante :
:variant="loop.first ? 'featured' : 'default'"
La première itération transmet featured. Toutes les autres transmettent default.
Dans le template du composant, la variante complète une classe CSS :
class="article-card article-card--{{ variant }}"
Nous obtenons ainsi :
<article class="article-card article-card--featured">
ou :
<article class="article-card article-card--default">
Le CSS modifie uniquement la disposition de la version mise en avant :
.article-card--featured {
display: grid;
grid-column: 1 / -1;
grid-template-columns: minmax(260px, .85fr) 1.5fr;
}
Une variante représente une différence prévue par le design. Il faut éviter de créer une nouvelle variante pour chaque détail ponctuel.
Comprendre le sac attributes
Une page peut avoir besoin d’ajouter un identifiant, une classe, un attribut ARIA ou une information data-* sans que le composant déclare une prop pour chaque attribut HTML possible.
Prenons cet appel volontairement complet :
<twig:ArticleCard
:title="article.title"
variant="featured"
id="article-principal"
class="homepage-card"
data-card-index="1"
aria-label="Article principal"
/>
Twig Components sépare automatiquement ce qui est transmis en deux groupes :
titleetvariantcorrespondent à des propriétés déclarées dans la classe PHP : ce sont des props ;id,class,data-card-indexetaria-labelne correspondent à aucune prop : ils sont rangés dans l’objet spécialattributes.
Autrement dit, une donnée utile au fonctionnement du composant devient généralement une prop. Une information destinée à la balise HTML racine reste un attribut HTML libre.
Cependant, le sac attributes n’est pas ajouté magiquement au HTML. Le template du composant doit décider où l’afficher :
<article {{ attributes.defaults({
class: 'article-card article-card--' ~ variant
}) }}>
defaults() fournit les classes de base du composant tout en conservant les attributs transmis par la page. La classe supplémentaire homepage-card est donc fusionnée avec celles du composant.
Le navigateur reçoit finalement :
<article
id="article-principal"
class="article-card article-card--featured homepage-card"
data-card-index="1"
aria-label="Article principal"
>
Si nous supprimons {{ attributes... }} du template, id, class, data-card-index et aria-label ne seront tout simplement pas rendus.
Attention aux fautes de frappe
Imaginons que nous écrivions :titel="article.title" au lieu de :title="article.title". Comme titel n’est pas une prop connue, Twig Components peut le considérer comme un simple attribut HTML. Pendant ce temps, la vraie prop obligatoire title n’a reçu aucune valeur : le composant peut alors provoquer une erreur ou produire un résultat incorrect.
La règle simple à retenir est donc :
donnée du composant → prop PHP déclarée
information HTML racine → sac attributes
Ce mécanisme est particulièrement utile pour :
id;class;aria-labelet les autres attributs d’accessibilité ;data-*pour les tests ou Stimulus ;titleourole.
Adapter le template aux props
Le template n’utilise plus article.title, mais directement la propriété title :
<span class="article-card__meta">
{{ category }} · {{ readingTime }} min
</span>
<h2>{{ title }}</h2>
<p>{{ excerpt }}</p>
<a href="{{ path('app_article_show', {slug}) }}">
Lire l'article
</a>
Le composant reste chargé de la présentation. Le contrôleur continue à fournir exactement le même tableau d’articles qu’auparavant.
Tester la variante et l’intégration
Le test isolé rend explicitement la variante featured :
$rendered = $this->renderTwigComponent('ArticleCard', [
'slug' => 'tester-un-composant',
'title' => 'Tester un composant Twig',
'excerpt' => 'Le composant est rendu indépendamment.',
'category' => 'Tests',
'readingTime' => 3,
'icon' => 'T',
'accent' => '#7f56d9',
'variant' => 'featured',
]);
$crawler = $rendered->crawler();
self::assertCount(1, $crawler->filter('.article-card--featured'));
Le test fonctionnel de la page vérifie ensuite l’assemblage complet :
self::assertCount(3, $crawler->filter('.article-card'));
self::assertCount(3, $crawler->filter('[data-card-index]'));
self::assertCount(1, $crawler->filter('.article-card--featured'));
Nous vérifions ainsi qu’une seule carte est mise en avant et que les attributs supplémentaires atteignent réellement le HTML final.
Ce que nous n’avons pas encore ajouté
Le composant reste entièrement rendu côté serveur pendant la requête HTTP.
Il ne contient encore :
- aucun contrôleur Stimulus ;
- aucun appel JavaScript ;
- aucune
LivePropouLiveAction; - aucune communication supplémentaire avec Symfony après le chargement.
Cette progression est volontaire. Les props et les attributs seront réutilisés lorsque nous connecterons plus tard le composant à Stimulus.
Conclusion
Nous avons transformé une carte réutilisable simple en composant configurable :
props typées → contrat clair
variant → plusieurs apparences prévues
attributes → attributs HTML libres sur la racine
Le prochain chapitre pourra introduire Stimulus sur ce socle, sans mélanger l’organisation du HTML et le comportement JavaScript.
