JeuWeb - Crée ton jeu par navigateur

Version complète : [Tutoriel] Moteur de template
Vous consultez actuellement la version basse qualité d’un document. Voir la version complète avec le bon formatage.
Pages : 1 2 3 4
Voilà, j'avais dit que j'essayerai de faire un petit tuto sur la création d'un moteur de template sans prétention.


C'est parti !

Le but
Comme dit, l'idée est de créer une classe Template qui nous permettra de mettre en place et d'afficher du HTML auquel nous rajouterons nos variables, passés depuis PHP.

Une page en HTML :
<!DOCTYPE html>

<html>
   <head>
       <title>{{titre}}</title>
       <meta charset="UTF-8">
       <meta name="viewport" content="width=device-width, initial-scale=1.0">
   </head>
   <body>        
       {%FOR:pays%}
           <b>{{nom}} </b><br/>
           {%FOR:regions%}
               {{nom}} a le numéro : {{numero}}<br/>
           {%ENDFOR%}
       {%ENDFOR%}
   </body>
</html>


Notre classe se contentera de parser ce fichier et de remplacer les différentes balises par leurs équivalents PHP (enfin, à peu près ^^).
C'est tout ? Oui. Mais si vous le souhaitez, il sera très facile de le faire évoluer (gestion du cache etc).

Prérequis :
Un minimum de connaissances en PHP Orienté Objet est nécessaire, ainsi qu'en expressions régulières.
Le serveur doit accepter la fonction 'file_get_contents()', désactivée sur certains hébergeurs gratuits.

Attaquons !

Voilà le code de notre classe :

Code PHP :
<?php

class Tpl
{
   
   
// Données
   private $data = array();
   
   
// Chemin du template
   private $template;
   
   
// Contenu du template
   private $content;
   
   public
function __construct($name, $data)
   {
       $this->template = $name;
       $this->data = $data;
   }
   
   public
function display()
   {
       // Temporisation de sortie
       ob_start();
       // Récupération du contenu du template
       $this->get_content();
       // Compilation du template
       $this->parse();
       //Ecriture du fichier compilé
       $this->write_file();
       
       
// Affichage du fichier compilé et destruction du tampon
       require($this->template . '.cache.php');
       echo ob_get_clean();
   }
   
   private
function get_content()
   {
       // On récupère tout le contenu du template
       $this->content = file_get_contents($this->template);
   }
   
   private
function parse()
   {
       // Parsage des variables {{VAR}}
       $this->content = preg_replace('#\{\{(\w+)\}\}#', '<?php echo $this->data[\'$1\']; ?>', $this->content);
   }
   
   private
function write_file()
   {
       // On écrit le template compilé dans un fichier
       file_put_contents($this->template . '.cache.php', $this->content);
   }
}


Cette classe est assez basique.
La partie la plus 'difficile' est sûrement le preg_replace.
Le parseur va chercher les expressions régulières de type '#\{\{(\w+)\}\}#' , (qui revient à {{Ma_Var}} , \w correspondant à un mot, soit [a-zA-Z0-9_]).
Il va ensuite remplacer cette expression par '<?php echo $this->data['Ma_Var']; ?> .

Pas de questions, on continue.

Ecrivons maintenant notre template, que j'ai appelé test.html
<!DOCTYPE html>

<html>
   <head>
       <title>{{titre}}</title>
       <meta charset="UTF-8">
       <meta name="viewport" content="width=device-width, initial-scale=1.0">
   </head>
   <body>
       <span>{{Mon_sous_titre}}</span>
   </body>
</html>



Et maintenant nous allons initialiser et lancer tout ça :

Code PHP :
<?php 
require ('Tpl.php');

// Tableau de données
$data = array(
           "titre" => "Première page",
           "Mon_sous_titre" => "C'est tout simple"
);
// Création d'une instance de Tpl
$tpl = new Tpl('test.html', $data);
// Affichage
$tpl->display();


Résultat : On a bien nos données affichées à l'écran.

Regardons notre code compilé, dans test.html.cache.php :

<!DOCTYPE html>

