PHP est né comme un langage de gabarits, et il le reste : n'importe quel fichier peut mêler du HTML et des instructions d'affichage sans aucune bibliothèque. Cette simplicité explique que beaucoup de projets écrivent leurs vues directement en PHP, et elle explique aussi les défauts qu'on y trouve invariablement : de la logique métier au milieu du balisage, des variables globales qui apparaissent de nulle part, et surtout des valeurs affichées sans échappement. Écrire un système de gabarits en PHP pur consiste à poser une fine couche au dessus du langage pour corriger ces trois points, et cela tient en moins de cent cinquante lignes, sans aucune dépendance ni étape de compilation. Le résultat n'a rien à envier aux moteurs existants sur un projet dont les vues sont écrites par des développeurs.

Ce qu'on cherche à obtenir

Avant d'écrire, il faut savoir ce qui distingue un gabarit correct d'un fichier de vue ordinaire. Trois propriétés suffisent, elles se tiennent les unes les autres, dans le prolongement de l'organisation décrite dans notre article sur la manière de structurer un projet PHP sans framework.

Une portée de variables maîtrisée

Un fichier de vue inclus directement hérite de tout ce qui existe dans la fonction appelante, ce qui produit des gabarits qui fonctionnent par accident et cessent brutalement de fonctionner quand on les appelle depuis un autre endroit du code. Le rendu doit se faire dans une fonction dédiée, qui n'expose au gabarit que les données explicitement passées. Cette isolation est la première chose à écrire, et c'est elle qui rend un gabarit réutilisable, testable et compréhensible sans connaître le code qui l'appelle.

Un échappement par défaut

Le défaut de PHP est que l'affichage brut est le plus court à écrire, et l'affichage échappé le plus long, ce qui garantit que l'échappement sera oublié un jour. Un système de gabarits corrige cela en fournissant une fonction d'affichage courte qui échappe, réservant l'affichage brut à une fonction dont le nom dit clairement ce qu'elle fait. Le renversement de cette facilité est le seul moyen fiable d'éviter les failles d'injection dans les vues, la vigilance individuelle échouant systématiquement au bout de quelques mois.

Un héritage de mise en page

Sans mécanisme d'héritage, chaque vue commence par inclure un en tête et se termine par inclure un pied de page, ce qui interdit à la mise en page d'entourer réellement le contenu et complique tout ce qui doit être injecté dans l'en tête depuis la vue. L'héritage inverse la relation : la vue déclare la mise en page qu'elle utilise et remplit des blocs nommés, la mise en page décidant où chacun s'affiche et ce qui se passe quand il est vide.

Ce qu'on ne cherche pas

Il ne s'agit pas de réécrire un moteur avec sa propre syntaxe, ses filtres et sa compilation. Ces outils existent, sont excellents, et se justifient pleinement sur les projets où des personnes non développeuses écrivent ou modifient les gabarits. Sur un projet où les vues sont écrites par ceux qui écrivent le code, la couche mince décrite ici offre l'essentiel des bénéfices sans introduire un langage supplémentaire à apprendre, ni une étape de compilation à gérer au déploiement, ni un cache supplémentaire à invalider.

Le critère de décision

La question à se poser est simple : qui écrit les gabarits ? Si la réponse inclut des personnes qui ne doivent pas pouvoir exécuter du code arbitraire, un moteur avec sa propre syntaxe s'impose, car il est le seul à offrir un bac à sable réellement étanche. Si les gabarits restent entre les mains de l'équipe technique, PHP fait très bien le travail et supprime une dépendance externe, un cache à invalider et une syntaxe supplémentaire à documenter pour les nouveaux arrivants.

La question de l'accessibilité et du balisage

Un système de gabarits est aussi l'endroit où se jouent des choix qui engagent le site entier, et qu'il vaut mieux poser dès la mise en place plutôt que de rattraper page par page. La hiérarchie des titres, par exemple, dépend à la fois de la mise en page et de la vue : si les deux déclarent librement leurs niveaux, le site finit avec plusieurs titres de premier niveau sur certaines pages et des sauts de niveau ailleurs. Une convention simple, la mise en page ne posant jamais de titre et la vue déclarant le sien, règle le problème définitivement. Le même raisonnement vaut pour les points de repère structurels, l'en tête, la navigation, le contenu principal et le pied de page, qui doivent être déclarés une fois dans la mise en page et jamais dupliqués dans les vues. Ces décisions ne coûtent rien au moment de la conception et deviennent très coûteuses à corriger sur quinze cents pages.

Gabarit enfant remplissant les blocs définis par une mise en page parente

Écrire le noyau

Le cœur du système tient en une classe et quelques fonctions d'aide, toutes très courtes. Sa taille réduite est un objectif en soi, car un moteur de gabarits maison qui grossit finit par reproduire un moteur existant en moins bien, ce qui rejoint le raisonnement exposé dans notre article sur la façon d'injecter ses dépendances sans conteneur.

La fonction de rendu isolée

