Le moteur de templates Twig

Les templates, ou vues, sont très intéressants. Nous l'avons déjà vu, leur objectif est de séparer le code PHP du code HTML. Ainsi, lorsque vous faites du PHP, vous n'avez pas 100 balises HTML qui gênent la lecture de votre code PHP. De même, lorsque votre designer fait du HTML, il n'a pas à subir votre code barbare PHP auquel il ne comprend rien.

Intéressé ? Lisez la suite. ;)

Les templates Twig

Intérêt

Les templates vont nous permettre de séparer le code PHP du code HTML/XML/Text, etc. Seulement, pour faire du HTML de présentation, on a toujours besoin d'un peu de code dynamique : faire une boucle pour afficher tous les articles d'un blog, créer des conditions pour afficher un menu différent pour les utilisateurs authentifiés ou non, etc. Pour faciliter ce code dynamique dans les templates, le moteur de templates Twig offre son pseudo-langage à lui. Ce n'est pas du PHP, mais c'est plus adapté et voici pourquoi :

  • La syntaxe est plus concise et plus claire. Rappelez-vous, pour afficher une variable, {{ mavar }} suffit, alors qu'en PHP il faudrait faire <?php echo $mavar; ?>.
  • Il y a quelques fonctionnalités en plus, comme l'héritage de templates (nous le verrons). Cela serait bien entendu possible en PHP, mais il faudrait coder soi-même le système et cela ne serait pas aussi esthétique.
  • Il sécurise vos variables automatiquement : plus besoin de se soucier de htmlentities(), addslashes() ou que sais-je encore.

Pour ceux qui se posent la question de la rapidité : aucune inquiétude ! Oui, il faut transformer le langage Twig en PHP avant de l'exécuter pour, finalement, afficher notre contenu. Mais Twig ne le fait que la première fois et met en cache du code PHP simple afin que, dès la deuxième exécution de votre page, ce soit en fait aussi rapide que du PHP simple.

Des pages web, mais aussi des e-mails et autres

En effet, pourquoi se limiter à nos pages HTML ? Les templates peuvent (et doivent) être utilisés partout. Quand on enverra des e-mails, leurs contenus seront placés dans un template. Il existe bien sûr un moyen de récupérer le contenu d'un template sans l'afficher immédiatement. Ainsi, en récupérant le contenu du template dans une variable quelconque, on pourra le passer à la fonction mail de notre choix.

Mais il en va de même pour un flux RSS par exemple ! Si l'on sait afficher une liste des news de notre site en HTML grâce au template liste_news.html.twig, alors on saura afficher un fichier RSS en gardant le même contrôleur, mais en utilisant le template liste_news.rss.twig à la place.

En pratique

On a déjà créé un template, mais un rappel ne fait pas de mal. Depuis le contrôleur, voici la syntaxe pour retourner une réponse HTTP toute faite, dont le contenu est celui d'un certain template :

1
2
3
4
5
6
7
<?php
// Depuis un contrôleur

return $this->render('SdzBlogBundle:Blog:index.html.twig', array(
  'var1' => $var1,
  'var2' => $var2
));

Et voici comment, au milieu d'un contrôleur, récupérer le contenu d'un template en texte :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<?php
// Depuis un contrôleur

$contenu = $this->renderView('SdzBlogBundle:Blog:email.txt.twig', array(
  'var1' => $var1,
  'var2' => $var2
));

// Puis on envoie l'e-mail, par exemple :
mail('moi@siteduzero.com', 'Inscription OK', $contenu);

Et le template SdzBlogBundle:Blog:email.txt.twig contiendrait par exemple :

