Création d'un moteur de templates en PHP 5 objet

Allez ! Comme c'est Noël, je vous offre un de mes articles, paru dans le magasine Programmez! numéro 111 (septembre 2008). Celui-ci concerne la création d'un moteur de templates en PHP 5 orienté objet. Enjoy ! :p

Pour ceux qui préfère avoir une version hors ligne, voici la version PDF : Création d'un moteur de templates en PHP 5 orienté objet.

1. Introduction

Tout webmaster ayant déjà programmé un site professionnel a dû se confronter à la difficulté du développement multi-navigateurs. En effet, bien que des normes aient été énoncées par le W3C, de lourdes différences continuent d'exister entre différents concurrents : un site peut très bien s'afficher sous Mozilla Firefox et ne ressembler à rien sous Internet Explorer (ou inversement).

Ces différences d'interprétation rendent l'intégration de design particulièrement ardue. Cette tâche, pouvant sembler mineure aux yeux du profane, se révèle extrêmement coûteuse en ressources. Afin d'assurer une certaine productivité et donc de répondre aux exigences du marché, il est donc essentiel de faire appel à des spécialistes en la matière, experts de l’intégration de webdesign.

L'ensemble des infographistes se sépare en deux voies quant au rendu de leurs travaux. Une première partie délivre au client un fichier contenant une image du site. Le développeur devra alors découper lui-même l'image, et la mettre en ligne. Cela implique de très solides connaissances de l’interprétation CSS des différents navigateurs. Cette solution, bien qu’avantageuse financièrement, est bien souvent plus désastreuse au niveau horaire/homme. Il est donc préférable de choisir le second type de rendu : le design, accompagné de l’intégration XHTML.

Cette dernière solution pose cependant un problème. En effet, un développement en Y est généralement décidé pour la réalisation d'un contrat, afin de faire patienter le moins possible la clientèle. Le design doit donc être intégré simultanément au développement. La solution à ce développement collaboratif est l'utilisation du modèle MVC (Model-View-Controller).

Le modèle MVC permet la séparation des différentes couches d'une application. On a tout d'abord la couche Model, qui gérera les accès aux bases de données, calculera les différentes valeurs des variables, etc. C'est le développeur qui aura l'exclusivité de cette couche métier. La deuxième (couche View) contient tout ce qui sera vu par l'utilisateur final : formulaires, textes, images. C'est cette couche qui sera manipulée par le graphiste. Enfin, la dernière (couche Controller) permet le contrôle des saisies utilisateurs (une adresse e-mail a-t-elle le bon format xxxxxxxx@xxxxxx.xxx ?).

Le principe d'un moteur de templates est très simple : il relie la couche modèle à la couche vue. C'est ainsi qu'il gère le passage de variables entre ces deux couches. Par exemple, si on veut afficher un âge constamment mis à jour, d’une année sur l’autre. Le développeur calculera celui-ci selon la date récupérée d'une base de données. Ensuite, il transmettra au graphiste une variable que l'on pourra nommer age_membre. Celui-ci pourra alors, même s'il n'est pas familier avec le langage utilisé, afficher facilement l'âge du membre sans s'interroger sur la partie de code PHP correspondante.

Par conséquent, l'intégration graphique d'un site est rendue bien plus aisée. D'autant plus si des conventions de nommage sont appliquées. Le développeur réalise toute la partie métier de l'application pendant que le designer applique celle-ci de son côté dans la partie graphique. Ce modèle, reconnu dans de nombreux autres langages tels que Java à l'origine, a déjà fait ses preuves, et permet une intégration des plus rapides. De plus, le client peut en direct voir son site évoluer, et faire ses remarques au designer, qui pourra modifier en toute tranquillité son code sans pour autant interférer avec les codeurs de la couche métier.

Mais outre l'avantage time-to-market de ce système, on accède à un autre avantage non-négligeable : la séparation du code en ces différentes composantes, ce qui est de plus en plus dans l'ère du temps (ce qui est aisé à comprendre en terme de pérennité du code).

Il est en revanche important de parler des inconvénients d'un tel système. L'ajout d'un script supplémentaire liant les différentes parties ajoute forcément un délai supplémentaire lors de l'affichage de la page. Il est cependant possible de le réduire un maximum en optimisant son code, et en supprimant les fonctions inutiles.

2. Lecture d'un template

Commençons tout d'abord par créer notre classe TemplateEngine.

<?php

	class TemplateEngine
	{
	}