Le rendu s'effectue dans une méthode qui extrait les données passées en variables locales, met en tampon la sortie, inclut le fichier, puis récupère et retourne le contenu du tampon. Cette poignée de lignes fournit à elle seule l'isolation de portée, la possibilité de traiter le résultat avant de l'envoyer, et celle d'imbriquer un rendu dans un autre sans effet de bord. Le tampon doit être vidé proprement en cas d'exception, sans quoi une erreur survenue au milieu d'un gabarit laisse une sortie partielle collée au message d'erreur, produisant une page illisible et parfois une fuite d'information technique.

Résoudre le chemin des gabarits

Le nom d'un gabarit doit être une chaîne courte, sans extension ni chemin absolu, transformée en chemin réel par le système. Cette indirection permet de réorganiser les fichiers sans toucher au moindre appel, et surtout de refuser tout nom contenant une séquence de remontée de dossier. C'est un point de sécurité réel dès que le nom du gabarit peut dépendre, même indirectement, d'une donnée reçue de l'extérieur, cas plus fréquent qu'il n'y paraît sur les sites à gabarits multiples.

L'échappement et ses contextes

Échapper pour le HTML ne suffit pas : une valeur insérée dans un attribut, dans une adresse, dans un bloc de script ou dans une feuille de style demande un traitement différent. Un système honnête fournit donc une fonction par contexte, avec des noms explicites, et documente laquelle employer où. En pratique, quatre fonctions suffisent, et la plus utilisée d'entre elles doit être la plus courte à écrire pour que personne ne soit tenté de l'éviter. Il faut également décider de l'encodage et le passer explicitement à chaque appel, la valeur par défaut ayant changé selon les versions du langage, ce qui a produit par le passé des échappements silencieusement inopérants.

Les blocs et leur remplissage

Un bloc s'ouvre par un appel qui démarre un tampon, et se ferme par un appel qui stocke le contenu sous un nom. La mise en page affiche ensuite ces blocs par leur nom, avec une valeur par défaut lorsque le bloc n'a pas été rempli par la vue. Ce mécanisme représente une vingtaine de lignes à peine et couvre tous les besoins courants d'un site de contenu : titre, contenu principal, colonne latérale, scripts de pied de page, éléments à injecter dans l'en tête.

L'héritage proprement dit

La vue déclare la mise en page qu'elle utilise par un appel placé en tête de fichier, qui se contente d'enregistrer un nom sans rien afficher. À la fin du rendu de la vue, le système constate qu'une mise en page a été demandée, place le contenu produit dans un bloc réservé, et rend la mise en page à son tour. La récursion est naturelle, une mise en page pouvant elle même en étendre une autre, et il suffit de limiter la profondeur d'héritage à quelques niveaux pour éviter une boucle infinie en cas d'erreur de déclaration.

Besoin Mécanisme Sans lui
Isolation des variables Rendu dans une méthode dédiée Gabarits dépendant du contexte d'appel
Sécurité de l'affichage Fonction d'échappement courte Injections dans les vues
Mise en page commune Héritage et blocs nommés Inclusions d'en tête et de pied
Fragments réutilisables Rendu imbriqué de gabarits Duplication de balisage
Injection dans l'en tête Bloc rempli depuis la vue Variables globales
Gestion des erreurs Nettoyage du tampon sur exception Sortie partielle illisible

Les règles d'écriture des gabarits

Le noyau ne suffit pas : sans discipline d'écriture, les gabarits redeviennent en quelques mois ce qu'ils étaient. Quatre règles couvrent l'essentiel, et elles se vérifient toutes par une simple relecture.

Aucune requête dans un gabarit

Un gabarit affiche des données, il ne va jamais les chercher lui même. Une requête de base ou un appel à un service dans une vue produit un nombre d'accès imprévisible, rend le rendu impossible à tester et disperse la logique. Les données doivent être rassemblées avant l'appel au rendu, dans le contrôleur ou dans un objet dédié, et passées au gabarit sous la forme d'une structure unique et explicite. C'est la règle la plus souvent violée, et celle dont le respect a le plus d'effet sur la maintenabilité comme sur la performance, une boucle qui interroge la base à chaque itération étant la cause la plus fréquente des pages lentes.

Pas de logique métier, mais de la logique d'affichage

La distinction mérite d'être posée clairement, car l'interdiction absolue de toute condition dans un gabarit est irréaliste et n'est respectée nulle part. Décider si un bloc s'affiche, mettre en forme une date, choisir un pluriel, tronquer un texte trop long, alterner une classe sur une ligne sur deux relèvent de l'affichage et ont toute leur place dans un gabarit. Calculer un prix, décider d'un statut, filtrer ou trier une liste relèvent du métier et doivent être faits avant l'appel au rendu. Le test pratique consiste à se demander si la même donnée affichée ailleurs, dans un courriel ou dans un export, demanderait exactement le même calcul : si oui, ce calcul n'a rien à faire dans un gabarit.

Une syntaxe alternative pour les structures

