Programmation PHP avec Symfony/Twig
Installation
modifierTwig est un moteur de templates pour le langage de programmation PHP, utilisé par défaut par le framework Symfony. Son livre officiel faisant 156 pages[1], la présente pas aura plutôt un rôle d'aide mémoire et d'illustration.
Pour exécuter du code sans installer Twig, il existe https://twigfiddle.com/.
composer require symfony/templating
Anciennement sur Symfony 3 :
composer require twig/twig
Syntaxe native
modifierLes mots réservés suivants s'ajoutent au HTML déjà interprété :
- {{ ... }} : appel à une variable ou une fonction PHP, ou un template Twig parent (
{{ parent() }}
). - {# ... #} : commentaires.
- {% ... %} : commande, comme une affectation, une condition, une boucle ou un bloc HTML.
- {% set foo = 'bar' %} : assignation[2].
- {% if (i is defined and i == 1) or j is not defined or j is empty %} ... {% endif %} : condition.
- {% for i in 0..10 %} ... {% endfor %} : compteur dans une boucle.
- ' : caractère d'échappement.
Chaines de caractères
modifierConcaténation
modifierIl existe de multiples manière de concaténer des chaines[3]. Par exemple avec l'opérateur de concaténation ou par interpolation :
"{{ variable1 ~ variable2 }}" "#{variable1} #{variable2}"
Les apostrophes ne fonctionnent pas avec l'interpolation.
Tableaux
modifierCréation
modifierPour créer un tableau itératif :
{% set myArray = [1, 2] %}
Un tableau associatif :
{% set myArray = {'key': 'value'} %}
À plusieurs lignes :
{% set months = {
1: 'janvier',
2: 'février',
3: 'mars',
} %}
{{ dump(months[1]) }} {# 'janvier' #}
Ajouter une ligne :
{% set months = months|merge({4: 'avril'}) %}
Ajouter une ligne avec clé variable :
{% set key = 5 %}
{% set months = months|merge({(key): 'mai'}) %}
Ajouter une ligne en préservant les clés numériques :
{% set key = 6 %}
{% set months = months + {(key): 'juin'} %}
Multidimensionnel :
{% set myArray = [
{'key1': 'value1'},
{'key2': 'value2'}
] %}
Dans un "for ... in", pour séparer chaque élément avec une virgule :
{% if loop.first != true %}
,
{% endif %}
Pour créer un tableau associatif JavaScript à partir d'un tableau Twig :
<script type="text/javascript">
const monTableauJs = JSON.parse('{{ monTableauTwig |json_encode |raw }}');
for (const maLigneJs in monTableauJs) {
console.log(maLigneJs);
console.log(monTableauJs[maLigneJs]);
}
</script>
Modification d'une ligne
modifierPour modifier une ligne, utiliser "merge()"[4]. Ex :
{% set tests = {'a': 1} %} {% set tests = tests|merge({'b': 2}) %} {{ dump(tests) }} {% set tests = tests|merge({'b': 3}) %} {{ dump(tests) }}
array:2 [▼ "a" => 1 "b" => 2 ] array:2 [▼ "a" => 1 "b" => 3 ]
La clé de la ligne ne doit pas être numérique (même convertie en chaine) sinon Twig modifie les clés, donc cela ajoute une ligne :
{% set tests = {'1': 1} %} {% set tests = tests|merge({'2': 2}) %} {{ dump(tests) }} {% set tests = tests|merge({'2': 3}) %} {{ dump(tests) }}
array:2 [▼ 0 => 1 1 => 2 ] array:3 [▼ 0 => 1 1 => 2 2 => 3 ]
Modification des lignes
modifierPour ajouter une ou plusieurs lignes à un tableau, utiliser "merge()" aussi :
{% set oldArray = [1] %}
{% set newArray = oldArray|merge([2,3]) %}
{{ dump(newArray) }}
0 => 1 1 => 2 2 => 3
Pour ajouter une ligne associative :
{% set oldArray = {'key1': 'value1'} %}
{% set newArray = oldArray|merge({'key2': 'value2'}) %}
{{ dump(newArray) }}
[ "key1" => "value1" "key2" => "value2" ]
Pour ajouter une ligne de sous-tableau :
{% set oldArray = [{'key1': 'value1'}] %}
{% set newArray = oldArray|merge([{'key2': 'value2'}]) %}
{{ dump(newArray) }}
[ 0 => ["key1" => "value1"] 1 => ["key2" => "value2"] ]
Lecture
modifierPour savoir si une variable est un tableau :
if my_array is iterable
Pour savoir :
- si un tableau est vide, utiliser empty comme pour les chaines de caractères. Par exemple pour savoir si un tableau est vide ou null :
my_array is empty
- la taille du tableau :
my_array |length
- si un élément est dans un tableau :
my_item in my_array
- si un élément n'est pas dans un tableau :
my_item not in my_array
- si un élément est dans les clés d'un tableau :
my_item in my_array|keys
Pour filtrer le tableau, utiliser filter[5]. Par exemple pour savoir si un tableau multidimensionnel a ses sous-tableaux vides :
my_array|filter(v => v is not empty) is empty
Précédence des opérateurs
modifierDu moins au plus prioritaire[6] :
Opérateur | Rôle |
---|---|
b-and | Et booléen |
b-xor | Ou exclusif |
b-or | Ou booléen |
or | Ou |
and | Et |
== | Est-il égal |
!= | Est-il différent |
< | Inférieur |
> | Supérieur |
>= | Supérieur ou égal |
<= | Inférieur ou égal |
in | Dans (ex : {% if x in [1, 2] %} )
|
matches | Correspond |
starts with | Commence par |
ends with | Se termine par |
.. | Séquence (ex : 1..5 )
|
+ | Plus |
- | Moins |
~ | Concaténation |
* | Multiplication |
/ | Division |
// | Division arrondie à l'inférieur |
% | Modulo |
is | Test (ex : is defined ou is not empty )
|
** | Puissance |
| | Filtre |
[] | Entrée de tableau |
. | Attribut ou méthode d'un objet (ex : country.name )
|
Pour afficher la valeur NULL dans un opérateur ternaire, il faut la mettre entre apostrophes :
{{ (myVariable is not empty) ? '"' ~ myVariable.value ~ '"' : 'null' }}
Fonctions usuelles
modifierChemins, routes et URLs
modifierurl('route_name')
: affiche l'URL complète d'une route. Les paramètres GET peuvent être ajoutés dans un tableau ensuite (ex :url('ma_route_de_controleur', {'parametre1': param1})
).absolute_url('path')
: affiche l'URL complète d'un chemin.path('route_name')
: affiche le chemin, en absolu par défaut, mais il existe le paramètrerelative=true
. Les paramètres GET peuvent être ajoutés dans un tableau ensuite (ex :path('ma_route_de_controleur', {'parametre1': param1}
).asset('path')
: pointe le dossier des "assets" ("web" dans SF2, "public" dans SF4). Ex :<img src="{{ asset('images/mon_image.png') }}" />
.controller('controller_name')
: exécute la méthode d'un contrôleur. Ex :{{ render(controller('App\\Controller\\DefaultController:indexAction')) }}
.
absolute_url()
renvoie l'URL de l'application si l'appel provient d'un contrôleur, mais http://localhost s'il vient d'une commande (CLI)[7]. La solution est donc de définir l'URL de l'environnement dans une variable, soit default_uri
de routing.yaml, soit maison et injectée par le contrôleur dans le Twig.
Divers
modifierconstant(constant_name)
: importe une constante d'une classe PHP[10].attribute(object, method)
: accède à l'attribut d'un objet PHP. C'est équivalent au "." mais la propriété peut être dynamique[11].date()
: convertit en date, ce qui permet leur comparaison. Ex :{% if date(x) > date(y) %}
. NB : comme en PHP, "d/m/Y" correspond au format "jj/mm/aaaa".- min() : renvoie le plus petit nombre de ceux en paramètres (ou dans un tableau en paramètre 1).
- max() : renvoie le plus grand nombre de ceux en paramètres (ou dans un tableau en paramètre 1).
Filtres
modifierLes filtres fournissent des traitements sur une expression, si on les place après elle séparés par des pipes. Par exemple :
capitalize
: équivaut au PHPucfirst()
, met une majuscule à la première lettre d'une chaine de caractères, et passe les autres en minuscules.upper
: équivaut au PHPstrtoupper()
, met la chaine en lettres capitales. Exemple pour ne mettre la majuscule que sur la première lettre :{{ variable[:1]|upper ~ variable[1:] }}
.first
: affiche la première ligne d'un tableau, ou la première lettre d'une chaine.length
: équivaut au PHPsizeof()
, renvoie la taille de la variable (chaine ou tableau).format
: équivaut au PHPprintf()
.date
: équivaut au PHPdate()
mais son format est du type DateInterval[12].date_modify
: équivaut au PHP DateTime->modify(). Ex :{% set tomorrow = 'now'|date_modify("+1 day") %}
.replace
: équivaut au PHPstr_replace()
. Ex :{{ 'Mon titre %tag%.'|replace({'%tag%': '1'}) }}
.join
: équivaut au PHPimplode()
: convertit un tableau en chaine avec un séparateur en paramètre.split
: équivaut au PHPexplode()
: convertit une chaine en tableau avec un séparateur en paramètre.slice(début, fin)
: équivaut au PHParray_slice()
+substr()
: découpe un tableau ou une chaine selon deux positions[13].trim
: équivaut au PHPtrim()
.raw
: ne pas échapper les balises HTML.json_encode
: transforme un tableau en chaine de caractères JSON.default
: ce filtre lève les exceptions sur les variables non définies ou vides[14]. Ex :
{{ variable1 |default(null) }}
Variables spéciales
modifierloop
contient les informations de la boucle dans laquelle elle se trouve. Par exempleloop.index
donne le nombre d'itérations déjà survenue (commence par 1 et pas par 0).- Les variables globales commencent par des underscores, par exemple[15] :
_route
: partie de l'URL située après le domaine._self
: nom de du fichier courant._charset
: jeu de caractères de la page. Ex : UTF-8._context
: variables injectées dans le template. Cela peut donc permettre d'y accéder en variables variables. Ex :{{ attribute(_context, 'constante'~variable) }}
{{ attribute(form, 'constante'~variable) }}
pour un champ de formulaire.
- Les variables d'environnement CGI, telles que
{{ app.request.server.get('SERVER_NAME') }}
Pour obtenir la route d'une page : {{ path(app.request.attributes.get('_route'), app.request.attributes.get('_route_params')) }}
L'URL courante : {{ app.request.uri }}
La page d'accueil du site Web : url('homepage')
app.environment
renvoie la valeur de APP_ENV.
Gestion des espaces
modifierspaceless
modifierUn Twig bien formaté ne correspond pas forcément au rendu qu'il doit apporter. Pour supprimer les espaces du formatage dans ce rendu :
{% apply spaceless %}
<b>
Hello World!
</b>
{% endspaceless %}
NB : en Twig < 2.7, c'était[16] :
{% spaceless %}
{% autoescape false %}
<b>
Hello World!
</b>
{% endspaceless %}
Par ailleurs, il existe un filtre |spaceless[17].
-
modifierDe plus, on peut apposer le symboles "-" aux endroits où ignorer les espacements (dont retours chariot) du formatage :
Hello {% ... -%}
{%- ... %} World!
Cela fonctionne aussi entre {{- -}}
.
Utilisation du traducteur
modifierConfiguration
modifierLe module de traduction Symfony s'installe avec :
composer require translator
Quand une page peut apparaitre dans plusieurs langues, inutile d'injecter la locale dans le Twig depuis le contrôleur PHP, c'est une variable d'environnement que l'on peut récupérer avec :
{{ app.request.getLocale() }}
{{ app.request.get('mon_query_param') }}
Le fichier YAML contenant les traductions dans cette langue sera automatiquement utilisé s'il est placé dans le dossier "translations" apparu lors de l'installation. En effet, il est identifié par le code langue ISO de son suffixe (ex : le Twig de la page d'accueil pourra être traduit dans homepage.fr.yml, homepage.en.yml, etc.).
Pour définir le préfixe des YAML auquel un Twig fera appel, on le définit sans suffixe en début de fichier Twig :
{% trans_default_domain 'homepage' %}
Par ailleurs, la commande PHP pour lister les traductions les traductions d'une langue est[18] :
php bin/console debug:translation en --only-unused // Pour les inutilisées
php bin/console debug:translation en --only-missing // Pour les manquantes
Filtre trans
modifierUne fois la configuration effectuée, on peut apposer le filtre trans
aux textes traduis dans le Twig.
{{ MessageInMyLanguage |trans }}
Parfois, il peut être utile de factoriser les traductions de plusieurs Twig dans un seul YAML. Pour piocher dans un YAML qui n'est pas celui par défaut, il suffit de le nommer en second paramètre du filtre trans
:
{{ 'punctuation_separator'|trans({}, 'common') }}
Si le YAML contient des balises HTML à interpréter, il faut apposer le filtre raw
après trans
.
Si une variable doit apparaitre dans une langue différente de celle de l'utilisateur, on le précisera dans le troisième paramètre du filtre trans
:
{{ FrenchMessage |trans({}, 'common', 'fr') }}
Si le YAML doit contenir une variable, on la place entre pourcentages pour la remplacer en Twig avec le premier paramètre du filtre trans
:
{{ variableMessage |trans({"%price%": formatPrice(myPrice)}) }}
Si la clé à traduire doit être variable, on ne peut pas réaliser la concaténation dans la même commande que la traduction : il faut décomposer en deux lignes :
{% set variableMessage = 'constante.' ~ variable %}
{{ variableMessage |trans }}
Opération trans
modifierIl existe aussi une syntaxe alternative au filtre. Par exemple les deux paragraphes ci-dessous sont équivalents :
{{ 'punctuation_separator'|trans({}, 'common') }}
{% trans from 'common' %}
punctuation_separator
{% endtrans %}
De plus, on peut injecter une variable avec "with". Voici deux équivalents :
{{ 'Bonjour %name% !' |trans({"%name%": name}) }}
{% trans with {'%name%': name}%}Bonjour %name% !{% endtrans %}
Méthodes PHP appelables en Twig
modifierEn PHP, on peut définir des fonctions invocables en Twig, sous forme de fonction ou de filtre selon la méthode parente surchargée. Exemple :
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
class TwigExtension extends AbstractExtension
{
public function getFilters(): array
{
return [
new TwigFilter('getPrice', [$this, 'getPrice']),
];
}
public function getFunctions(): array
{
return [
new TwigFunction('getPrice', [$this, 'getPrice']),
];
}
public function getPrice($value): string
{
return number_format($value, 2, ',', ' ') . ' €';
}
}
Héritages et inclusions
modifierextends
modifierSi une fichier appelé doit être inclus dans un tout, il doit en hériter avec le mot extends
. Le cas typique est celui d'une "base.html.twig" qui contient l'en-tête et le pied de page HTML commun à toutes les pages d'un site. Ex :
{% extends "base.html.twig" %}
Twig ne supporte pas l'héritage multiple[19].
Il est possible de surcharger totalement ou en partie les blocs du template parent. Exemple depuis le template qui hérite :
{% block header %}
Mon en-tête qui écrase le parent
{% endblock %}
{% block footer %}
Mon pied de page qui complète le parent
{{ parent() }}
{% endblock %}
include
modifierÀ contrario, si un fichier doit en inclure un autre (par exemple pour qu'un fragment de vue soit réutilisable dans plusieurs pages), on utilise le mot include
. Ex :
{% include("partials/footer.html.twig") %}
En lui injectant des paramètres :
{% include("partials/footer.html.twig") with {'clé': 'valeur'} %}
embed
modifierEnfin, embed
combine les deux précédentes fonctions :
{% embed "footer.html.twig" %}
...
{% endembed %}
import
modifierimport
récupère certaines fonctions d'un fichier en contenant plusieurs :
{% from 'mes_macros.html' import format_price as price, format_date %}
Macros
modifierLes macros sont des fonctions globales, appelables depuis un fichier Twig[22].
Exemple :
{% macro format_price(price, currency = '€') %}
{% set locale = (app.request is null) ? 'fr_FR' : app.request.locale %}
{% if locale == 'fr_FR' %}
{{ price|number_format(2, ',', ' ') }} {{ currency }}
{% else %}
{{ price|number_format(2, '.', ' ') }}{{ currency }}
{% endif %}
{% endmacro %}
Lors de l'appel, les paramètres nommés ne fonctionnent que si 100 % des paramètres appelés le sont.
Exemples
modifier{% extends "base.html.twig" %}
{% block navigation %}
<ul id="navigation">
{% for item in navigation %}
<li>
<a href="{{ item.href }}">
{% if item.level == 2 %} {% endif %}
{{ item.caption|upper }}
</a>
</li>
{% endfor %}
</ul>
{% endblock navigation %}
Pour ne pas qu'un bloc hérité écrase son parent, mais l'incrémente plutôt, utiliser :
{{ parent() }}
Bonnes pratiques
modifierLes noms des fichiers .twig doivent être rédigés en snake_case[23].
Références
modifier- ↑ https://twig.symfony.com/pdf/2.x/Twig.pdf
- ↑ https://twig.sensiolabs.org/doc/2.x/tags/set.html
- ↑ https://www.designcise.com/web/tutorial/how-to-concatenate-strings-and-variables-in-twig
- ↑ https://twig.symfony.com/doc/3.x/filters/merge.html
- ↑ https://twig.symfony.com/doc/3.x/filters/filter.html
- ↑ http://twig.sensiolabs.org/doc/templates.html
- ↑ https://stackoverflow.com/questions/73026340/absolute-url-in-template-returns-localhost-in-email-templates
- ↑ https://symfony.com/doc/current/reference/twig_reference.html
- ↑ https://symfony.com/doc/current/http_cache/esi.html
- ↑ https://twig.symfony.com/doc/2.x/functions/constant.html
- ↑ https://twig.symfony.com/doc/2.x/functions/attribute.html
- ↑ https://twig.symfony.com/doc/3.x/filters/date.html
- ↑ https://twig.symfony.com/doc/3.x/filters/slice.html
- ↑ https://twig.symfony.com/doc/2.x/filters/default.html
- ↑ https://twig.symfony.com/doc/3.x/templates.html#global-variables
- ↑ https://twig.symfony.com/doc/2.x/tags/spaceless.html
- ↑ https://twig.symfony.com/doc/2.x/filters/spaceless.html
- ↑ https://symfony.com/doc/current/translation/debug.html
- ↑ https://twig.symfony.com/doc/3.x/tags/extends.html
- ↑ https://twig.symfony.com/doc/1.x/functions/include.html
- ↑ https://twig.symfony.com/doc/2.x/tags/include.html
- ↑ https://twig.symfony.com/doc/2.x/tags/macro.html
- ↑ https://symfony.com/doc/current/contributing/code/standards.html