PEJO Framework : moteur de templates (classe TemplateEngine)

Première étape dans la compréhension du framework PEJO, et pierre angulaire de tout cet édifice complexe, le moteur de templates permet la séparation du code PHP et du code HTML, et ce afin de simplifier un maximum le développement, et d'optimiser la répartition du travail entre intégrateur et développeur. Voyons donc le fonctionnement de cette classe centrale.
A noter que pour une meilleur compréhension de l'utilité des moteurs de templates, je vous invite à (re)lire mon article "Création d’un moteur de templates en PHP 5 objet".
La première partie de cette article comprendra la documentation de la classe, ainsi qu'un exemple d'utilisation. La deuxième, réservée aux développeurs souhaitant m'aider à optimiser cette classe, expliquera en détail l'intérieur des différentes méthodes. Elle ne sera en rien nécessaire à l'utilisation de ce framework.
Télécharger la classe TemplateEngine, moteur de templates du framework PEJO
Méthodes de la classe TemplateEngine
Cette classe contient différentes méthodes. Nous ne verrons ici, dans cette partie réservée aux utilisateurs du framework PEJO, uniquement les méthodes accessibles depuis l'extérieur, et requise pour la manipulation quotidienne de la classe.
Constructeur :
Tout d'abord, commençons à instancier notre moteur de template, dans le fichier index.php par exemple.
< ?php
require_once('templateEngine.php');
$tpl = new TemplateEngine();
?>
Ce constructeur prend un paramètre booléen, $localization. Celui-ci permettra d'indiquer au moteur s'il doit internationaliser notre application en utilisant l'extension PHP gettext. Par défaut, il ne cherchera pas à le faire. Et nous non plus, pour simplifier les choses. :)
Affichage de sous-templates
Il est possible de séparer un fichier HTML en plusieurs sous parties. Ce que nous allons faire ici. Créons donc un fichier index.html, et insérons-y le code suivant :
<!---===== tpl_name = bonjour =====---> <p>Bonjour le monde !</p> <!---===== tpl_name = main =====---> <h1>Test du moteur de templates !</h1>
Ce fichier est séparé en une sous-partie d'identifiant bonjour et en une autre d'identifiant main. On remarque la syntaxe particulière de séparation des sous-parties :
<!---===== tpl_name = IDENTIFIANT =====--->
A noter qu'il faut au minimum une sous-partie par fichier, afin de pouvoir l'utiliser.
Comment récupérer une sous-partie depuis notre moteur de templates ? C'est le rôle de la méthode GetHTMLCode.
< ?php
require_once('templateEngine.php');
$tpl = new TemplateEngine();
$buffer = $tpl->GetHTMLCode('index.html', 'bonjour');
?>
La méthode GetHTMLCode prend deux arguments. Le premier est le nom du fichier où aller récupérer la sous-partie, et le deuxième l'identifiant de la sous-partie. Dans ce cas, notre variable $buffer cmoprendra donc le paragraphe "Bonjour le monde !".
On pourra constater que ce code n'affiche rien à l'heure actuel. Pour afficher quelque chose, il nous faudra passer par la méthode Output.
Affichage
La méthode Output va nous permettre d'afficher le contenu de la variable passée en argument.
TemplateEngine::Output($buffer);
Cette méthode peut-être utile au cas où vous auriez des problèmes d'affichage (problèmes d'encodage, d'échappement d'apostrophes, etc.). En effet, il vous suffira de mettre votre fonction de traitement à l'intérieur de cette méthode, sans avoir à manipuler les buffers.
Utilisation de variables
A présent, nous allons voir comment interfacer notre code PHP avec notre code HTML. Imaginons le code PHP suivant :
<?php
require_once('templateEngine.php');
$prenom = 'Jonathan';
$nom = 'Petitcolas';
$tpl = new TemplateEngine();
$bonjour = $tpl->GetHTMLCode('index.html', 'bonjour');
TemplateEngine::Output($tpl->GetHTMLCode('index.html', 'main'));
?>
Et effectuons quelques modifications à notre code HTML :
<!---===== tpl_name = bonjour =====--->
<p>Bonjour ${prenom} ${nom} !</p>
<!---===== tpl_name = main =====--->
<h1>Test du moteur de templates !</h1>
${bonjour}
En observant le résultat, on aura le code source suivant :
<h1>Test du moteur de templates !</h1> <p>Bonjour Jonathan Petitcolas !</p>
Que s'est-il passé ? A la ligne 9 du code PHP, nous avons récupéré le sous-template bonjour. A l'intérieur, nous avons deux variables, repérées par l'écriture ${VARIABLE}. Le moteur de templates s'est alors chargé de remplacer ces étiquettes par les valeurs des variables respectives, par l'intermédiaire de la méthode GetHTMLCode.
Après avoir placé ce code modifié dans une variable, nous l'avons affiché dans la partie main, avant de tout envoyer au navigateur, via la méthode statique Output. Rien de plus compliqué. :)
A présent, vous connaissez toute la théorie de niveau supérieur pour utiliser ce moteur de templates. Vous pourrez ainsi développer proprement, en séparant à la fois partie PHP et partie HTML. En effet, pour obtenir un taux de maintenabilité le plus élevé possible, il est important de séparer le code de votre application en ses différentes composantes : PHP, HTML, Javascript, CSS, etc... Tout doit être dans un fichier différent (idéalement). Nous veillerons particulièrement à cette best-pratice dans l'utilisation du framework PEJO.
Exemple d'utilisation
Nous allons voir ici un exemple d'utilisation de cette classe, exemple que nous aurons souvent besoin de retrouver dans la suite de nos développements, à savoir l'affichage d'un tableau (mais on pourrait aussi prendre l'affichage d'une liste, ou de tout autre élément).
Imaginons que nous récupérons un tableau de la couche métier de notre application. Ce tableau peut par exemple provenir d'une base de données. Pour des raisons de simplification, nous déclarerons ce tableau "en dur" dans le code.
$datas[0]['titre'] = 'Les Misérables'; $datas[0]['auteur'] = 'Victor Hugo'; $datas[1]['titre'] = 'Candide'; $datas[1]['auteur'] = 'Voltaire'; $datas[2]['titre'] = 'Le Cid'; $datas[2]['auteur'] = 'Corneille'; $datas[2]['titre'] = 'L\'Avare'; $datas[3]['auteur'] = 'Molière';
A présent, nous voulons afficher le tout dans un tableau. Pour ce faire, modifions notre fichier HTML :
<!---===== tpl_name = main =====--->
<h1>Librairie</h1>
<table>
<thead>
<tr>
<th>Titre</th>
<th>Auteur</th>
</tr>
</thead>
<tbody>
${livres}
</tbody>
</table>
<!---===== tpl_name = livre =====--->
<tr>
<td>${titre}</td>
<td>${auteur}</td>
</tr>
Rien qu'en voyant ce code, vous devriez être en mesure de deviner le cheminement. Nous allons concaténer dans une variable ${livres} le contenu du sous-template livre, en faisant varier ${titre} et ${auteur} grâce aux données de notre tableau.
Traduisons donc cela en PHP :
<?php
require_once('templateEngine.php');
$datas[0]['titre'] = 'Les Misérables'; $datas[0]['auteur'] = 'Victor Hugo';
$datas[1]['titre'] = 'Candide'; $datas[1]['auteur'] = 'Voltaire';
$datas[2]['titre'] = 'Le Cid'; $datas[2]['auteur'] = 'Corneille';
$datas[2]['titre'] = 'L\'Avare'; $datas[3]['auteur'] = 'Molière';
$tpl = new TemplateEngine();
foreach($datas as $livre)
{
$titre = $livre['titre'];
$auteur = $livre['auteur'];
$livres .= $tpl->GetHTMLCode('index.html', 'livre');
}
TemplateEngine::Output($tpl->GetHTMLCode('index.html', 'main'));
?>
Je ne m'étendrais pas davantage dans les explications, étant donné que le paragraphe précédant est suffisamment explicite.
Une petite remarque cependant, destinée aux développeurs. De nombreux stagiaires ayant travaillé sur mon moteur de templates me posent la : pourquoi ne pas prévoir une méthode nous permettant d'écrire directement dans le HTML une variable sous la forme (par exemple) : ${livre['titre']}. Je refuse ce développement, et ce pour une simple raison.
Si nous insérons l'élément du tableau directement dans le HTML, cela nous fera perdre la possibilité d'effectuer une mise en forme sur l'élément avant de l'afficher. Par exemple, si notre tableau contient un élément de type timestamp (à savoir le nombre de secondes écoulées depuis le 1er janvier 1970), il est bien plus ergonomique d'afficher la date au format humain (par exemple 12/03/2008) plutôt que cet entier incompréhensible. La conversion se fait en PHP, et doit donc être dans le fichier index.php. Suivant cette logique, la question n'a plus lieu d'être.
Ainsi s'achève cet exemple. Ainsi que la partie "grand public" (bien que public très restreint) se termine. La suite est réservée aux développeurs souhaitant comprendre les fondations de ce framework, et pourquoi pas le faire évoluer. :)
Documentation technique
Si vous êtes encore là, c'est que vous n'avez pas peur de mettre les mains dans le cambouis. Bravo ! :)
Voici donc une petite documentation technique, qui vous permettra d'approfondir davantage le fonctionnement de la classe, au cas où certains commentaires vous sembleraient obscurs. Allons-y donc méthode par méthode. :)
Constructeur
/**
* Initializes all options of our templates engine.
* @param $localization Will our application be internationalized (with PHP extension gettext)? Default: no.
* @return void
*/
public function __construct($localization = false)
{
$this->files = array();
$this->subtemplates = array();
$this->localization = $localization;
}
Ici, rien de particulier à indiquer, si ce n'est l'utilité des variables. L'attribut $files contiendra les fichiers déjà parsés de notre moteur de templates, ce qui permettra une sorte de mise en cache, dans le but d'optimiser les performances. L'attribut subtemplates, quant à lui, permettra de stocker le contenu de chacune des parties, afin de faciliter les traitements ultérieurs internes au moteur. Quant à $localization, cela nous permettra de garder en mémoire si nous devons utiliser l'extension PHP gettext ou non, pour internationaliser notre application.
Méthode GetHTMLCode
/**
* Transforms a sub-template to the HTML output.
* @param $filename File containing the used sub-template.
* @param $subtemplate Name of the subtempalte to use.
* @return string HTML code ready to be displayed.
*/
public function GetHTMLCode($filename, $subtemplate)
{
$this->ParseFile($filename); // Reads the sub-template
return $this->Evaluate($filename, $subtemplate); // Replaces all HTML variables.
}
Pas grand chose à dire sur cette fonction, les commentaires se suffisant à eux-mêmes. :)
Méthode ParseFile
/**
* Reads the file and extracts all sub-templates.
* @param $filename File to parse.
* @return void
*/
private function ParseFile($filename)
{
// If the file has already be parsed, we do not parse it again.
if($this->files[$filename] !== true)
{
// Retrieving file content.
$filename = addslashes($filename);
if(!file_exists($filename)) throw new Exception('File '.$filename.' does not exist.');
$this->files[$filename] = file_get_contents($filename);
// Retrieving all sub-templates from file.
$this->GetSubTemplates($filename);
}
}
Ce code est légèrement plus complexe. Cette méthode va lire le contenu du fichier $filename, s'il n'a pas déjà été lu. En effet, si on a déjà parsé ce fichier dans le passé, l'attribut $this->files[$filename] vaudra true. Et ainsi, il sera inutile de relire ce fichier, étant donné que nous avons déjà ses sous-parties en mémoire dans l'attribut subtemplates. On ne rentrera pas dans la structure de contrôle.
Afin de simplifier la saisie de l'utilisateur, on échappe automatiquement les slashes. Sans cette ligne, le nom du fichier aurait dû, pour un maximum de compatibilité, contenir des doubles slashes, chose assez rébarbative, il faut bien l'avouer.
Le fichier est alors lu (s'il existe) et son contenu placé dans $this->files.
Méthode GetSubTemplates
Comme indiqué dans la documentation utilisateur un peu plus haut dans cet article, chaque fichier sera décomposé en plusieurs sous-parties, appelées sous-templates. Chaque sous-partie sera séparée par : , où TEMPLATE_NAME sera remplacé par un nom unique dans chaque fichier, identifiant de cette partie.
/**
* Retrieving all sub-templates from a given file.
* @param $filename File from which to extract sub-templates.
* @return void
*/
private function GetSubTemplates($filename)
{
/**
* All sub-templates will look like the following:
*
* <!---===== tpl_name = TEMPLATE_NAME =====--->
* <h1>This is my sub-templates</h1>
*
* No closing tags are required.
*/
// Splitting the file in sub-templates.
$explosion = explode('<!---===== tpl_name = ', $this->files[$filename]);
// Retrieving the name and content for each sub-template.
foreach($explosion as $tmp)
{
$tmp = explode(' =====--->', $tmp);
$subTemplateName = $tmp[0];
$subTemplateContent = $tmp[1];
// If internationalized, we translate it now with gettext extension.
$this->subtemplates[$filename][$subTemplateName] = ($this->localization ? $this->Localize($subTemplateContent) : $subTemplateContent);
}
// If the file is parsed, we replace the textual content heavy in memory by a single bit.
$this->files[$filename] = true;
}
On découpe notre fichier en sous-templates ligne 18 grâce à la fonction explode. Nous aurons alors dans $explosion l'ensemble de nos sous-templates, contenant encore l'identifiant et le morceau parasite =====--->. Qu'à cela ne tienne, nous séparons le nom du contenu grâce à l'explosion de la ligne 23.
Pourquoi ne pas avoir pris une expression régulière pour effectuer cette découpe ? Pour des raisons de performances, un explode étant bien plus rapide qu'une REGEX. D'autre part, cela est bien plus simple à appréhender. Et comme le disait si justement mon prof de mécanique en hypotaupe : on va pas prendre un char d'assaut pour percer un trou de 8 !. Tout est dit. :p
On range alors le contenu de notre template (éventuellement internationalisé grâce à l'extension php_gettext grâce à la méthode Localize) dans le tableau associatif $subtemplates, classé par nom de fichier et nom de template.
Enfin, pour nous assurer que nous ne relirons pas le même fichier deux fois et donc mettre en place une mise en cache de base, nous remplaçons le contenu de notre fichier dans la variable $files relativement coûteux en mémoire par un simple true, codé sur un seul et simple bit.
Méthode Evaluate
Cette méthode va nous permettre de remplacer nos variables (de la forme ${VARIABLE}) par leur valeur dans notre code HTML.
/**
* Replaces all variables by their content in the HTML file.
* @param $filename File where to replace the variables.
* @param $subtemplate Sub-template to use.
* @return string HTML code ready to be displayed.
*/
private function Evaluate($filename, $subtemplate)
{
/**
* All variables will be written in the HTML code with ${VARIABLE_NAME}.
*/
// Retrieving the localized (or not) HTML content, without replacing variables.
$evaluatedCode = $this->subtemplates[$filename][$subtemplate];
// Retrieving all variables names from the sub-template.
preg_match_all('#\${(.*)}#U', $evaluatedCode, $variables);
$variables = $variables[1];
// Replacing the value of each variable.
foreach ($variables as $variable)
{
global $$variable;
$evaluatedCode = str_replace('${'.$variable.'}', $$variable, $evaluatedCode);
}
// Returns the code with variable values inside.
return $evaluatedCode;
}
Nous récupérons d'abord tous les noms de variables grâce à l'expression régulière ligne 17.
Puis, pour chaque noms de variables contenus dans $variables, on travaille sur la variable $$variable. On va en fait utiliser une variable du nom la valeur contenue dans $variable. Petit exemple, qui sera sans doute bien plus explicite :
$a = 1; $variable = 'a'; echo $$variable;
Ici, afficher $$variable est équivalent à afficher $a, donc 1. Pratique n'est-ce pas ? :p
On place donc notre variable en global, ce qui nous permettra de récupérer la valeur de celle-ci lors de son initialisation dans notre script PHP (nous évitant ainsi de devoir la passer en paramètre de notre méthode). Puis, nous remplaçons simplement l'écriture ${variable} dans notre code HTML avec la fonction str_replace, plus performant qu'une autre expression régulière.
Méthode Localize
Nous n'allons pas nous étendre très profondément sur cette méthode, un futur article traitant de l'internationalisation de nos applications (prévue par le framework : je vous avais dit que c'était un truc sympa ! (-:) étant prévu.
/**
* Translates the content of a sub-template thanks to gettext PHP extension.
* @param $content Content of the sub-template to translate.
* @return string Translated content.
*/
private function Localize($content)
{
/**
* All texts to internationalize will be written like the following: _("Text to translate.").
*/
// Retrieving text to internationalize.
preg_match_all('#_\(\"(.+)\"\)#Us', $content, $unlocalizedTexts);
$unlocalizedTexts = $unlocalizedTexts[1];
// Translating all pieces of text.
foreach($unlocalizedTexts as $unlocalizedText)
{
$content = preg_replace('#_\(\"'.$unlocalizedText.'\"\)#Us', gettext($unlocalizedText), $content);
}
// Returns the translated text.
return $content;
}
Dans notre template, il est possible d'internationaliser du texte. Celui-ci devra être compris entre les chaînes de caractères : _("Texte à traduire"). Il s'agit de la syntaxe la plus proche de l'extension gettext, d'où ce choix. Cependant, étant donné que la traduction passe par le moteur de templates, vous pouvez modifier celle-ci si elle ne vous plaît pas.
Nous récupérons donc tous les textes à traduire ligne ligne 13, et nous les remplaçons par leurs versions traduites à la ligne 19, en utilisant la méthode gettext, qui cherchera des correspondances dans notre fichier de langue.
Ainsi s'achève les méthodes plutôt techniques. Voyons donc les deux dernières, qui reposeront le cerveau.
Méthode Output
Cette méthode va permettre d'afficher le contenu d'une variable, en lui appliquant éventuellement un ou plusieurs traitements.
/**
* Writes a buffer, results of some templates engine treatments. Static function for better performances.
* @param $buffer Text to write.
* @return void
*/
public static function Output($buffer)
{
// You will be able to use here some display functions depending your configuration, as the following:
// $buffer = utf8_encode($buffer);
// $buffer = strip_slashes($buffer);
echo $buffer;
}
Tout d'abord, on remarque que cette méthode est statique. Et pour cause, une méthode statique est 4 fois plus efficace (environ) qu'une méthode non statique (dynamique ?).
Cette fonction affiche simplement notre variable. C'est une sur-couche de echo en quelques sortes. Pourquoi avoir fait cela ? Simplement pour vous permettre d'encoder en UTF-8, de retirer des slashes en trop, ou que sais-je encore... avant l'affichage. Un comportement un peu similaire aux filtres WordPress pour les connaisseurs. Cela vous évitera donc de manipuler les buffers, à la documentation plutôt énigmatique et à l'utilité contestable, si on pense bien son code. ;)
Méthode Debug
Cette méthode se retrouve dans toutes les classes du framework PEJO, et permet d'afficher à l'instant t le contenu de notre classe.
/**
* Used to print the content of our template engine. For debug purposes.
* @return void
*/
public function Debug()
{
echo '<pre>';
print_r($this);
die('</pre>');
}
Cela se passe de commentaires.
Télécharger la classe TemplateEngine, moteur de templates du framework PEJO
Ainsi s'achève cet article sur le moteur de templates du framework PEJO. N'hésitez surtout pas à me faire part de vos remarques (c'est après tout le but de la publicisation de ce framework) ou de vos questions. Les commentaires sont faits pour ça après tout. ;)
Prochaine étape dans notre découverte de ce framework : l'arborescence normalisée et le routeur de navigation. Patience... :)

Pourquoi :
"Si nous insérons l’élément du tableau directement dans le HTML, cela nous fera perdre la possibilité d’effectuer une mise en forme sur l’élément avant de l’afficher. Par exemple, si notre tableau contient un élément de type timestamp (à savoir le nombre de secondes écoulées depuis le 1er janvier 1970)"
Alors que
foreach($datas as $livre) { $livre['date'] = date('d/m/y h:i', $livre['date']; $livres .= $tpl->GetHTMLCode('index.html', 'livre'); }Couplé à :
<!---===== tpl_name = livre =====--> ${titre['date']} ${titre['auteur']}marche bien.
On gagne en lisibilité, en assignation de variable et mémoire, non ?
Honnêtement je n'adhère pas du tout à l'architecture de ce moteur de templates, pour la simple et bonne raison que ton template ne contrôle absolument pas la manière dont il affiche les données. Pourquoi ? L'exemple le plus flagrant est le foreach qui ne se trouve pas dans le template mais dans le code métier.
Comment gérerais-tu dans ton template un cas classique du type :
sans créer un sous-template supplémentaire ?
Pour moi un template doit pouvoir utiliser 3 opérations :
Désolé du retard de ces réponses, mais le temps me manque cruellement pour blogger comme je le souhaiterais. :)
@Arbo :
Je ne crois pas (sauf erreur de ma part) que ce moteur de templates, dans sa version actuelle, puisse prendre de tels arguments. En effet, si on considère le code suivant :
Cela affichera :
Par conséquent, l'assignation avec le double dollar ne fonctionne pas pour les objets non scalaires (ce qui se confirme après un test avec ce moteur). Il faudrait donc éventuellement modifier la gestion des variables, afin de permettre de tels traitements. Cela pourrait être intéressant, si on ne perd pas trop en performances.
Cependant, sur ce dernier point, la perte pourrait être compensée par l'utilisation mémoire. A voir donc... :)
@Konal :
Sur ce point, je ne suis pas tout à fait d'accord avec toi. A mon sens, la couche View ne doit que gérer l'affichage des données. Or, le parcours d'un tableau d'objets nous fait déjà rentrer à l'intérieur de la couche métier. D'autant plus si on utilise les prédicats.
Par exemple, parcourir un tableau d'objets afin de n'afficher l'objet que s'il remplit certaines conditions ne fait pas partie des affectations de la couche vue. Tout n'est que question de représentation du MVC, mais cela s'éloigne de ma vision de ce modèle. ;)
Qui plus est, le Web est tout de même légèrement différent du monde applicatif. Et, lors de la collaboration de deux équipes (chose que je fais généralement : une équipe s'occupe de l'intégration tandis que l'autre s'occupe de la partie métier), il est bien plus simple pour les infographistes, qui bien souvent sortent d'écoles artistiques sans jamais avoir vu de if de leur vie - sauf peut-être dans des forêts ? :p - de se contenter d'utiliser des variables déjà toute prêtes, sans avoir à s'embrouiller avec des boucles ou des structures d'itérations, qui leur semblent bien bizarres et compliquées.
Ou comment mal réinventer la roue... 2 grands inconvénients :
Je signale que PHP est historiquement un langage de template pour PERL. Il suffit de faire une classe qui "gère" la séparation des données, et utiliser un fichier .phtml pour le template.
Edit : la suite du message n'est pas passé (protection contre les balises PHP sans aucun doute). Reposte les avec les htmlentities, et je les rajouterai à ce commentaire. ;)