PHP propose une écriture des conditions et des boucles utilisant deux points et un mot clé de fermeture explicite, qui se lit bien mieux au milieu du balisage que les accolades. L'adopter partout dans les vues, et interdire les accolades par convention, rend les gabarits nettement plus lisibles, en particulier lorsque plusieurs niveaux d'imbrication se referment au même endroit, cas où une suite d'accolades ne dit plus rien à personne.

Des gabarits courts et nombreux

Un gabarit de six cents lignes est illisible, impossible à réutiliser et pénible à modifier à plusieurs. Le découper en fragments, une carte de produit, un bloc d'avis, un élément de navigation, rend chaque morceau compréhensible et réutilisable. Le coût de cette division se limite à un appel de rendu supplémentaire par fragment, parfaitement négligeable, et le bénéfice est considérable dès qu'une modification doit s'appliquer partout où un même élément apparaît, ce qui arrive à chaque évolution du design.

Tester les gabarits

Un gabarit correctement isolé se teste comme n'importe quelle fonction, ce qui est un bénéfice inattendu de cette architecture. Le test consiste à appeler le rendu avec un jeu de données connu et à vérifier quelques propriétés du résultat obtenu : présence d'une chaîne attendue, absence d'une valeur qui aurait dû être filtrée, nombre d'éléments dans une liste. Ces tests s'exécutent en quelques millisecondes puisqu'ils ne touchent ni la base de données ni le réseau, et ils attrapent une catégorie d'erreurs que rien d'autre ne détecte, notamment les régressions d'échappement. Un test qui passe une chaîne contenant des chevrons et vérifie qu'ils ressortent échappés vaut toutes les relectures de code, et se pose une fois pour l'ensemble des gabarits sous forme de test générique appliqué à chacun.

Répartition du temps de génération d'une page sur un projet PHP fait main
Requêtes de données
54 %
Traitement métier
21 %
Appels à des services externes
17 %
Rendu des gabarits
5 %
Démarrage et assemblage
3 %

Décomposition mesurée par profilage sur un projet de taille moyenne. La couche de gabarits, souvent suspectée en premier, représente la plus petite part du temps de génération.

Performance et déploiement

Un système de gabarits en PHP pur a un avantage rarement mis en avant : il n'introduit aucune étape supplémentaire entre le code écrit et le code exécuté, ce qui simplifie considérablement le déploiement décrit dans notre article sur la manière de séparer configuration, code et contenu.

Aucune compilation à gérer

Les moteurs dotés de leur propre syntaxe traduisent leurs gabarits en PHP et mettent le résultat en cache sur le disque, ce qui impose de gérer ce cache au déploiement, de le vider, de s'assurer que le dossier est accessible en écriture, et de diagnostiquer les cas où il sert une version périmée. Un gabarit en PHP pur est déjà du PHP : il bénéficie directement du cache d'opcodes du serveur, sans aucune configuration, sans dossier temporaire et sans risque de servir une version périmée après un déploiement.

Le coût réel du rendu

La mise en tampon et l'extraction des variables ont un coût, mais il se compte en microsecondes et devient négligeable dès que la page contient la moindre requête. Sur les projets où nous avons mesuré, le rendu complet d'une page représente moins de cinq pour cent du temps de génération, très loin derrière l'accès aux données et les appels à des services externes. Optimiser cette couche est donc rarement pertinent, et la garder simple vaut mieux que la rendre rapide, d'autant qu'un moteur simple se relit et se corrige en quelques minutes.

Ce qu'il faut mettre en cache

Quand la performance devient réellement un sujet, ce n'est pas le rendu qu'il faut chercher à accélérer mais son résultat qu'il faut conserver. Mettre en cache le fragment produit par un gabarit coûteux, une navigation à plusieurs niveaux ou une liste de contenus liés par exemple, apporte bien plus que n'importe quelle optimisation du moteur lui même. La clé de cache doit inclure tout ce qui fait varier le rendu, faute de quoi un visiteur se voit servir le fragment destiné à un autre, incident classique sur les blocs qui dépendent d'une connexion.

Les erreurs et leur affichage

Une erreur survenue dans un gabarit doit produire, en production, une page d'erreur propre et une entrée détaillée dans le journal applicatif, jamais un message technique au milieu d'une page à moitié rendue. Cela suppose que le tampon soit vidé avant l'affichage de la page d'erreur, ce qui est exactement le rôle du nettoyage sur exception évoqué plus haut. En développement, le comportement inverse est souhaitable, avec la trace d'exécution complète, la ligne concernée et le nom du gabarit fautif affichés à l'écran, information que le système doit conserver au moment de l'inclusion, l'exception levée depuis un fichier inclus ne mentionnant pas toujours le gabarit d'origine. Le plus simple consiste à empiler le nom du gabarit courant dans une variable dédiée au moment de l'inclusion et à la dépiler ensuite, mécanisme d'une dizaine de lignes qui rend chaque erreur immédiatement localisable.