1
2
3
4
5
6
7
{# src/Sdz/BlogBundle/Resources/views/Blog/email.txt.twig #}

Bonjour {{ pseudo }},

Toute l'équipe du site se joint à moi pour vous souhaiter la bienvenue sur notre site !

Revenez nous voir souvent !

À savoir

Première chose à savoir sur Twig : vous pouvez afficher des variables et pouvez exécuter des expressions. Ce n'est pas la même chose :

  • {{ … }} affiche quelque chose ;
  • {% … %} fait quelque chose ;
  • {# … #} n'affiche rien et ne fait rien : c'est la syntaxe pour les commentaires, qui peuvent être sur plusieurs lignes.

L'objectif de la suite de ce chapitre est donc :

  • D'abord, vous donner les outils pour afficher des variables : variables simples, tableaux, objets, appliquer des filtres, etc. ;
  • Ensuite, vous donner les outils pour construire un vrai code dynamique : faire des boucles, des conditions, etc. ;
  • Enfin, vous donner les outils pour organiser vos templates grâce à l'héritage et à l'inclusion de templates. Ainsi vous aurez un template maître qui contiendra votre design (avec les balises <html>, <head>, etc.) et vos autres templates ne contiendront que le contenu de la page (liste des news, etc.).

Afficher des variables

Syntaxe de base pour afficher des variables

Afficher une variable se fait avec les doubles accolades « {{ … }} ». Voici quelques exemples.

Description

Exemple Twig

Équivalent PHP

Afficher une variable

Pseudo : {{ pseudo }}

Pseudo : <?php echo $pseudo; ?>

Afficher l'index d'un tableau

Identifiant : {{ user['id'] }}

Identifiant : <?php echo $user['id']; ?>

Afficher l'attribut d'un objet, dont le getter respecte la convention $objet->getAttribut()

Identifiant : {{ user.id }}

Identifiant : <?php echo $user->getId(); ?>

Afficher une variable en lui appliquant un filtre. Ici, « upper » met tout en majuscules :

Pseudo en majuscules : {{ pseudo|upper }}

Pseudo en lettre majuscules : <?php echo strtoupper($pseudo); ?>

Afficher une variable en combinant les filtres. « striptags » supprime les balises HTML. « title » met la première lettre de chaque mot en majuscule. Notez l'ordre d'application des filtres, ici striptags est appliqué, puis title.

Message : {{ news.texte|striptags|title }}

Message : <?php echo ucwords(strip_tags($news->getTexte())); ?>

Utiliser un filtre avec des arguments. Attention, il faut que date soit un objet de type Datetime ici.

Date : {{ date|date('d/m/Y') }}

Date : <?php echo $date->format('d/m/Y'); ?>

Concaténer

Identité : {{ nom ~ " " ~ prenom }}

Identité : <?php echo $nom.' '.$prenom; ?>

Précisions sur la syntaxe {{ objet.attribut }}

Le fonctionnement de la syntaxe {{ objet.attribut }} est un peu plus complexe qu'elle n'en a l'air. Elle ne fait pas seulement objet->getAttribut. En réalité, voici ce qu'elle fait exactement :

  • Elle vérifie si objet est un tableau, et si attribut est un index valide. Si c'est le cas, elle affiche objet['attribut'].
  • Sinon, et si objet est un objet, elle vérifie si attribut est un attribut valide (public donc). Si c'est le cas, elle affiche objet->attribut.
  • Sinon, et si objet est un objet, elle vérifie si attribut() est une méthode valide (publique donc). Si c'est le cas, elle affiche objet->attribut().
  • Sinon, et si objet est un objet, elle vérifie si getAttribut() est une méthode valide. Si c'est le cas, elle affiche objet->getAttribut().
  • Sinon, et si objet est un objet, elle vérifie si isAttribut() est une méthode valide. Si c'est le cas, elle affiche objet->isAttribut().
  • Sinon, elle n'affiche rien et retourne null.

Les filtres utiles

Il y a quelques filtres disponibles nativement avec Twig, en voici quelques-uns :

Filtre

Description

Exemple Twig

Upper

Met toutes les lettres en majuscules.

{{ var|upper }}

Striptags

Supprime toutes les balises XML.

{{ var|striptags }}

Date

Formate la date selon le format donné en argument. La variable en entrée doit être une instance de Datetime.

{{ date|date('d/m/Y') }} Date d'aujourd'hui : {{ "now"|date('d/m/Y') }}

Format

Insère des variables dans un texte, équivalent à printf.

{{ "Il y a %s pommes et %s poires"|format(153, nb_poires) }}

Length

Retourne le nombre d'éléments du tableau, ou le nombre de caractères d'une chaîne.

Longueur de la variable : {{ texte|length }} Nombre d'éléments du tableau : {{ tableau|length }}

La documentation de tous les filtres disponibles est dans la documentation officielle de Twig : http://twig.sensiolabs.org/doc/filters/index.html.

Nous pourrons également créer nos propres filtres ! On le verra plus loin dans ce cours.

Twig et la sécurité

Dans tous les exemples précédents, vos variables ont déjà été protégées par Twig ! Twig applique par défaut un filtre sur toutes les variables que vous affichez, afin de les protéger de balises HTML malencontreuses. Ainsi, si le pseudo d'un de vos membres contient un « < » par exemple, lorsque vous faites {{ pseudo }} celui-ci est échappé, et le texte généré est en réalité « mon<pseudo » au lieu de « mon<pseudo », ce qui poserait problème dans votre structure HTML. Très pratique ! Et donc à savoir : inutile de protéger vos variables en amont, Twig s'occupe de tout en fin de chaîne !

Et dans le cas où vous voulez afficher volontairement une variable qui contient du HTML (JavaScript, etc.), et que vous ne voulez pas que Twig l'échappe, il vous faut utiliser le filtre raw comme suit : {{ ma_variable_html|raw }}. Avec ce filtre, Twig désactivera localement la protection HTML, et affichera la variable en brut, quel que soit ce qu'elle contient.

Les variables globales

Symfony2 enregistre quelques variables globales dans Twig pour nous faciliter la vie. Voici la liste des variables globales disponibles dans tous vos templates :

Variable

Description

{{ app.request }}

Le service « request » qu'on a vu au chapitre précédent sur les contrôleurs.

{{ app.session }}

Le service « session » qu'on a vu également au chapitre précédent.

{{ app.environment }}

L'environnement courant : « dev », « prod », et ceux que vous avez définis.

{{ app.debug }}

True si le mode debug est activé, False sinon.

{{ app.security }}

Le service « security », que nous verrons plus loin dans ce cours.

{{ app.user }}

L'utilisateur courant, que nous verrons également plus loin dans ce cours.

Bien entendu, on peut enregistrer nos propres variables globales, pour qu'elles soient accessibles depuis toutes nos vues, au lieu de les injecter à chaque fois. Pour cela, il faut éditer le fichier de configuration de l'application, comme suit :

1
2
3
4
5
6
7
8
# app/config/config.yml

# …

twig:
    # …
    globals:
        webmaster: moi-même

Ainsi, la variable {{ webmaster }} sera injectée dans toutes vos vues, et donc utilisable comme ceci :

1
<footer>Responsable du site : {{ webmaster }}.</footer>

Je profite de cet exemple pour vous faire passer un petit message. Pour ce genre de valeurs paramétrables, la bonne pratique est de les définir non pas directement dans le fichier de configuration config.yml, mais dans le fichier des paramètres, à savoir parameters.yml. Attention, je parle bien de la valeur du paramètre, non de la configuration. Voyez par vous-mêmes.

Valeur du paramètre :

1
2
3
4
5
# app/config/parameters.yml

parameters:
    # …
    app_webmaster: moi-même

Configuration (ici, injection dans toutes les vues) qui utilise le paramètre :

1
2
3
4
5
# app/config/config.yml

twig:
    globals:
        webmaster: %app_webmaster%

On a ainsi séparé la valeur du paramètre, stockée dans un fichier simple, et l'utilisation de ce paramètre, perdue dans le fichier de configuration.

Structures de contrôle et expressions

Les structures de contrôle

Nous avons vu comment afficher quelque chose, maintenant nous allons faire des choses, avec la syntaxe {% … %}.

Condition : {% if %}

Exemple Twig :

1
2
3
4
5
6
7
{% if membre.age < 12 %}
  Il faut avoir 12 ans pour ce film.
{% elseif membre.age < 18 %}
  OK bon film.
{% else %}
  Un peu vieux pour voir ce film non ?
{% endif %}

Équivalent PHP :

1
2
3
4
5
6
7
<?php if($membre->getAge() < 12) { ?>
  Il faut avoir 12 ans pour ce film.
<?php } elseif($membre->getAge() < 18) { ?>
  OK bon film.
<?php } else { ?>
  Un peux vieux pour voir ce film non ?
<?php } ?>

Boucle : {% for %}

Exemple Twig :

1
2
3
4
5
6
7
<ul>
  {% for membre in liste_membres %}
    <li>{{ membre.pseudo }}</li>
  {% else %}
    <li>Pas d'utilisateur trouvé.</li>
  {% endfor %}
</ul>

Et pour avoir accès aux clés du tableau :

1
2
3
4
5
<select>
  {% for valeur, option in liste_options %}
    <option value="{{ valeur }}">{{ option }}</option>
  {% endfor %}
</select>

Équivalent PHP :

1
2
3
4
5
6
7
8
9
<ul>
<?php if(count($liste_membres) > 0) {
  foreach($liste_membres as $membre) {
    echo '<li>'.$membre->getPseudo().'</li>';
  }
} else { ?>
  <li>Pas d'utilisateur trouvé.</li>
<?php } ?>
</ul>

Avec les clés :

1
2
3
4
<?php
foreach($liste_options as $valeur => $option) {
  // …
}

Définition : {% set %}

Exemple Twig :

1
{% set foo = 'bar' %}

Équivalent PHP :

1
<?php $foo = 'bar'; ?>

Une petite information sur la structure {% for %}, celle-ci définit une variable {{ loop }} au sein de la boucle, qui contient les attributs suivants :

Variable

Description

{{ loop.index }}

Le numéro de l'itération courante (en commençant par 1).

{{ loop.index0 }}

Le numéro de l'itération courante (en commençant par 0).

{{ loop.revindex }}

Le nombre d'itérations restantes avant la fin de la boucle (en finissant par 1).

{{ loop.revindex0 }}

Le nombre d'itérations restantes avant la fin de la boucle (en finissant par 0).

{{ loop.first }}

true si c'est la première itération, false sinon.

{{ loop.last }}

true si c'est la dernière itération, false sinon.

{{ loop.length }}

Le nombre total d'itérations dans la boucle.

Les tests utiles

Defined

Pour vérifier si une variable existe.

Exemple Twig :

1
{% if var is defined %}{% endif %}

Équivalent PHP :

1
<?php if(isset($var)) { }

Even / Odd

Pour tester si un nombre est pair / impair.

Exemple Twig :

1
2
3
4
5
6
{% for valeur in liste %}
  <span class="{% if loop.index is even %}pair{% else %}
    impair{% endif %}">
    {{ valeur }}
  </span>
{% endfor %}

Équivalent PHP :

1
2
3
4
5
6
7
8
<?php
$i = 0;
foreach($liste as $valeur) {
  echo '<span class="';
  echo $i % 2 ? 'impair' : 'pair';
  echo '">'.$valeur.'</span>';
  $i++;
}

La documentation de tous les tests disponibles est dans la documentation officielle de Twig.

Hériter et inclure des templates

L'héritage de template

Je vous ai fait un teaser précédemment : l'héritage de templates va nous permettre de résoudre la problématique : « J'ai un seul design et n'ai pas l'envie de le répéter sur chacun de mes templates ». C'est un peu comme ce que vous devez faire aujourd'hui avec les include(), mais en mieux !

Le principe

Le principe est simple : vous avez un template père qui contient le design de votre site ainsi que quelques trous (appelés « blocks » en anglais, que nous nommerons « blocs » en français) et des templates fils qui vont remplir ces blocs. Les fils vont donc venir hériter du père en remplaçant certains éléments par leur propre contenu.

L'avantage est que les templates fils peuvent modifier plusieurs blocs du template père. Avec la technique des include(), un template inclus ne pourra pas modifier le template père dans un autre endroit que là où il est inclus !

Les blocs classiques sont le centre de la page et le titre. Mais en fait, c'est à vous de les définir ; vous en ajouterez donc autant que vous voudrez.

La pratique

Voici à quoi peut ressembler un template père (appelé plus communément layout). Mettons-le dans src/Sdz/BlogBundle/Resources/views/layout.html.twig :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{# src/Sdz/BlogBundle/Resources/views/layout.html.twig #}

<!DOCTYPE HTML>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>{% block title %}SdzBlog{% endblock %}</title>
  </head>
  <body>

    {% block body %}
    {% endblock %}

  </body>
</html>

Voici un de nos templates fils. Mettons-le dans src/Sdz/BlogBundle/Resources/views/Blog/index.html.twig :

1
2
3
4
5
6
7
8
9
{# src/Sdz/BlogBundle/Resources/views/Blog/index.html.twig #}

{% extends "SdzBlogBundle::layout.html.twig" %}

{% block title %}{{ parent() }} - Index{% endblock %}

{% block body %}
    OK, même s'il est pour l'instant un peu vide, mon blog sera trop bien !
{% endblock %}

Qu'est-ce que l'on vient de faire ?

Pour bien comprendre tous les concepts utilisés dans cet exemple très simple, détaillons un peu.

Le nom du template père

On a placé ce template dans views/layout.html.twig et non dans views/qqch/layout.html.twig. C'est tout à fait possible ! En fait, il est inutile de mettre dans un sous-répertoire les templates qui ne concernent pas un contrôleur particulier et qui peuvent être réutilisés par plusieurs contrôleurs. Attention à la notation pour accéder à ce template : du coup, ce n'est plus SdzBlogBundle:MonController:layout.html.twig, mais SdzBlogBundle::layout.html.twig. C'est assez intuitif, en fait : on enlève juste la partie qui correspond au répertoire MonController. C'est ce que l'on a fait à la première ligne du template fils.

La balise {% block %} côté père

Pour définir un « trou » (dit bloc) dans le template père, nous avons utilisé la balise {% block %}. Un bloc doit avoir un nom afin que le template fils puisse modifier tel ou tel bloc de façon nominative. La base, c'est juste de faire {% block nom_du_block %}{% endblock %} et c'est ce que nous avons fait pour le body. Mais vous pouvez insérer un texte par défaut dans les blocs, comme on l'a fait pour le titre. C'est utile pour deux cas de figure :

  • Lorsque le template fils ne redéfinit pas ce bloc. Plutôt que de n'avoir rien d'écrit, vous aurez cette valeur par défaut.
  • Lorsque les templates fils veulent réutiliser une valeur commune. Par exemple, si vous souhaitez que le titre de toutes les pages de votre site commence par « SdzBlog », alors depuis les templates fils, vous pouvez utiliser {{ parent() }} qui permet d'utiliser le contenu par défaut du bloc côté père. Regardez, nous l'avons fait pour le titre dans le template fils.

La balise {% block %} côté fils

Elle se définit exactement comme dans le template père, sauf que cette fois-ci on y met notre contenu. Mais étant donné que les blocs se définissent et se remplissent de la même façon, vous avez pu deviner qu'on peut hériter en cascade ! En effet, si l'on crée un troisième template petit-fils qui hérite de fils, on pourra faire beaucoup de choses.

Le modèle « triple héritage »

Pour bien organiser ses templates, une bonne pratique est sortie du lot. Il s'agit de faire de l'héritage de templates sur trois niveaux, chacun des niveaux remplissant un rôle particulier. Les trois templates sont les suivants :

  • Layout général : c'est le design de votre site, indépendamment de vos bundles. Il contient le header, le footer, etc. La structure de votre site donc (c'est notre template père).
  • Layout du bundle : il hérite du layout général et contient les parties communes à toutes les pages d'un même bundle. Par exemple, pour notre blog, on pourrait afficher un menu particulier, rajouter « Blog » dans le titre, etc.
  • Template de page : il hérite du layout du bundle et contient le contenu central de votre page.

Nous verrons un exemple de ce triple héritage juste après dans l'exemple du blog.

Question : puisque le layout général ne dépend pas d'un bundle en particulier, où le mettre ?

Dans votre répertoire /app ! En effet, dans ce répertoire, vous pouvez toujours avoir des fichiers qui écrasent ceux des bundles ou bien des fichiers communs aux bundles. Le layout général de votre site fait partie de ces ressources communes. Son répertoire exact doit être app/Resources/views/layout.html.twig.

Et pour l'appeler depuis vos templates, la syntaxe est la suivante : « ::layout.html.twig ». Encore une fois, c'est très intuitif : après avoir enlevé le nom du contrôleur tout à l'heure, on enlève juste cette fois-ci le nom du bundle.

Afin de bien vous représenter l'architecture adoptée, je vous propose un petit schéma à la figure suivante. Il vaut ce qu'il vaut, mais vous permet de bien comprendre ce qu'on fait.

Héritage de templates sur trois niveaux

Je vous parle du bloc rouge un peu après, c'est une inclusion non pas de template, mais d'action de contrôleur ! Il ne fait pas partie du modèle triple héritage à proprement parler.

L'inclusion de templates

La théorie : quand faire de l'inclusion ?

Hériter, c'est bien, mais inclure, cela n'est pas mal non plus. Prenons un exemple pour bien faire la différence.

Le formulaire pour ajouter un article est le même que celui pour… modifier un article. On ne va pas faire du copier-coller de code, cela serait assez moche, et puis nous sommes fainéants. C'est ici que l'inclusion de templates intervient. On a nos deux templates SdzBlogBundle:Blog:ajouter.html.twig et SdzBlogBundle:Blog:modifier.html.twig qui héritent chacun de SdzBlogBundle::layout.html.twig. L'affichage exact de ces deux templates diffère un peu, mais chacun d'eux inclut SdzBlogBundle:Blog:formulaire.html.twig à l'endroit exact pour afficher le formulaire.

On voit bien qu'on ne peut pas faire d'héritage sur le template formulaire.html.twig, car il faudrait le faire hériter une fois de ajouter.html.twig, une fois de modifier.html.twig, etc. Comment savoir ? Et si un jour nous souhaitons ne le faire hériter de rien du tout pour afficher le formulaire tout seul dans une popup par exemple ? Bref, c'est bien une inclusion qu'il nous faut ici.

La pratique : comment le faire ?

Comme toujours avec Twig, cela se fait très facilement. Il faut utiliser la balise {% include %}, comme ceci :

1
{% include "SdzBlogBundle:Blog:formulaire.html.twig" %}

Ce code inclura le contenu du template à l'endroit de la balise. Une sorte de copier-coller automatique, en fait ! Voici un exemple avec la vue ajouter.html.twig :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{# src/Sdz/BlogBundle/Resources/views/Blog/ajouter.html.twig #}

{% extends "SdzBlogBundle::layout.html.twig" %}

{% block body %}

  <h2>Ajouter un article</h2>

  {% include "SdzBlogBundle:Blog:formulaire.html.twig" %}

  <p>
     Attention : cet article sera ajouté directement
     sur la page d'accueil après validation du formulaire.
  </p>

{% endblock %}

Et voici le code du template inclus :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{# src/Sdz/BlogBundle/Resources/views/Blog/formulaire.html.twig #}

{# Cette vue n'hérite de personne, elle sera incluse par d'autres vues qui, elles, hériteront probablement du layout. #}
{# Je dis « probablement » car, ici pour cette vue, on n'en sait rien et c'est une info qui ne nous concerne pas. #}

<h3>Formulaire d'article</h3>

{# Ici on laisse vide la vue pour l'instant, on la comblera plus tard lorsqu'on saura afficher un formulaire. #}
<div class="well">
  Ici se trouvera le formulaire.
</div>

À l'intérieur du template inclus, vous retrouvez toutes les variables qui sont disponibles dans le template qui fait l'inclusion : exactement comme si vous copiiez-colliez le contenu.

L'inclusion de contrôleurs

La théorie : quand inclure des contrôleurs ?

Voici un dernier point à savoir absolument avec Twig, un des points les plus puissants dans son utilisation avec Symfony2. On vient de voir comment inclure des templates : ceux-ci profitent des variables du template qui fait l'inclusion, très bien.

Seulement dans bien des cas, depuis le template qui fait l'inclusion, vous voudrez inclure un autre template, mais vous n'avez pas les variables nécessaires pour lui. Restons sur l'exemple de notre blog, dans le schéma précédent je vous ai mis un bloc rouge : considérons que dans cette partie du menu, accessible sur toutes les pages même hors du blog, on veut afficher les 3 derniers articles du blog.

C'est donc depuis le layout général qu'on va inclure non pas un template du bundle Blog — nous n'aurions pas les variables à lui donner —, mais un contrôleur du bundle Blog. Le contrôleur va créer les variables dont il a besoin, et les donner à son template, pour ensuite être inclus là où on le veut !

La pratique : comment le faire ?

Au risque de me répéter : cela se fait très simplement !

Du côté du template qui fait l'inclusion, à la place de la balise {% include %}, il faut utiliser la fonction {{ render() }}, comme ceci :

1
{{ render(controller("SdzBlogBundle:Blog:menu")) }}

Ici, SdzBlogBundle:Blog:menu n'est pas un template mais une action de contrôleur, c'est la syntaxe qu'on utilise dans les routes, vous l'aurez reconnue.

Si vous utilisez une version de Symfony antérieure à la 2.2, alors il faut utiliser la syntaxe suivante : {% render "SdzBlogBundle:Blog:menu" %} à la place du code précédent. Cela ne change strictement rien au comportement, c'est juste la syntaxe qui change. ;)

Voici par exemple ce qu'on mettrait dans le layout :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
{# app/Resources/views/layout.html.twig #}
<!DOCTYPE HTML>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>{% block title %}SdzBlog{% endblock %}</title>
  </head>
  <body>

    <div id="menu">
      {{ render(controller("SdzBlogBundle:Blog:menu")) }}
    </div>

    {% block body %}
    {% endblock %}

    </body>
</html>

Et du côté du contrôleur, c'est une méthode très classique (regardez la ligne 16) :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<?php
// src/Sdz/BlogBundle/Controller/BlogController.php

// …

  public function menuAction()
  {
    // On fixe en dur une liste ici, bien entendu par la suite on la récupérera depuis la BDD !
    $liste = array(
      array('id' => 2, 'titre' => 'Mon dernier weekend !'),
      array('id' => 5, 'titre' => 'Sortie de Symfony2.1'),
      array('id' => 9, 'titre' => 'Petit test')
    );

    return $this->render('SdzBlogBundle:Blog:menu.html.twig', array(
      'liste_articles' => $liste // C'est ici tout l'intérêt : le contrôleur passe les variables nécessaires au template !
    ));
  }

Et enfin, un exemple de ce que pourrait être le template menu.html.twig :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{# src/Sdz/BlogBundle/Resources/views/Blog/menu.html.twig #}

{# Ce template n'hérite de personne, tout comme le template inclus avec {% include %}. #}

<ul>
<ul class="nav nav-pills nav-stacked">
  {% for article in liste_articles %}
    <li><a href="{{ path('sdzblog_voir', {'id': article.id}) }}">{{ article.titre }}</a></li>
  {% endfor %}
</ul>
</ul>

Application : les templates de notre blog

Revenons à notre blog. Faites en sorte d'avoir sous la main le contrôleur que l'on a réalisé au chapitre précédent. Le but ici est de créer tous les templates que l'on a utilisés depuis le contrôleur, ou du moins leur squelette. Étant donné que l'on n'a pas encore la vraie liste des articles, on va faire avec des variables vides : cela va se remplir par la suite, mais le fait d'employer des variables vides va nous permettre dès maintenant de construire le template.

Pour encadrer tout ça, nous allons utiliser le modèle d'héritage sur trois niveaux : layout général, layout du bundle et template.

Layout général

La théorie

Comme évoqué précédemment, le layout est la structure HTML de notre site avec des blocs aux endroits stratégiques pour permettre aux templates qui hériteront de ce dernier de personnaliser la page. On va ici créer une structure simple ; je vous laisse la personnaliser si besoin est. Pour les blocs, pareil pour l'instant, on fait simple : un bloc pour le body et un bloc pour le titre.

Je vais également en profiter pour introduire l'utilisation de ressources CSS/JS/etc. dans Symfony2. Cela se fait très bien avec la fonction {{ asset() }} de Twig, qui va chercher vos ressources dans le répertoire /web. Regardez simplement comment elle est utilisée dans l'exemple et vous saurez l'utiliser de façon basique.

Pour le design du blog que l'on va construire, je vais utiliser le Bootstrap de Twitter. C'est un framework CSS, l'équivalent pour le CSS de ce que Symfony2 est pour le PHP. Cela permet de faire des beaux designs, boutons ou liens très rapidement. Vous pourrez le voir dans les vues que je fais par la suite. Mais tout d'abord, vous devez le télécharger et l'extraire dans le répertoire /web. Vous devez avoir le fichier bootstrap.css disponible dans le répertoire /web/css/bootstrap.css par exemple. Il existe un cours sur Bootstrap sur le Site du Zéro. Je vous invite à y jeter un œil si ce framework vous intéresse, ou juste par curiosité.

La pratique

Commençons par faire le layout général de l'application, la vue située dans le répertoire /app. Voici le code exemple que je vous propose :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
{# app/Resources/views/layout.html.twig #}

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

    <title>{% block title %}Sdz{% endblock %}</title>

    {% block stylesheets %}
      <link rel="stylesheet" href="{{ asset('css/bootstrap.css') }}" type="text/css" />
    {% endblock %}
  </head>

  <body>
    <div class="container">
      <div id="header" class="hero-unit">
        <h1>Mon Projet Symfony2</h1>
        <p>Ce projet est propulsé par Symfony2, et construit grâce au tutoriel du siteduzero.</p>
        <p><a class="btn btn-primary btn-large" href="http://www.siteduzero.com/informatique/tutoriels/developpez-votre-site-web-avec-le-framework-symfony2">
          Lire le tutoriel »
        </a></p>
      </div>

      <div class="row">
        <div id="menu" class="span3">
          <h3>Le blog</h3>
          <ul class="nav nav-pills nav-stacked">
            <li><a href="{{ path('sdzblog_accueil') }}">Accueil du blog</a></li>
            <li><a href="{{ path('sdzblog_ajouter') }}">Ajouter un article</a></li>
          </ul>

          {{ render(controller("SdzBlogBundle:Blog:menu", {'nombre': 3})) }}
        </div>
        <div id="content" class="span9">
          {% block body %}
          {% endblock %}
        </div>
      </div>

      <hr>

      <footer>
        <p>The sky's the limit © 2012 and beyond.</p>
      </footer>
    </div>

  {% block javascripts %}
    {# Ajoutez ces lignes JavaScript si vous comptez vous servir des fonctionnalités du bootstrap Twitter #}
    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
    <script type="text/javascript" src="{{ asset('js/bootstrap.js') }}"></script>
  {% endblock %}

  </body>
</html>

Voici les lignes qui contiennent un peu de Twig :

  • Ligne 8 : création du bloc « title » avec « Sdz » comme contenu par défaut ;
  • Lignes 11 : appel du CSS situé dans /web/css/bootstrap.css ;
  • Lignes 29 et 30 : utilisation de la fonction {{ path }} pour faire des liens vers d'autres routes ;
  • Ligne 33 : inclusion de la méthode menu du contrôleur Blog du bundle SdzBlogBundle, avec l'argument nombre défini à 3 ;
  • Lignes 36 et 37 : création du bloc « body » sans contenu par défaut.

Et voilà, nous avons notre layout général ! Pour pouvoir tester nos pages, il faut maintenant s'attaquer au layout du bundle.

Layout du bundle

La théorie

Comme on l'a dit, ce template va hériter du layout général, ajouter la petite touche personnelle au bundle Blog, puis se faire hériter à son tour par les templates finaux. En fait, il ne contient pas grand-chose. Laissez courir votre imagination, mais, moi, je ne vais rajouter qu'une balise <h1>, vous voyez ainsi le mécanisme et pouvez personnaliser à votre sauce.

La seule chose à laquelle il faut faire attention, c'est au niveau du nom des blocs que ce template crée pour ceux qui vont l'hériter. Une bonne pratique consiste à préfixer le nom des blocs par le nom du bundle courant. Regardez le code et vous comprendrez.

La pratique

Voici ce que j'ai mis pour le layout du bundle :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
{# src/Sdz/BlogBundle/Resources/views/layout.html.twig #}

{% extends "::layout.html.twig" %}

{% block title %}
  Blog - {{ parent() }}
{% endblock %}

{% block body %}

  {# On définit un sous-titre commun à toutes les pages du bundle, par exemple #}
  <h1>Blog</h1>

  <hr>

  {# On définit un nouveau bloc, que les vues du bundle pourront remplir #}
  {% block sdzblog_body %}
  {% endblock %}

{% endblock %}

On a ajouté un <h1> dans le bloc body, puis créé un nouveau bloc qui sera personnalisé par les templates finaux. On a préfixé le nom du nouveau bloc pour le body afin d'avoir un nom unique pour notre bundle.

Les templates finaux

Blog/index.html.twig

C'est le template de la page d'accueil. On va faire notre première boucle sur la variable {{ articles }}. Cette variable n'existe pas encore, on va modifier le contrôleur juste après.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{# src/Sdz/BlogBundle/Resources/views/Blog/index.html.twig #}

{% extends "SdzBlogBundle::layout.html.twig" %}

{% block title %}
  Accueil - {{ parent() }}
{% endblock %}

{% block sdzblog_body %}

  <h2>Liste des articles</h2>

  <ul>
    {% for article in articles %}
      <li>
        <a href="{{ path('sdzblog_voir', {'id': article.id}) }}">{{ article.titre }}</a>
        par {{ article.auteur }},
        le {{ article.date|date('d/m/Y') }}
      </li>
    {% else %}
      <li>Pas (encore !) d'articles</li>
    {% endfor %}
  </ul>

{% endblock %}

Pas grand-chose à dire, on a juste utilisé les variables et expressions expliquées dans ce chapitre.

Afin que cette page fonctionne, il nous faut modifier l'action indexAction() du contrôleur pour passer une variable {{ articles }} à cette vue. Pour l'instant, voici juste de quoi se débarrasser de l'erreur :

1
2
3
4
5
6
7
<?php
// src/Sdz/BlogBundle/Controller/BlogController.php

// Dans l'action indexAction() :
return $this->render('SdzBlogBundle:Blog:index.html.twig', array(
  'articles' => array()
));

Vous pouvez dès maintenant voir votre nouvelle peau : http://localhost/Symfony/web/app_dev.php/blog !

Si vous n'aviez pas rajouté l'action menu du contrôleur tout à l'heure, voici comment le faire, et aussi comment l'adapter à l'argument qu'on lui a passé cette fois-ci :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<?php
// src/Sdz/BlogBundle/Controller/BlogController.php

  public function menuAction($nombre) // Ici, nouvel argument $nombre, on l'a transmis via le render() depuis la vue
  {
    // On fixe en dur une liste ici, bien entendu par la suite on la récupérera depuis la BDD !
    // On pourra récupérer $nombre articles depuis la BDD,
    // avec $nombre un paramètre qu'on peut changer lorsqu'on appelle cette action
    $liste = array(
      array('id' => 2, 'titre' => 'Mon dernier weekend !'),
      array('id' => 5, 'titre' => 'Sortie de Symfony2.1'),
      array('id' => 9, 'titre' => 'Petit test')
    );

    return $this->render('SdzBlogBundle:Blog:menu.html.twig', array(
      'liste_articles' => $liste // C'est ici tout l'intérêt : le contrôleur passe les variables nécessaires au template !
    ));
  }

Avec sa vue associée :

1
2
3
4
5
6
7
8
9
{# src/Sdz/BlogBundle/Resources/views/Blog/menu.html.twig #}

<h3>Les derniers articles</h3>

<ul class="nav nav-pills nav-stacked">
  {% for article in liste_articles %}
    <li><a href="{{ path('sdzblog_voir', {'id': article.id}) }}">{{ article.titre }}</a></li>
  {% endfor %}
</ul>

Vous voulez voir des articles au lieu du message pas très drôle comme quoi il n'y a pas encore d'article ? Voici un tableau d'articles à ajouter temporairement dans la méthode indexAction(), que vous pouvez passer en paramètre à la méthode render(). C'est un tableau pour l'exemple, par la suite il faudra bien sûr récupérer les articles depuis la base de données.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php
// src/Sdz/BlogBundle/Controller/BlogController.php

// …

public function indexAction()
{
  // …

  // Les articles :
  $articles = array(
    array(
      'titre'   => 'Mon weekend a Phi Phi Island !',
      'id'      => 1,
      'auteur'  => 'winzou',
      'contenu' => 'Ce weekend était trop bien. Blabla…',
      'date'    => new \Datetime()),
    array(
      'titre'   => 'Repetition du National Day de Singapour',
      'id'      => 2,
      'auteur' => 'winzou',
      'contenu' => 'Bientôt prêt pour le jour J. Blabla…',
      'date'    => new \Datetime()),
    array(
      'titre'   => 'Chiffre d\'affaire en hausse',
      'id'      => 3, 
      'auteur' => 'M@teo21',
      'contenu' => '+500% sur 1 an, fabuleux. Blabla…',
      'date'    => new \Datetime())
  );

  // Puis modifiez la ligne du render comme ceci, pour prendre en compte nos articles :
  return $this->render('SdzBlogBundle:Blog:index.html.twig', array(
    'articles' => $articles
  ));
}

Rechargez la page, et profitez du résultat. ;) Si vous avez bien ajouté le CSS de Twitter, le résultat devrait ressembler à la figure suivante.

Le rendu de notre blog

Attention, on vient de définir des articles en brut dans le contrôleur, mais c'est uniquement pour l'exemple d'utilisation de Twig ! Ce n'est bien sûr pas du tout une façon correcte de le faire, par la suite nous les récupérerons depuis la base de données.

Blog/voir.html.twig

Il ressemble beaucoup à index.html.twig sauf qu'on passe à la vue une variable {{ article }} contenant un seul article, et non plus une liste d'articles. Voici un code par exemple :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{# src/Sdz/BlogBundle/Resources/views/Blog/voir.html.twig #}

{% extends "SdzBlogBundle::layout.html.twig" %}

{% block title %}
  Lecture d'un article - {{ parent() }}
{% endblock %}

{% block sdzblog_body %}

  <h2>{{ article.titre }}</h2>
  <i>Par {{ article.auteur }}, le {{ article.date|date('d/m/Y') }}</i>

  <div class="well">
    {{ article.contenu }}
  </div>

  <p>
    <a href="{{ path('sdzblog_accueil') }}" class="btn">
      <i class="icon-chevron-left"></i>
      Retour à la liste
    </a>
    <a href="{{ path('sdzblog_modifier', {'id': article.id}) }}" class="btn">
      <i class="icon-edit"></i>
      Modifier l'article
    </a>
    <a href="{{ path('sdzblog_supprimer', {'id': article.id}) }}" class="btn">
      <i class="icon-trash"></i>
      Supprimer l'article
    </a>
  </p>

{% endblock %}

Et l'adaptation du contrôleur bien évidemment :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
// src/Sdz/BlogBundle/Controller/BlogController.php

// …

public function voirAction($id)
{
  // …

  $article = array(
    'id'      => 1,
    'titre'   => 'Mon weekend a Phi Phi Island !',
    'auteur'  => 'winzou',
    'contenu' => 'Ce weekend était trop bien. Blabla…',
    'date'    => new \Datetime()
  );

  // Puis modifiez la ligne du render comme ceci, pour prendre en compte l'article :
  return $this->render('SdzBlogBundle:Blog:voir.html.twig', array(
    'article' => $article
  ));
}

La figure suivante représente le rendu de /blog/article/1.

Visualisation d'un article

Blog/modifier.html.twig et ajouter.html.twig

Ceux-ci contiennent une inclusion de template. En effet, rappelez-vous, j'avais pris l'exemple d'un formulaire utilisé pour l'ajout, mais également la modification. C'est notre cas ici, justement. Voici donc le fichier modifier.html.twig :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{# src/Sdz/BlogBundle/Resources/views/Blog/modifier.html.twig #}

{% extends "SdzBlogBundle::layout.html.twig" %}

{% block title %}
  Modifier un article - {{ parent() }}
{% endblock %}

{% block sdzblog_body %}

  <h2>Modifier un article</h2>

  {% include "SdzBlogBundle:Blog:formulaire.html.twig" %}

  <p>
    Vous éditez un article déjà existant,
    ne le changez pas trop pour éviter
    aux membres de ne pas comprendre
    ce qu'il se passe.
  </p>

  <p>
    <a href="{{ path('sdzblog_voir', {'id': article.id}) }}" class="btn">
      <i class="icon-chevron-left"></i>
      Retour à l'article
    </a>
  </p>

{% endblock %}

Le template ajouter.html.twig lui ressemble énormément, je vous laisse donc le faire.

Quant à formulaire.html.twig, on ne sait pas encore le faire, car il demande des notions de formulaire, mais faisons déjà sa structure pour le moment :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{# src/Sdz/BlogBundle/Resources/views/Blog/formulaire.html.twig #}

{# Cette vue n'hérite de personne, elle sera incluse par d'autres vues qui, elles, hériteront probablement du layout #}
{# Je dis « probablement » car, ici pour cette vue, on n'en sait rien et c'est une info qui ne nous concerne pas #}

<h3>Formulaire d'article</h3>

{# Ici on laisse vide la vue pour l'instant, on la comblera plus tard lorsqu'on saura afficher un formulaire #}
<div class="well">
  Ici se trouvera le formulaire.
</div>

Une chose importante ici : dans ce template, il n'y a aucune notion de bloc, d'héritage, etc. Ce template est un électron libre : vous pouvez l'inclure depuis n'importe quel autre template.

Et, bien sûr, il faut adapter le contrôleur pour passer la variable article :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
// src/Sdz/BlogBundle/Controller/BlogController.php

  public function modifierAction($id)
  {
    // Ici, on récupérera l'article correspondant à $id

    // Ici, on s'occupera de la création et de la gestion du formulaire

    $article = array(
      'id'      => 1,
      'titre'   => 'Mon weekend a Phi Phi Island !',
      'auteur'  => 'winzou',
      'contenu' => 'Ce weekend était trop bien. Blabla…',
      'date'    => new \Datetime()
    );

    // Puis modifiez la ligne du render comme ceci, pour prendre en compte l'article :
    return $this->render('SdzBlogBundle:Blog:modifier.html.twig', array(
      'article' => $article
    ));
  }

Ainsi, /blog/modifier/1 nous donnera la figure suivante.

Modification d'un article

Pour conclure

Et voilà, nous avons généré presque tous nos templates. Bien sûr, ils sont encore un peu vides, car on ne sait pas utiliser les formulaires ni récupérer les articles depuis la base de données. Mais vous savez maintenant les réaliser et c'était une étape importante ! Je vais vous laisser créer les templates manquants ou d'autres afin que vous vous fassiez la main. Bon code !

Cela termine ce chapitre : vous savez afficher avec mise en forme le contenu de votre site. Vous avez maintenant presque toutes les billes en main pour réaliser un site internet. Bon, OK, c'est vrai, il vous manque encore des concepts clés tels que les formulaires, la base de données, etc. Mais vous maîtrisez pleinement la base du framework Symfony2, et apprendre ces prochains concepts sera bien plus facile !

Pour plus d'informations concernant Twig et ses possibilités, n'hésitez pas à lire la documentation officielle.


En résumé

  • Un moteur de templates tel que Twig permet de bien séparer le code PHP du code HTML, dans le cadre de l'architecture MVC ;
  • La syntaxe {{ var }} affiche la variable var ;
  • La syntaxe {% if %} fait quelque chose, ici une condition ;
  • Twig offre un système d'héritage (via {% extends %}) et d'inclusion (via {% include %} et {% render %}) très intéressant pour bien organiser les templates ;
  • Le modèle triple héritage est très utilisé pour des projets avec Symfony2.