<html>
   <head>
       <title><?php echo $this->data['titre']; ?></title>
       <meta charset="UTF-8">
       <meta name="viewport" content="width=device-width, initial-scale=1.0">
   </head>
   <body>
       <span><?php echo $this->data['Mon_sous_titre']; ?></span>
   </body>
</html>


Le parseur a bien remplacé nos variables TPL en code PHP.

Ce n'est qu'un début, passons à la suite.

Que se passerait-il si, dans mon template, je faisais référence à une variable qui n'existe pas ?
=> J'aurai une belle erreur "Notice : Undefined index".

Pallions à cela :

Dans notre preg_replace, modifions le code PHP généré :
Code PHP :
<?php echo $this->data[\'$1\']; ?>

Par ceci :
Code PHP :
<?php $this->_show_var(\'$1\'); ?>

Ainsi, nous demanderons à PHP de ne pas afficher directement la variable, mais d'exécuter la fonction _show_var en lui passant en argument la variable.
Comme cela, nous pourrons traiter en amont le cas où la variable n'est pas définie.

Ecrivons notre fonction dans notre classe :

Code PHP :
<?php 
private function _show_var($name)

{
   if (isset($this->data[$name]))
   {
       echo $this->data[$name];
   }
   else
   
{
       echo $name ;
   }
}


Rien de bien compliqué : Si la variable existe, on affiche son contenu, sinon on affiche son nom.
L'avantage, c'est que vous pourrez modifier cette fonction pour faire évoluer le moteur de templates.

Testez si vous le souhaitez, et continuons.

Les boucles

Nous allons maintenant implémenter à notre moteur un système de boucle, traduisez 'foreach'.
Le principe est simple, on passe un array à notre template afin de simuler un foreach $var, et on peut afficher son contenu.

Dans le template, une boucle va se modéliser comme ceci :

{%FOR:users%}

   Mon prénom est {{name}}, j'ai {{age}} ans.
{%ENDFOR%}


Codons tout cela.

Nous allons tout d'abord ajouter un nouvel attribut à notre classe :
Code PHP :
<?php 
private $stack = array();

Cette stack sera en quelque sorte un tampon où nous stockerons nos datas lors d'une boucle.

Puis dans notre méthode parse, nous allons rajouter 2 preg_replace, qui remplaceront respectivement les {%FOR:var%} et {%ENDFOR%} :

Code PHP :
<?php 
// Parsage des FOR

$this->content = preg_replace('#\{\%FOR:(\w+)\%\}#', '<?php foreach ($this->data[\'$1\'] as \$_$1_vars): $this->_stack(\$_$1_vars); ?>', $this->content);
//ENDFOR
$this->content = preg_replace('#\{\%ENDFOR\%\}#', '<?php $this->_unstack(); endforeach; ?>', $this->content);


Comme tout à l'heure, si vous êtes un peu à l'aise avec les expressions régulières le code n'est pas compliqué.
Vous pouvez remarquer que le code PHP qui remplacera notre code TPL, fait appel aux méthodes _stack et _unstack.
Nous allons tout de suite écrire ces méthodes :

Code PHP :
<?php 
private function _stack($elem)

{
   $this->stack[] = $this->data;
   foreach ($elem as $key => $value)
   {
       $this->data[$key] = $value;
   }
}
private function
_unstack()
{
   $this->data = array_pop($this->stack);
}


La function _stack sera appelé à chaque itération du foreach.
l'attribut stack servira alors de 'pile', où l'on empilera les datas.
Puis on ajoutera à l'attribut data le contenu de la ligne du tableau ($elem).

Enfin, à la fin de chaque boucle, on lancera la méthode _unstack, qui supprimera le dernier élément ajouté.

Vous avez suivi ? La méthode peut paraitre inutilement tordue, mais elle est courte et a un avantage énorme : Permettre d'imbriquer plusieurs foreach.

Testons cela. Nous allons d'abord modifier notre tableau $data comme suit :
Code PHP :
<?php 
$data
= array(

   "titre" => "Seconde page",
   "pays" => array(
       0 => array(
           "regions" => array(
               0 => array(
                   "nom" => "Nord",
                   "numero" => "59"
               ),
               1 => array(
                   "nom" => "Oise",
                   "numero" => "60"
               ),
           ),
           "nom" => "France"
       ),
       1 => array(
           "regions" => array(
               0 => array(
                   "nom" => "Flamand",
                   "numero" => "Y'en a ?"
               ),
               1 => array(
                   "nom" => "Wallons",
                   "numero" => "Je sais pas.."
               ),
           ),
           "nom" => "Belgique"
       )
   )
);

Puis notre template :
<!DOCTYPE html>

<html>
   <head>
       <title>{{titre}}</title>
       <meta charset="UTF-8">
       <meta name="viewport" content="width=device-width, initial-scale=1.0">
   </head>
   <body>        
       {%FOR:pays%}
           <b>{{nom}} </b><br/>
           {%FOR:regions%}
               {{nom}} a le numéro : {{numero}}<br/>
           {%ENDFOR%}
       {%ENDFOR%}
   </body>
</html>

On lance le navigateur, et tadaa ! ^^
Désolé les amis belges, je ne connais pas trop votre pays Wink

Voilà voilà.

Récapitulatif

Voilà le code complet de notre classe :
Code PHP :
<?php


class Tpl
{

// Données
private $data = array();
// Chemin du template
private $template;
// Contenu du template
private $content;

public function
__construct($name, $data)
{
$this->template = $name;
$this->data = $data;
}

public function
display()
{
// Temporisation de sortie
ob_start();
// Récupération du contenu du template
$this->get_content();
// Compilation du template
$this->parse();
//Ecriture du fichier compilé
$this->write_file();

// Affichage du fichier compilé et destruction du tampon
require($this->template . '.cache.php');
echo
ob_get_clean();
}

private function
get_content()
{
// On récupère tout le contenu du template
$this->content = file_get_contents($this->template);
}

private function
parse()
{
// Parsage des variables {{VAR}}
$this->content = preg_replace('#\{\{(\w+)\}\}#', '<?php $this->_show_var(\'$1\'); ?>', $this->content);

// Parsage des FOR
$this->content = preg_replace('#\{\%FOR:(\w+)\%\}#', '<?php foreach ($this->data[\'$1\'] as \$_$1_vars): $this->_stack(\$_$1_vars); ?>', $this->content);

//ENDFOR
$this->content = preg_replace('#\{\%ENDFOR\%\}#', '<?php $this->_unstack(); endforeach; ?>', $this->content);
}

private function
write_file()
{
// On écrit le template compilé dans un fichier
file_put_contents($this->template . '.cache.php', $this->content);
}

private function
_show_var($name)
{
if (isset(
$this->data[$name]))
{
echo
$this->data[$name];
}
else
{
echo
$name;
}
}

private function
_stack($elem)
{
$this->stack[] = $this->data;
foreach (
$elem as $key => $value)
{
$this->data[$key] = $value;
}
}

private function
_unstack()
{
$this->data = array_pop($this->stack);
}

}

Aller plus loin

Le tuto s'arrête ici. C'est selon moi une base simple.
Cependant, il est très facile de faire évoluer notre moteur.
Par exemple, le boulot du cache est presque terminé, étant donné que notre fichier compilé est écrit.

Sinon, je vous conseille de remplacer les preg_replace par preg_replace_callback, afin de pousser un peu plus les possibilités.

Si certains veulent en voir plus, postez ici vos demandes auxquelles j'essayerai de répondre.

Merci à ceux qui auront eu le courage de tout lire Smile
Quelques remarques:

• Place la balise <meta charset.../> en premier. En effet, la <meta charset.../> [...] doit apparaître parmi les 512 premiers octets de la page car certains navigateurs ne consultent seulement ces premiers octets pour déterminer l'encodage utilisé pour la page. (Source: MDN). Si le titre de la page dépasse cette longueur, des surprises apparaitront...

• Utilise [ code=html ] ou [ html ] ou au pire [ code=xml ] pour tes encarts de code dans tes posts.

• Perso, les $this qui se promènent au milieu de nulle part, dans le code PHP compilé, j'aime moyen, mais disons que c'est du code compilé (temporaire, interne), donc l'humain n'a pas à le lire

• Que se passe-t-il quand plusieurs utilisateurs vont vouloir une même page? Ton système de template risque de créer une page pour l'utilisateur1, puis une pour l'utilisateur2 (qui écrase celle du 1), puis il fera le requi_reonce de l'utilisateur1, et donc celui-ci verra la page de l'utilisateur2. Creuse ce qui concerne les Systèmes de verrou de fichier pour éviter cela, et les Systèmes concurrents (PDF) pour la culture.

• Sur la rédaction, c'est bien écrit, pas de soucis Smile

• Le template n'offre aucune notionanti-XSS, comme le ferait XSL: si la variable $this->data[] contient du code HTML, il sera interprété comme tel, et non comme du texte. Mieux vaudrait également que template.cache.php ne soit pas accessible de l'extérieur (cad depuis le web).

• Niveau OO, le moteur de template s'occupe de plusieurs (trop?!) de choses: remplacer les variables dans le template, le sauver dans un fichier, le charger depuis ce fichier, et même, vérifier que les varables d'entrée existent... Découper en plusieurs classes me semble donc plus approprié. L'intérêt étant que la classe de sauvegarde dans un fichier pourra alors céder sa place à une classe de sauvegarde dans le flux php://memory ou php://temp, sans toucher au moteur de template. La simplicité de l'informatique n'est pas dans la création de lib à 1~2 classes, mais dans la création de systèmes fragmentés où changer un p'tit bout n'affecte pas les autres morceaux.

• Note que si tu fais ta propre syntaxe de template (je sais pas si c'est exactement le cas ici), alors tu ne bénéficieras pas d'un autre intérêt des libs et des langages standardisés: leur prise en charge par les IDE (coloration syntaxique, refactoring,...)

• Tu parles de la possibilité d'ajouter d'autres capacités au moteur, mais tu l'appliques de travers: privilégie l'open-closed principle au lieu de la modification de ta classe. Sinon, plus tu voudras ajouter de possibilités au moteur de templates, plus ta classe deviendra un énorme fourre-tout.

• Si tu fais un {%FOR%} sans le {%ENDFOR%}, ton moteur de template risque de tourner zinzin... Là, tu essaye finalement de refaire un lexer/parser en PHP. Niveau perfs, ce sera mauvais, et tu risques de tomber sur des schémas syntaxiques qui vont faire planter ton système (cf 1ere ligne de ce point).

• Dans ton foreach, tu utilises $this->data au lieu de $this->_show_var(). Note d'ailleurs que les appels de fonctions étant lents en PHP, une boucle qui appelle des fonctions risque d'être un goulet d'étranglement.

Bonne chance pour les corrections Smile
A mon sens, un tuto doit obligatoirement présenter quelque chose qui peut effectivement passer en prod, sinon le risque est de voir arriver en prod, sur les projets de ceux ayant lu le tuto, des codes bancals. C'est peut-être juste une question de terminologie ("présentation d'un système de template" au lieu de "tuto sur un moteur de template").

Le moteur a justement le défaut de ne pas être fait "pour être amélioré": dans l'état des choses, la classe est présentée et conçue comme un "fourre-tout" du moteur de template, une sorte de grosse boite noire.

Simplifier ne signifie pas "mettre tout en vrac". Une bibliothèque bien classée dans une maison ordonnée est bien plus simple à utiliser qu'un coffre à jouet remplis de bouquins en vrac.

Si on présente un code comme tuto, il faut qu'il soit à prendre tel quel. Sinon, c'est une présentation de son travail, pour appeler des critiques.

L'histoire des %FOR%, des compteurs, etc n'est qu'un bricolage des parser/lexer existants en PHP. Tourne-toi vers eux.

J'ai effectivement lu de travers, j'ai cru que le code généré était celui envoyé au client. En fait, là, t'as plutôt un raccourcis syntaxique {{...}} pour éviter de tapper <?php echo $...; ?>. Pourquoi ne pas utiliser des macros de l'IDE?
Bien que ce n'était pas le but, je vais essayer de vous présenter dans les jours à venir une réelle implémentation d'un tel système.
Par contre un tuto de cette taille, faut vraiment avoir le temps :/

Sinon oui, le moteur en l'état est un simple lexer/parser.
Si je préfère taper {{var}} au lieu de <?php echo $var ?>, c'est surtout une question de lisibilité. En l'état c'est convenable, mais je prévois d'intégrer une gestion de tags, par exemple {{var|esc}} qui m'afficherait ma variable préalablement passée au htmlspecialchars().
Et {{var|esc}} est bien plus simple, rapide à taper et plus lisible (selon moi) que [b]<?php echo htmlspecialchars($var, ENT_QUOTES, 'UTF-8') ?>[/b

Edit : Sinon j'aime bien ton blog, c'est bien amené et tourné, sur des sujets que tu maitrises.
Merci du compliment Smile

Pourquoi ne pas utiliser la syntaxe XML alors?

<p>Bonjour <var name="user_name"/>!</p>

Pas de lexer/parser à faire, puisqu'on peut utiliser celui de PHP (DOMDocument), la possibilité d'utiliser la coloration syntaxique de l'éditeur (voire plus, si l'IDE permet de mettre une couleur différente pour chaque tag), et on reste dans le standard.
Même, avec les XML namespaces, on allège encore et on sépare les tags de template des tags html:


<!-- ... -->
<html xmlns:tpl="http://www.monsite.com/xml-namespace/template">
<!-- ... -->
<p>
Bonjour, <tpl:user_name/>
</p>
<!-- ... -->
</html>

Note que même la coloration syntaxique du forum parvient à rendre lisible le code ainsi "XMLizé".

(je vais réussir à retomber sur XSL en continuant ainsi :p )
(23-02-2015, 03:37 PM)Xenos a écrit : [ -> ](je vais réussir à retomber sur XSL en continuant ainsi :p )

Ça rejoint ce que je disais sur ton blog, c'est bien amené ^^

Sinon pour le XML, si je ne l'utilise pas c'est que je ne trouve pas ça intuitif. Cela vient sûrement d'un non intéressement de ma part vis à vis de ce format, où que je bosses avec HTML depuis mes débuts en programmation, mais je préfère rester sur du HTML.
XML, c'est la grammaire du HTML, rien de plus.
Si tu préfères, XML, c'est xHTML où on met ce qu'on veut comme nom de balise (<machin>) ou d'attribut (<machin truc="bidule">), et la différence HTML/xHTML peut être simplifiée par "xHTML, c'est du HTML où on ferme les balises": <input ...> devient <input .../> et toute balise ouvrante type <tr> doit avoir une balise fermante correspondante </tr>.
Quel serait l'avantage d'utiliser <tpl:user_name/> plutôt que {{ $user_name }} ?

par exemple, dans le template suivant :

<li class="group {{ $user->group }}">{{ $user_name }}</li>
<tpl:.../> fait de {{...}} un vrai tag XML au lieu d'un morceau de texte. Cela permet:
• Une manipulation DOM possible (exemple: XPath)
• Une coloration syntaxique possible
• Une séparation des espaces de nom (pour mélanger des langages)
• Un traitement par XSL (oui, j'insiste lourdement :p mais je serai curieux de comparer les perfs d'un template XSL processé par libXSL natif, face à un template {{...}} processé en PHP)

Après, si on veut effectivement traiter le {{...}} comme du pur contenu texte (ce qui serait le cas de @class="group {{$user->group}}"), un tag n'est pas approprié: il sert à structurer, si la structure n'est pas voulue, le tag n'est pas nécessaire.

Note que XSL a, pour le coup, une structure assez verbeuse:

<li>
<xsl:attribute name="class">
<xsl:text>group </xsl:text>
<xsl:value-of select="$user/group"/>
</xsl:attribute>
<xsl:value-of select="$user/name"/>
</li>

Au fait, cela se passe comment si tu veux afficher littéralement {{exemple}} ?
D'ailleurs, à la relecture, on parle ici plus d'un système de code-snippet plutôt que d'un moteur de template: le code PHP de ce "moteur" n'a aucun intérêt à être exécuté en temps réel. C'est un peu comme le précompilateur SASS: je reconnait que cela évite de taper des lettres (comme un code snippet d'ailleurs), mais je ne qualifierai pas cela de "moteur".
Pages : 1 2 3 4