?>

A présent, nous allons lire le contenu d'un fichier *.html grâce à notre moteur. Pour ce faire, nous allons créer un attribut privé $buffer qui contiendra le contenu parsé de nos templates. Pour le remplir, nous ferons appel à une méthode ParseFile et pour l'afficher à une méthode Output.

private $buffer;

public function __construct()
{
	$this->buffer = '';
}

public function ParseFile($filename)
{
	$filename = addslashes($filename);

	if(file_exists($filename))
	{
		$this->buffer = file_get_contents($filename);
	}
	else throw new Exception('Le fichier '.$filename.' n'existe pas.');
}

public function Output()
{
	echo $this->buffer;
}

Ce code est très simple à comprendre. La méthode ParseFile prend en paramètre le nom du fichier à parser. On échappe automatiquement les caractères pouvant poser problèmes (notamment les /) afin de faciliter la tâche des développeurs. On se contente ensuite de mettre dans notre buffer le contenu du fichier.

L'utilisation d'une exception est bien entendu facultative. Il est tout à fait possible d'afficher à la place un message d'erreur. Celle-ci n'est présente que si vous désirez faire de la remontée d'exceptions, dans le cas par exemple où vous auriez un gestionnaire d'erreurs spécifique.

L'utilité de la méthode Output réside en le fait qu'il est possible de changer la méthode de sortie très facilement. Par exemple, on peut appliquer un utf8_encode à tout un site en appliquant cette fonction une seule et unique fois sur le buffer.

Testons à présent notre moteur. Pour ce faire, faisons un simple fichier *.html :

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
		<title>Ca marche !</title>
		</head>
	<body>
		<p>Notre moteur de templates fonctionne ! :)</p>
	</body>
</html>

Appelons donc notre moteur de templates et sa méthode Output dans un script PHP.

<?php

	require('templateEngine.php');
	$tpl = new TemplateEngine();
	$tpl->ParseFile('index.html');
	$tpl->Output();

?>

Il ne nous reste alors plus qu'à tester notre page.

Test du moteur de templates PHP 5

3. Passage de variables

Pour l'instant, l'utilité est plutôt négligeable. En effet, nous ne faisons qu'afficher une page statique. Dans ce cas, pourquoi ne pas afficher directement la page *.html ?

Nous allons donc voir la première utilité de ce moteur : le passage de variables du PHP au code HTML, sans mélanger les deux. Pour ce faire, nous allons coder la méthode privée SetVariables. On décidera d'inclure nos variables dans notre template sous la forme ${nom_variable}.

private function SetVariables()
{
	# Récupération de toutes les variables du template.
	preg_match_all('#${(.*)}#U', $this->buffer, $varNames, PREG_PATTERN_ORDER);
	$varNames = $varNames[1];

	# Pour chaque variable, on remplace celle-ci par sa valeur.
	foreach($varNames as $varName)
	{
		global $$varName;
		$this->buffer = preg_replace('#${'.$varName.'}#', $$varName, $this->buffer);
	}
}

Ce code peut sembler complexe aux premiers abords, mais il n'en est rien. La seule difficulté réside dans le choix des expressions régulières. On place dans le tableau $varNames tous les noms de variables récupérés dans le template, en respectant la norme fixée (à savoir ${nom_variable}). On prend le second élément du tableau (indice 1), le premier étant un récapitulatif des occurrences vérifiant la regex passée en paramètre.

Il ne faut pas oublier de rendre l'algorithme de recherche non gourmand en ajoutant l'option U (pour ungreedy) à notre expression régulière. En effet, si on le supprime, le parseur prendra en compte tout le texte compris entre la première et la dernière accolade, et non pas le texte entre chaque paire d'accolades.

4. Sous-templates

Nous allons maintenant découper notre template en plusieurs sous-parties. Cela peut-être utile pour, par exemple, afficher une liste de membres.

Nous utiliserons une notation qui permet de bien séparer visuellement les différentes parties du template. Transformons donc notre template principal pour afficher une liste de membres.

<!---===== begin_template members =====--->
<ul>
	${members }
</ul>
<!---===== end_template =====--->

<!---===== begin_template member =====--->
<li>
	<strong>${firstName} ${lastName}</strong><br />
	${title}
</li>
<!---===== end_template =====--->

