Créer des vues en utilisant TWIG

Un modèle est le meilleur moyen d’organiser et de restituer le HTML depuis l’intérieur de votre application, que vous ayez besoin de restituer du HTML à partir d’un contrôleur ou de générer le contenu d’un e-mail. Les modèles dans Symfony sont créés avec Twig : un moteur de modèles flexible, rapide et sécurisé.

Langage de modélisation Twig

Le langage de modélisation Twig vous permet d’écrire des modèles concis et lisibles, plus conviviaux pour les concepteurs Web et, à plusieurs égards, plus puissants que les modèles PHP. Jetez un coup d’œil à l’exemple de modèle Twig suivant. Même si c’est la première fois que vous voyez Twig, vous en comprenez probablement la plupart :

<!DOCTYPE html>
<html>
    <head>
        <title>Bienvenue dans Symfony !!!</title>
    </head>
    <body>
        <h1>{{ page_title }}</h1>

        {% if user.isLoggedIn %}
            Bonjour{{ user.name }}!
        {% endif %}

    </body>
</html>

La syntaxe de Twig est basée sur ces trois constructions :

  • {{ … }}, utilisée pour afficher le contenu d’une variable ou le résultat de l’évaluation d’une expression ;
  • {% … %}, utilisé pour exécuter une logique, telle qu’une conditionnelle ou une boucle ;
  • {# … #}, utilisé pour ajouter des commentaires au modèle (contrairement aux commentaires HTML, ces commentaires ne sont pas inclus dans la page rendue).

Vous ne pouvez pas exécuter de code PHP à l’intérieur des modèles Twig, mais Twig fournit des utilitaires pour exécuter une certaine logique dans les modèles. Par exemple, les filtres modifient le contenu avant d’être rendu, comme le filtre upper pour mettre le contenu en majuscules :

{{ title|upper }}

Twig est livré avec une longue liste de balises, de filtres et de fonctions qui sont disponibles par défaut. Dans les applications Symfony, vous pouvez également utiliser ces filtres et fonctions Twig définis par Symfony et vous pouvez créer vos propres filtres et fonctions Twig.

Twig est rapide dans l’environnement prod (car les modèles sont compilés en PHP et mis en cache automatiquement), mais pratique à utiliser dans l’environnement dev (car les modèles sont recompilés automatiquement lorsque vous les modifiez).

Configuration de Twig

Twig dispose de plusieurs options de configuration pour définir des éléments tels que le format utilisé pour afficher les chiffres et les dates, la mise en cache des modèles, etc. Lisez la référence sur la configuration de Twig pour en savoir plus sur ces options.

Création de modèles

Avant d’expliquer en détail comment créer et rendre des modèles, regardez l’exemple suivant pour avoir un aperçu rapide de l’ensemble du processus. Tout d’abord, vous devez créer un nouveau fichier dans le répertoire templates/ pour stocker le contenu du modèle :

{# templates/user/notifications.html.twig #}
<h1>Hello {{ user_first_name }}!</h1>
<p>You have {{ notifications|length }} new notifications.</p>

Ensuite, créez un contrôleur qui rend ce modèle et lui transmet les variables nécessaires :

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

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;

class UserController extends AbstractController
{
    // ...

    public function notifications(): Response
    {
        // get the user information and notifications somehow
        $userFirstName = '...';
        $userNotifications = ['...', '...'];

        // the template path is the relative file path from `templates/`
        return $this->render('user/notifications.html.twig', [
            // this array defines the variables passed to the template,
            // where the key is the variable name and the value is the variable value
            // (Twig recommends using snake_case variable names: 'foo_bar' instead of 'fooBar')
            'user_first_name' => $userFirstName,
            'notifications' => $userNotifications,
        ]);
    }
}

Nommage des modèles

Symfony recommande ce qui suit pour les noms de modèles :

Utilisez la casse pour les noms de fichiers et les répertoires (par exemple blog_posts.html.twig, admin/default_theme/blog/index.html.twig, etc.) ;
Définir deux extensions pour les noms de fichiers (par exemple, index.html.twig ou blog_posts.xml.twig), la première extension (html, xml, etc.) étant le format final que le modèle va générer.
Bien que les modèles génèrent généralement des contenus HTML, ils peuvent générer n’importe quel format textuel. C’est pourquoi la convention à deux extensions simplifie la création et le rendu des modèles pour plusieurs formats.

Emplacement des modèles

Les modèles sont stockés par défaut dans le répertoire templates/. Lorsqu’un service ou un contrôleur rend le modèle product/index.html.twig, il fait en fait référence au fichier /templates/product/index.html.twig.

Le répertoire de templates par défaut est configurable avec l’option twig.default_path et vous pouvez ajouter d’autres répertoires de templates comme expliqué plus loin dans cet article.

Variables de modèle

Un besoin courant pour les modèles est d’imprimer les valeurs stockées dans les modèles transmis par le contrôleur ou le service. Les variables stockent généralement des objets et des tableaux plutôt que des chaînes de caractères, des nombres et des valeurs booléennes. C’est pourquoi Twig offre un accès rapide aux variables PHP complexes. Considérons le modèle suivant :

Copie

{{nomdel’utilisateur}} a ajouté ce commentaire le {{comment.publishedAt|date}}

La notation user.name signifie que vous voulez afficher une information (nom) stockée dans une variable (user). User est-il un tableau ou un objet ? name est-il une propriété ou une méthode ? Dans Twig, cela n’a pas d’importance.

En utilisant la notation foo.bar, Twig essaie de récupérer la valeur de la variable dans l’ordre suivant :

  • $foo[‘bar’] (tableau et élément) ;
  • $foo->bar (objet et propriété publique) ;
  • $foo->bar() (objet et méthode publique) ;
  • $foo->getBar() (objet et méthode getter) ;
  • $foo->isBar() (objet et méthode isser) ;
  • $foo->hasBar() (objet et méthode hasser) ;

Si aucun des éléments ci-dessus n’existe, utilisez null (ou lancez une exception Twig\Error\RuntimeError si l’option strict_variables est activée).
Cela permet de faire évoluer le code de votre application sans avoir à modifier le code du modèle (vous pouvez commencer par des variables de type tableau pour la validation de l’application, puis passer à des objets avec des méthodes, etc.)

Liens vers les pages

Au lieu d’écrire les URL des liens à la main, utilisez la fonction path() pour générer des URL en fonction de la configuration du routage.

Plus tard, si vous souhaitez modifier l’URL d’une page particulière, il vous suffira de changer la configuration de routage : les modèles généreront automatiquement la nouvelle URL.

Considérons la configuration de routage suivante :

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

// ...
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class BlogController extends AbstractController
{
    /**
     * @Route("/", name="blog_index")
     */
    public function index(): Response
    {
        // ...
    }

    /**
     * @Route("/article/{slug}", name="blog_post")
     */
    public function show(string $slug): Response
    {
        // ...
    }
}

Utilisez la fonction Twig path() pour créer un lien vers ces pages et transmettez le nom de l’itinéraire comme premier argument et les paramètres de l’itinéraire comme deuxième argument facultatif :

<a href="{{ path('blog_index') }}">Homepage</a>

{# ... #}

{% for post in blog_posts %}
    <h1>
        <a href="{{ path('blog_post', {slug: post.slug}) }}">{{ post.title }}</a>
    </h1>

    <p>{{ post.excerpt }}</p>
{% endfor %}

La fonction path() génère des URL relatives. Si vous devez générer des URL absolues (par exemple, lors du rendu de modèles d’e-mails ou de flux RSS), utilisez la fonction url(), qui prend les mêmes arguments que path() (par exemple, ).

Liens vers des ressources CSS, JavaScript et images

Si un template a besoin de faire un lien vers une ressource statique (par exemple une image), Symfony fournit une fonction asset() Twig pour aider à générer cette URL. Tout d’abord, installez le package asset :

composer require symfony/asset

Vous pouvez maintenant utiliser la fonction asset() :

{# the image lives at "public/images/logo.png" #}
<img src="{{ asset('images/logo.png') }}" alt="Symfony!"/>

{# the CSS file lives at "public/css/blog.css" #}
<link href="{{ asset('css/blog.css') }}" rel="stylesheet"/>

{# the JS file lives at "public/bundles/acme/js/loader.js" #}
<script src="{{ asset('bundles/acme/js/loader.js') }}"></script>

L’objectif principal de la fonction asset() est de rendre votre application plus portable. Si votre application se trouve à la racine de votre hôte (par exemple https://example.com), le chemin rendu devrait être /images/logo.png. Mais si votre application se trouve dans un sous-répertoire (par exemple https://example.com/my_app), chaque chemin d’accès aux ressources doit être rendu avec le sous-répertoire (par exemple /my_app/images/logo.png). La fonction asset() s’en charge en déterminant comment votre application est utilisée et en générant les chemins corrects en conséquence.

La fonction asset() supporte diverses techniques de suppression du cache via les options de configuration version, version_format, et json_manifest_path.

Si vous souhaitez obtenir de l’aide pour empaqueter, versionner et réduire vos ressources JavaScript et CSS d’une manière moderne, consultez Webpack Encore de Symfony.

Si vous avez besoin d’URLs absolues pour vos ressources, utilisez la fonction Twig absolute_url() comme suit :

<img src="{{ absolute_url(asset('images/logo.png')) }}" alt="Symfony!"/>

<link rel="shortcut icon" href="{{ absolute_url('favicon.png') }}">

La variable globale App

Symfony crée un objet contextuel qui est injecté dans chaque modèle Twig automatiquement comme une variable appelée app. Elle permet d’accéder à certaines informations de l’application :

<p>Username: {{ app.user.username ?? 'Anonymous user' }}</p>
{% if app.debug %}
    <p>Request method: {{ app.request.method }}</p>
    <p>Application Environment: {{ app.environment }}</p>
{% endif %}

app.user
L’objet utilisateur actuel ou null si l’utilisateur n’est pas authentifié.

app.request
L’objet Request qui stocke les données de la requête actuelle (selon votre application, il peut s’agir d’une sous-requête ou d’une requête normale).

app.session
L’objet Session qui représente la session de l’utilisateur actuel ou null s’il n’y en a pas.

app.flashs
Un tableau de tous les messages flash stockés dans la session. Vous pouvez également obtenir uniquement les messages d’un certain type (par exemple, app.flashes(‘notice’)).

app.environment
Le nom de l’environnement de configuration actuel (dev, prod, etc).

app.debug
Vrai si en mode débogage. Faux sinon.

app.token
Un objet TokenInterface représentant le jeton de sécurité.
En plus de la variable globale app injectée par Symfony, vous pouvez également injecter des variables automatiquement à tous les templates Twig.

Rendu des modèles

Rendu d’un modèle dans les contrôleurs

Si votre contrôleur s’étend à partir de AbstractController, utilisez l’aide render() :

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

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;

class ProductController extends AbstractController
{
    public function index(): Response
    {
        // ...

        // the `render()` method returns a `Response` object with the
        // contents created by the template
        return $this->render('product/index.html.twig', [
            'category' => '...',
            'promotions' => ['...', '...'],
        ]);

        // the `renderView()` method only returns the contents created by the
        // template, so you can use those contents later in a `Response` object
        $contents = $this->renderView('product/index.html.twig', [
            'category' => '...',
            'promotions' => ['...', '...'],
        ]);

        return new Response($contents);
    }
}

Si votre contrôleur n’est pas issu de AbstractController, vous devrez récupérer les services dans votre contrôleur et utiliser la méthode render() du service twig.

Rendre un modèle dans les services

Injectez le service Symfony twig dans vos propres services et utilisez sa méthode render(). Lorsque vous utilisez l’autocâblage des services, il vous suffit d’ajouter un argument dans le constructeur du service et de lui donner un indice de type avec la classe Environment :

// src/Service/SomeService.php
namespace App\Service;

use Twig\Environment;

class SomeService
{
    private $twig;

    public function __construct(Environment $twig)
    {
        $this->twig = $twig;
    }

    public function someMethod()
    {
        // ...

        $htmlContents = $this->twig->render('product/index.html.twig', [
            'category' => '...',
            'promotions' => ['...', '...'],
        ]);
    }
}

Débogage des modèles

Symfony fournit plusieurs utilitaires pour vous aider à déboguer les problèmes dans vos modèles.

Linting des modèles Twig

La commande lint:twig vérifie que vos modèles Twig ne comportent pas d’erreurs de syntaxe. Il est utile de l’exécuter avant de déployer votre application en production (par exemple, dans votre serveur d’intégration continue) :

php bin/console lint:twig

php bin/console lint:twig templates/email/
php bin/console lint:twig templates/article/recent_list.html.twig

php bin/console lint:twig --show-deprecations templates/email/

Inspecter les informations de Twig

La commande debug:twig liste toutes les informations disponibles sur Twig (fonctions, filtres, variables globales, etc.). Elle est utile pour vérifier si vos extensions Twig personnalisées fonctionnent correctement et aussi pour vérifier les fonctionnalités Twig ajoutées lors de l’installation de paquets :

php bin/console debug:twig

php bin/console debug:twig --filter=date

php bin/console debug:twig @Twig/Exception/error.html.twig

Les utilitaires Dump Twig

Symfony fournit une fonction dump() comme alternative améliorée à la fonction var_dump() de PHP. Cette fonction est utile pour inspecter le contenu de n’importe quelle variable et vous pouvez également l’utiliser dans les modèles Twig.

Tout d’abord, assurez-vous que le composant VarDumper est installé dans l’application :

composer require symfony/var-dumper

Ensuite, utilisez la balise {% dump %} ou la fonction {{ dump() }} en fonction de vos besoins :

{# templates/article/recent_list.html.twig #}
{# the contents of this variable are sent to the Web Debug Toolbar
   instead of dumping them inside the page contents #}
{% dump articles %}

{% for article in articles %}
    {# the contents of this variable are dumped inside the page contents
       and they are visible on the web page #}
    {{ dump(article) }}

    <a href="/article/{{ article.slug }}">
        {{ article.title }}
    </a>
{% endfor %}

Pour éviter la fuite d’informations sensibles, la fonction/tag dump() est uniquement disponible dans les environnements de configuration dev et test. Si vous essayez de l’utiliser dans l’environnement prod, vous obtiendrez une erreur PHP.

Réutilisation du contenu des modèles

Inclure des modèles

Si un certain code Twig est répété dans plusieurs modèles, vous pouvez l’extraire dans un seul « fragment de modèle » et l’inclure dans d’autres modèles. Imaginez que le code suivant pour afficher les informations de l’utilisateur est répété à plusieurs endroits :

{# templates/blog/index.html.twig #}

{# ... #}
<div class="user-profile">
    <img src="{{ user.profileImageUrl }}" alt="{{ user.fullName }}"/>
    <p>{{ user.fullName }} - {{ user.email }}</p>
</div>

Tout d’abord, créez un nouveau modèle Twig appelé blog/user_profile.html.twig (le préfixe est facultatif, mais c’est une convention utilisée pour mieux différencier les modèles complets des fragments de modèle).

Ensuite, supprimez le contenu du modèle original blog/index.html.twig et ajoutez ce qui suit pour inclure le fragment de modèle :

{# templates/blog/index.html.twig #}

{# ... #}
{{ include('blog/_user_profile.html.twig') }}

La fonction include() Twig prend comme argument le chemin du modèle à inclure. Le modèle inclus a accès à toutes les variables du modèle qui l’inclut (utilisez l’option with_context pour contrôler cela).

Vous pouvez également passer des variables au modèle inclus. Ceci est utile par exemple pour renommer des variables. Imaginez que votre modèle stocke les informations sur l’utilisateur dans une variable appelée blog_post.author au lieu de la variable utilisateur attendue par le fragment de modèle. Utilisez ce qui suit pour renommer la variable :

{# templates/blog/index.html.twig #}

{# ... #}
{{ include('blog/_user_profile.html.twig', {user: blog_post.author}) }}

Intégration des contrôleurs

L’inclusion de fragments de modèles est utile pour réutiliser le même contenu sur plusieurs pages. Toutefois, cette technique n’est pas la meilleure solution dans certains cas.

Imaginons que le fragment de modèle affiche les trois articles les plus récents du blog. Pour ce faire, il doit effectuer une requête dans une base de données pour obtenir ces articles. Si vous utilisez la fonction include(), vous devrez effectuer la même requête de base de données dans chaque page qui inclut le fragment. Ce n’est pas très pratique.

Une meilleure alternative est d’intégrer le résultat de l’exécution d’un contrôleur avec les fonctions render() et controller() de Twig.

D’abord, créez le contrôleur qui rend un certain nombre d’articles récents :

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

use Symfony\Component\HttpFoundation\Response;
// ...

class BlogController extends AbstractController
{
    public function recentArticles(int $max = 3): Response
    {
        // get the recent articles somehow (e.g. making a database query)
        $articles = ['...', '...', '...'];

        return $this->render('blog/_recent_articles.html.twig', [
            'articles' => $articles
        ]);
    }
}

Ensuite, créez le fragment de modèle blog/recent_articles.html.twig (le préfixe dans le nom du modèle est facultatif, mais c’est une convention utilisée pour mieux différencier les modèles complets des fragments de modèle) :

{# templates/blog/_recent_articles.html.twig #}
{% for article in articles %}
    <a href="{{ path('blog_show', {slug: article.slug}) }}">
        {{ article.title }}
    </a>
{% endfor %}

Vous pouvez maintenant appeler ce contrôleur à partir de n’importe quel modèle pour intégrer son résultat :

{# templates/base.html.twig #}

{# ... #}
<div id="sidebar">
    {# if the controller is associated with a route, use the path() or url() functions #}
    {{ render(path('latest_articles', {max: 3})) }}
    {{ render(url('latest_articles', {max: 3})) }}

    {# if you don't want to expose the controller with a public URL,
       use the controller() function to define the controller to execute #}
    {{ render(controller(
        'App\\Controller\\BlogController::recentArticles', {max: 3}
    )) }}
</div>

Lors de l’utilisation de la fonction controller(), les contrôleurs ne sont pas accessibles par une route Symfony normale mais par une URL spéciale utilisée exclusivement pour servir ces fragments de modèles. Configurez cette URL spéciale dans l’option fragments :

# config/packages/framework.yaml
framework:
    # ...
    fragments: { path: /_fragment }

Héritage des modèles et mises en page

Au fur et à mesure que votre application se développe, vous trouverez de plus en plus d’éléments répétés entre les pages, comme des en-têtes, des pieds de page, des barres latérales, etc. L’inclusion de modèles et l’intégration de contrôleurs peuvent vous aider, mais lorsque les pages partagent une structure commune, il est préférable d’utiliser l’héritage.

Le concept d’héritage des modèles Twig est similaire à l’héritage des classes PHP. Vous définissez un modèle parent à partir duquel les autres modèles peuvent s’étendre et les modèles enfants peuvent remplacer certaines parties du modèle parent.

Symfony recommande l’héritage de modèles à trois niveaux suivants pour les applications moyennes et complexes :

  • templates/base.html.twig, définit les éléments communs à tous les modèles d’application, tels que , , , etc ;
  • templates/layout.html.twig, s’étend à partir de base.html.twig et définit la structure de contenu utilisée dans toutes ou la plupart des pages, comme une mise en page à deux colonnes de contenu + barre latérale. Certaines sections de l’application peuvent définir leurs propres mises en page (par exemple, templates/blog/layout.html.twig) ;
  • templates/*.html.twig, les pages de l’application qui s’étendent à partir du modèle principal layout.html.twig ou de tout autre modèle de section.

En pratique, le modèle base.html.twig ressemblerait à ceci :

{# templates/base.html.twig #}
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>{% block title %}My Application{% endblock %}</title>
        {% block stylesheets %}
            <link rel="stylesheet" type="text/css" href="/css/base.css"/>
        {% endblock %}
    </head>
    <body>
        {% block body %}
            <div id="sidebar">
                {% block sidebar %}
                    <ul>
                        <li><a href="{{ path('homepage') }}">Home</a></li>
                        <li><a href="{{ path('blog_index') }}">Blog</a></li>
                    </ul>
                {% endblock %}
            </div>

            <div id="content">
                {% block content %}{% endblock %}
            </div>
        {% endblock %}
    </body>
</html>

La balise Twig block définit les sections de la page qui peuvent être remplacées dans les modèles enfants. Elles peuvent être vides, comme le bloc de contenu, ou définir un contenu par défaut, comme le bloc de titre, qui s’affiche lorsque les modèles enfants ne les remplacent pas.

Le modèle blog/layout.html.twig pourrait être comme ceci :

{# templates/blog/layout.html.twig #}
{% extends 'base.html.twig' %}

{% block content %}
    <h1>Blog</h1>

    {% block page_contents %}{% endblock %}
{% endblock %}

Le modèle s’étend à partir de base.html.twig et définit uniquement le contenu du bloc de contenu. Les autres blocs du modèle parent afficheront leur contenu par défaut. Cependant, ils peuvent être remplacés par le modèle d’héritage de troisième niveau, tel que blog/index.html.twig, qui affiche l’index du blog :

{# templates/blog/index.html.twig #}
{% extends 'blog/layout.html.twig' %}

{% block title %}Blog Index{% endblock %}

{% block page_contents %}
    {% for article in articles %}
        <h2>{{ article.title }}</h2>
        <p>{{ article.body }}</p>
    {% endfor %}
{% endblock %}

Ce modèle s’étend à partir du modèle de deuxième niveau (blog/layout.html.twig) mais remplace les blocs de différents modèles parents : page_contents de blog/layout.html.twig et title de base.html.twig.

Lorsque vous effectuez le rendu du modèle blog/index.html.twig, Symfony utilise trois modèles différents pour créer le contenu final. Ce mécanisme d’héritage augmente votre productivité car chaque modèle n’inclut que son contenu unique et laisse le contenu répété et la structure HTML à certains modèles parents.