Pour gérer les sous-templates, il nous sera nécessaire de modifier notre moteur. Il faut qu'il soit capable de détecter les différents sous-templates, et de ne remplacer les variables que dans celui concerné, plutôt que de parcourir le fichier dans sa totalité.

On ajoute tout d’abord un attribut $filename que nous initialisons dans le constructeur. Nous pouvons aussi supprimer l’attribut $buffer, qui ne nous sera plus d’aucune utilité, comme nous le constaterons plus tard.

private $filename ;

public function __construct($filename)
{
          $this->filename = addslashes($filename);
}

Modifions donc notre méthode ParseFile afin de prendre en compte les différentes particularités de nos sous-templates :

private $originalSubTemplates;
private $modifiedSubTemplates;

private function ParseFile()
{
	if(file_exists($this->filename))
	{
		$content = file_get_contents($this->filename);
		preg_match_all("#<!---===== begin_template (w*) =====--->(.*)<!---===== end_template =====--->#sU", $content, $matchedExp, PATTERN_ORDER);
		$subTemplatesNames = $matchedExp[1];
		$subTemplatesContent = $matchedExp[2];
		foreach($subTemplatesNames as $index => $subTemplateName)
		{
			$this->originalSubTemplates[$subTemplateName] = $subTemplatesContent[$index];
		}
	}
	else throw new Exception('Le fichier '. $this->filename.' n'existe pas.');
}

Afin d’optimiser un maximum notre moteur, nous garderons en mémoire dans les deux variables $originalSubTemplates et $modifiedSubTemplates respectivement les parties du template tels que parsés lors de la première lecture du template, et dans la seconde, le contenu du sous-template avec remplacement de toutes les variables.

Pour l'expression régulière, il est essentiel de passer l'option s afin que le caractère universel (le point .) prenne en compte aussi les sauts de ligne. Il nous suffit maintenant de faire appel à cette méthode (devenue privée) dans notre constructeur, afin de mémoriser un tableau des différentes sous-parties de notre template.

Modifions à présent notre méthode de liaison du code à la partie graphique :

private function SetVariables($templateName)
{
	# Récupération de toutes les variables du template.
	preg_match_all('#${(.*)}#U', $this->originalSubTemplates[$templateName], $varNames, PREG_PATTERN_ORDER);
	$varNames = $varNames[1];

	# Réinitialisation du template modifié, au cas où des traitements aient déjà eu lieu sur celui-ci.
	$this->modifiedSubTemplates[$templateName] = $this->originalSubTemplates[$templateName];

	# Pour chaque variable, on remplace celle-ci par sa valeur.
	foreach($varNames as $varName)
	{
		global $$varName;
		$this->modifiedSubTemplates[$templateName] = preg_replace('#${'.$varName.'}#', $$varName,
							$this->modifiedSubTemplates[$templateName]);
	}
}

La seule différence avec la méthode précédente est que l'on modifie les différentes variables dans l'attribut $modifiedSubTemplates au lieu de la variable $buffer.

Il nous faut enfin une dernière méthode, qui nous permettra d'attribuer à une variable le contenu d'un sous-template. De plus, cette fonction nous servira de méthode d'accès à notre moteur (tous les arguments et méthodes étant désormais privés).

public function GetHTMLCode($templateName)
{
	$this->SetVariables($templateName);
	return $this->modifiedSubTemplates[$templateName];
}

Cette méthode, très simple, se contente d'attribuer à notre sous-template les variables correspondantes, et nous retourne le code HTML final.

On optimise enfin un peu notre code en rendant la méthode Output statique : en effet, une méthode statique s’exécute environ quatre fois plus rapidement environ qu’une méthode classique. De plus, cela nous facilitera la programmation de notre classe, comme nous pourrons le constater dans l’exemple suivant.

public static function Output($buffer)
{
         echo $buffer;
}

5. Exemple

Voyons à présent un exemple pratique de notre classe permettant l'affichage d'une liste de membres.

<?php

	require('templateEngine.php');

	$tpl = new TemplateEngine('index.html');

	# Récupération des données, habituellement provenant d'une base de données SQL.
	$members_table = array(  	array( 'Jonathan', 'Petitcolas', 'Web Lab. member'),
			  	array( 'Mauricio', 'Diaz Orlich', 'Web Lab. director'),
				array( 'Benjamin’, 'Bouché', ‘Web Lab. SCT’),
			 );

	foreach($members_table as $member)
	{
		$firstName = $member[0];
		$lastName = $member[1];
		$title = $member[2];
		$members .= $tpl->GetHTMLCode('member');
	}

	TemplateEngine::Output($tpl->GetHTMLCode('members’));
?>

Le code est suffisamment explicite de lui-même pour se passer de commentaires. On remarquera cependant que l'on concatène les différents éléments de la liste de membres, conformément au modèle défini dans notre template. Au final, le résultat obtenu est le suivant :

Test final du moteur de templates

A présent, nous disposons d'un moteur de templates des plus basiques. De nombreuses autres fonctionnalités restent à implémenter, selon vos propres besoins. Par exemple, il pourrait être intéressant de mettre une gestion des templates sur plusieurs fichiers, ou encore d'implémenter l'insertion de données émanant de tableaux avec une syntaxe simplifiée du type ${tab[0]}.

Mots-clefs : , , ,

13 réactions sur cet article.

  1. J'ai lu votre article sur la programmation de moteurs de templates dans "Programmez!". Je l'ai trouvé très intéressant... C'est d'ailleurs pour cet article que j'ai acheté ce magazine. :)

  2. Aymeric Lagier says:

    Très intéressant. Cependant, j'aurais une question. Dans ton code :

    # Pour chaque variable, on remplace celle-ci par sa valeur.
    foreach($varNames as $varName)
    {
    global $$varName;
    $this->buffer = preg_replace('#${'.$varName.'}#', $$varName, $this->buffer);
    }

    Je ne comprends pas le global $$varName ? Déjà, pourquoi $$ ? Et ensuite, pourquoi un global alors que le $varName vient du foreach ?

    Merci d'avance. :)

  3. On peut, en PHP, indiquer des noms de variables construit dynamiquement. Par exemple, considérons le code suivant :

    $a = 'b';
    $$a = 3;

    Dans ce code, la variable $$a est équivalent à la variable $b, et vaudra donc 3. D'où le double dollar.

    Enfin, le global est ici nécessaire, car on attribuera des valeurs aux variables à l'intérieur de notre script PHP, et nous devrons les récupérer à l'intérieur de la méthode de la classe. Sans celui-ci, $$varName serait local à la fonction, et n'aurait donc aucune valeur. Ce qui serait un peu inutile. :)

  4. Merci Jonathan pour cette explication. J'ai d'autres questions.

    Dans ton expression régulières #${(.*)}#U et #${'.$varName.'}# il faut echappé le $, non ?

    Autre question, pour le passage simple de variable, j'arrive à faire fonctionner ton code, mais avec les sous-templates ça ne va plus. Tu n'appelles jamais ta méthode parseFile(), à moins d'avoir besoin de lunettes ? :/

    J'ai fini par redévelopper les méthodes pour y voir plus clair :-) Au moins, ça fait un bon apprentissage.

  5. Effectivement, une autre petite erreur s'est glissée dans cet article. Il manque un anti-slash avant le dollar, qui sans celui-ci signifierait la fin de chaîne. Article corrigé. Merci de ta vigilance. :)

    Quant au ParseFile, il faut en effet faire appel à cette méthode, afin de lire le fichier et de le décomposer en plusieurs sous-templates. Tu peux le faire dans la méthode GetHTMLCode, avant le SetVariable par exemple.

    Il est en effet bien plus sage de s'en inspirer et de redévelopper un moteur de templates. Après tout, comme le disait si bien mon professeur de mécanique de Maths Sup. (petite dédicace au passage) : Voir faire n'est pas savoir faire. :)

  6. Bonjour,

    Suite a votre article (ainsi que quelques autres...), j'avais decidé de faire mon propre moteur de templates. Cela constituait à mon avis un bon exercice d'apprentissage du PHP.

    Votre article est bien... mais bon, l'objectif était de faire un truc neuf... pas de reprendre votre code !

    Bref, cet article a été le point de départ de pas mal d'apprentissage pour moi, et je vous en remercie grandement ! (oui, parce qu'à la base, je suis autodidacte...)

    Aujourd'hui, j'ai une base qui marche... Je n'ai "plus" qu'à rajouter des composants selon mes besoins. Bref, heureux et fier comme un bar/tabac, alors que pourtant, je n'ai pas inventé la poudre...

  7. Aurélien Derouineau says:

    Au passage, je conseille XSL comme moteur. C'est standard et très puissant.

Ils ont fait un lien vers cet article :

  1. Moteur de templates du framework PHP 5 PEJO | Jonathan Petitcolas

Réagissez sur cet article !