Comparatif : Découverte des principaux moteurs de templates en PHP

Smarty, phpBB2, TinyButStrong, VTemplate, ModeliXe

Guillaume Rossolini

Il y a tellement de moteurs de templates (gabarit) disponibles en PHP que n'importe quel développeur s'y perd facilement (moi y compris).

Ce cours reflète mon exploration des principaux moteurs qui ont attiré (ou qui ont été portés à) mon attention.

J'ai la prétention d'en apprendre suffisamment au travers de cette exploration pour être en mesure de faire un comparatif (publié en fin d'article).

Commentez ♪

- Titre : Comparatif : Découverte des principaux moteurs de templates en PHP

- Auteur : Guillaume Rossolini

- Parution : 13 mars 2007

- Licence : Copyright ® 2006 Guillaume Rossolini. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- I-A. Remerciements

- I-B. Préambule

- I-C. La démarche de comparaison

- I-D. Consignes générales d'installation

- I-E. Informations générales sur les tests de performances

- I-F. La base de données utilisée

- II. ModeliXe

- II-A. Présentation

- Le projet

- Installation

- Héritage de la classe

- II-B. Utilisation

- Variables

- Blocs

- Imbrication

- Alternance

- Segmentation du gabarit

- Gestion du cache

- II-C. Mon avis

- Avantages

- Inconvénients

- Conclusion

- III. phpBB2 (PHP Base Library - phplib)

- III-A. Présentation

- Les projets

- Installation

- Héritage de la classe

- III-B. Utilisation

- Variables

- Blocs

- Imbrication

- Alternance

- Segmentation du gabarit

- III-C. Mon avis

- Avantages

- Inconvénients

- Conclusion

- IV. Smarty

- IV-A. Présentation

- Le projet

- Installation

- Héritage de la classe

- IV-B. Utilisation

- Variables

- Blocs

- Imbrication

- Alternance

- Segmentation du gabarit

- Gestion du cache

- IV-C. Fonctionnalités avancées

- Plugin {mailto} : Protection d'adresses e-mail

- Plugin {debug} : Débogage

- Plugin {html_table} : Tableaux HTML

- Plugin {insert} : En tête du document de sortie

- Plugin {include} : Combination de gabarits

- Filtre de sortie

- IV-D. Mon avis

- Avantages

- Inconvénients

- Conclusion

- V. TinyButStrong

- V-A. Présentation

- Le projet

- Installation

- Héritage de la classe

- V-B. Utilisation

- Variables

- Blocs

- Imbriquation

- Alternance

- Segmentation du gabarit

- Gestion du cache

- V-C. Mon avis

- Avantages

- Inconvénients

- Conclusion

- VI. Virtual Template

- VI-A. Présentation

- Le projet

- Installation

- Héritage de la classe

- VI-B. Utilisation

- Variables

- Blocs

- Imbrication

- Alternance

- Segmentation du gabarit

- Gestion du cache

- VI-C. Mon avis

- Avantages

- Inconvénients

- Conclusion

- VII. Conclusion

- VII-A. Comparaison

- Les bibliothèques

- Test de performances

- Mon expérience avec ces bibliothèques (sans gestion du cache)

- Leur gestion du cache

- VII-B. Classement (subjectif)

- VII-C. Épilogue

- VII-D. Téléchargement

- VII-E. Liens

I - Introduction

I-A - Remerciements

Un grand merci à GrandFather pour ses remarques constructives, à hugo123 pour sa participation et sa patience ainsi qu'à eric190 et dje3000 pour leur précieux soutien. Merci également aux auteurs et mainteneurs qui ont pris le temps de me corriger : Skrol29 (TinyButStrong), François Campana (VTemplate), Rémy Trichard (ModeliXe)…

I-B - Préambule

In the end you will always have an intersection point between presentation and logic.  There is no way around it since the final thing you present is a merge of the two.  The whole question of templating is about where you put this intersection point.
- Rasmus Lerdorf (créateur de PHP) dans la mailing list principale de PHP (http://www.php.net/).

C'est exactement ce qui transparaîtra de ce tutoriel : certains moteurs ont des gabarits passifs, tandis que d'autres déportent une grande partie de la logique applicative dans le fichier de gabarit.

Nous verrons les avantages et les inconvénients des deux méthodes (laisser le script PHP en charge de la logique ou bien la déporter dans le gabarit) au travers d'une exploration sommaire des principaux moteurs du marché.

GrandFather a porté mon attention sur une distinction faite couramment par les gens : un système de templates désigne les scripts construits selon l'idée de Rasmus (en PHP, mais avec peu de code dans les fichiers HTML, il suffit donc d'exécuter le code qui peut être autonome), tandis que le moteur de templates met en place une méthode fondée sur une syntaxe spécifique (non PHP) qui doit donc être interprété par une classe PHP. Je me concentrerai ici sur cette seconde catégorie.

Cet article n'est pas un tutoriel complet sur chaque moteur. Au contraire, ce n'est qu'un survol de ce qu'il est possible de faire avec chacun d'eux en vue d'atteindre un objectif précis.

I-C - La démarche de comparaison

Objectif : prendre un document HTML et le reproduire à l'aide de chaque moteur de gabarit de manière à séparer la logique applicative de la présentation du document. Il s'agit de la page d'accueil de mon site sur Developpez.com. Cela permet de comparer les variables, les boucles et les imbrications, ce qui couvre l'ensemble de ce que je demande à un outil de ce style.

Cela ne permet pas de tester tous les aspects de chaque moteur, mais je préfère me concentrer sur les fonctionnalités en commun et qui sont utiles pour l'objectif que je me suis fixé.

De mon point de vue, l'avantage des moteurs de gabarit réside dans la possibilité de distinguer le script PHP du code du document final (au format HTML ou tout autre format texte) : il s'agit de la fameuse séparation entre la logique et la présentation, fondamentale pour adopter une approche MVC ou pour répartir les tâches entre les intervenants dans un projet.

J'estime qu'un graphiste doit pouvoir se concentrer sur l'aspect visuel de la page Web sans devoir apprendre un langage de programmation pour y parvenir. À ce titre, le choix final sera amplement influencé par la simplicité de la syntaxe du gabarit.

Voici l'arborescence du projet :

- C:\Web\offline\shared\ : Les bibliothèques téléchargées ;

- C:\Web\offline\sites\comparatifs\ : Les classes personnalisées à inclure dans notre projet ainsi que les fichiers de gabarit et les fichiers de cache ;

- C:\Web\online\comparatifs\ : Tous les éléments qui doivent être rendus publics.

Les scripts du jeu de démo (sans moteur de gabarit) :

- online\comparatifs\void-cache.php : Il affiche la version statique - HTML pur ;

- online\comparatifs\void-nocache.php : Il inclut les scripts (cf. ci-dessous) qui génèrent la version dynamique - PHP et base de données ;

- offline\sites\comparatifs\header.php : Il affiche l'en tête du document dynamique (quelques variables) ;

- offline\sites\comparatifs\home.php : Il affiche le corps du document dynamique (quelques boucles) ;

- offline\sites\comparatifs\footer.php : Il affiche le pied du document dynamique (rien de particulier).

void-nocache.php

1. <?php

3. function html($string){

4. return htmlentities($string, ENT_QUOTES, 'ISO-8859-1');

5. }

7. define('DIR_OFFLINE', 'C:/Web/offline/sites/comparatifs/void/');

9. include(DIR_OFFLINE.'home.php');

11. ?>

home.php

1. <?php

3. require_once(realpath(dirname(__FILE__).'/../db-connect.php'));

4. include('header.php');

6. ?>

7. <div id="alignement" style="text-align: center;">

8. <a href="http://g-rossolini.developpez.com/" title="Cours par Guillaume Rossolini"

9. ><img src="images/cours.png" alt="Les cours de Developpez.com" style="border: 1;" /></a>

10. <br /><br />

12. <?php

13. $sql = 'SELECT id, title

14. FROM subject';

15. $subjects = mysql_query($sql) or die(mysql_error());

16. while($subject = mysql_fetch_assoc($subjects))

17. {

18. ?>

19. <div class="bloc_cours">

20. <div class="titre_cours"><?php echo html($subject['title']); ?></div>

21. <?php

22. $sql = 'SELECT id, title

23. FROM category

24. WHERE subject_id = '.$subject['id'];

25. $categories = mysql_query($sql) or die(mysql_error());

26. while($category = mysql_fetch_assoc($categories))

27. {

28. ?>

29. <div class="categorie_cours"><?php echo html($category['title']) ?></div>

30. <div class="liste_cours">

31. <ul>

32. <?php

33. $sql = 'SELECT id, uri, title, description

34. FROM tutorial

35. WHERE category_id = '.$category['id'];

36. $tutorials = mysql_query($sql) or die(mysql_error());

37. while($tutorial = mysql_fetch_assoc($tutorials))

38. {

39. ?>

40. <li>

41. <a href="<?php echo html($tutorial['uri']); ?>"

42. ><?php echo html($tutorial['title']); ?></a> :

43. <?php echo html($tutorial['description']); ?>

44. </li>

45. <?php

46. }

47. ?>

48. </ul>

49. </div>

50. <hr />

51. <?php

52. }

53. ?>

54. </div>

55. <br />

56. <br />

57. <?php

58. }

59. ?>

61. <p id="validation">

62. <a href="http://validator.w3.org/check?uri=referer"

63. ><img src="http://www.w3.org/Icons/valid-xhtml11" alt="Valide XHTML 1.1" /></a>

64. <a href="http://jigsaw.w3.org/css-validator/check/referer"

65. ><img src="http://jigsaw.w3.org/css-validator/images/vcss" alt="Valide CSS !" /></a>

66. </p>

67. </div>

68. <?php

70. include('header.php');

72. ?>

header.php

1. <?php

3. require_once(realpath(dirname(__FILE__).'/../db-connect.php'));

6. $result = mysql_query('SELECT name, value FROM header') or die(mysql_error());

7. while($header = mysql_fetch_assoc($result))

8. {

9. $headers[$header['name']] = $header['value'];

10. }

13. $result = mysql_query('SELECT name, value FROM meta') or die(mysql_error());

14. while($tmp = mysql_fetch_assoc($result))

15. {

16. $meta[$tmp['name']] = $tmp['value'];

17. }

20. $result = mysql_query('SELECT name, value FROM css') or die(mysql_error());

21. while($tmp = mysql_fetch_assoc($result))

22. {

23. $css[$tmp['name']] = $tmp['value'];

24. }

26. ?>

27. <?xml version="1.0" encoding="iso-8859-1"?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "xhtml11.dtd">

28. <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr">

29. <head>

30. <title><?php echo html($headers['title']); ?></title>

31. <meta http-equiv="Content-Type" content="text/html; charset=<?php echo html($headers['charset']); ?>" />

33. <meta name="generator" content="<?php echo html($meta['generator']); ?>" />

34. <meta name="description" content="<?php echo html($meta['description']); ?>" />

35. <meta name="keywords" content="<?php echo html($meta['keywords']); ?>" />

36. <meta name="MS.LOCALE" content="<?php echo html($meta['mslocale']); ?>" />

38. <link rel="stylesheet" type="text/css" href="<?php echo html($css['mainstyle']); ?>" />

39. <link rel="stylesheet" type="text/css" media="print" href="<?php echo html($css['printer']); ?>" />

40. <link rel="stylesheet" type="text/css" media="screen" href="<?php echo html($css['ridan']); ?>" />

41. </head>

43. ...

void/footer.php

...

I-D - Consignes générales d'installation

J'ai souvent une manière très spéciale de configurer mon environnement… Je conçois que tout le monde n'adhère pas à ma vision des choses ; pourtant, si vous souhaitez suivre cet article et en utiliser les sources, il faudra vous plier à mon organisation.

J'utilise principalement trois répertoires :

- C:\Web\offline\shared\ : Il centralise toutes les bibliothèques que j'utilise, ce qui me permet de les partager d'un site à l'autre ;

- C:\Web\offline\sites\comparatifs\ : C'est le répertoire qui correspond aux scripts du site « comparatifs » qui ne doivent pas être accessibles par une URI : il s'agit généralement de scripts de configuration, de classes personnalisées ou de fichiers temporaires ;

- C:\Web\online\comparatifs\ : C'est mon document_root, il contient toutes les ressources qui doivent être accessibles par une URI publique.

Vous pouvez aussi bien configurer votre document_root que modifier les scripts pour qu'ils acceptent votre configuration.

I-E - Informations générales sur les tests de performances

Les tests de performances sont effectués à l'aide du logiciel ApacheBench, fourni en standard avec httpd (Apache).

Les chiffres en eux-mêmes n'ont aucun sens, c'est pourquoi je donnerai toutes les valeurs relativement à une valeur de référence (appelée void) : le temps de chargement de la page de test. Je ne donnerai les logs produits par les tests qu'à titre informatif.

Ainsi, les valeurs des tests de performances sont des coefficients par rapport au temps d'exécution des scripts de référence (void).

Idéalement, les moteurs disposant d'une gestion du cache devraient produire des pages plus rapidement que le script de référence void-nocache.php (puisque ces moteurs évitent les appels à la base de données).

I-F - La base de données utilisée

Il serait peu cohérent de faire un test de performances de gabarits sans récupérer les informations depuis une base de données, surtout si le moteur gère la mise en cache des documents produits.

J'ai utilisé une base MySQL : pas de SQLite possible, car il utilise le système de fichiers sans daemon/service (pour rappel, le système de fichiers est plus rapide qu'un daemon/service), ce qui fausse les statistiques.

Structure :

CREATE TABLE header
(
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(25) NOT NULL,
value VARCHAR(255) NOT NULL
);

CREATE TABLE meta
(
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(25) NOT NULL,
value VARCHAR(255) NOT NULL
);

CREATE TABLE css
(
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(25) NOT NULL,
value VARCHAR(255) NOT NULL
);

CREATE TABLE subject
(
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL
);

CREATE TABLE category
(
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
subject_id INT UNSIGNED NOT NULL,
title VARCHAR(255) NOT NULL
);

CREATE TABLE tutorial
(
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
category_id INT UNSIGNED NOT NULL,
uri VARCHAR(255) NOT NULL,
title VARCHAR(255) NOT NULL,
description VARCHAR(255) NOT NULL
);

Données :

INSERT INTO header
(id, name, value)
VALUES
(NULL, "charset", "ISO-8859-1"),
(NULL, "title", "Guillaume Rossolini - Cours Web & PHP - Club d'entraide des développeurs francophones");

INSERT INTO meta
(id, name, value)
VALUES
(NULL, "generator", "developpez-com"),
(NULL, "description", "Guillaume Rossolini - Cours Web & PHP"),
(NULL, "keywords", ""),
(NULL, "mslocale", "fr-FR");

INSERT INTO css
(id, name, value)
VALUES
(NULL, "mainstyle", "http://www.developpez.com/mainstyle-xhtml.css"),
(NULL, "printer", "http://www.developpez.com/template/printer-xhtml.css"),
(NULL, "ridan", "http://g-rossolini.developpez.com/ridan.css");

INSERT INTO subject
(id, title)
VALUES
(NULL, "Webmarketing"),
(NULL, "PHP");

INSERT INTO category
(id, title, subject_id)
VALUES
(NULL, "Introduction", 1),
(NULL, "Tutoriels", 1),
(NULL, "Logiciels", 2),
(NULL, "Tutoriels", 2),
(NULL, "Reportages", 2);

INSERT INTO tutorial
(
id, category_id,
uri,
title,
description
)
VALUES
(
NULL, 1,
"http://g-rossolini.developpez.com/tutoriels/seo/",
"Search Engine Optimization (SEO)",
"introduction aux techniques de référencement d'une page Web"
),
(
NULL, 2,
"http://g-rossolini.developpez.com/tutoriels/seo/white-hat-techniques/",
"Techniques de SEO dites de \"white hat\"",
"vue d'ensemble des bonnes pratiques de SEO pour améliorer le référencement de vos pages"
),
(
NULL, 2,
"http://g-rossolini.developpez.com/tutoriels/seo/url-rewriting/",
"Réécriture de liens (URL Rewriting)",
"améliorez le référencement de vos pages avec cette technique"
),
(
NULL, 2,
"http://g-rossolini.developpez.com/tutoriels/seo/changer-d-url-rewriting/",
"Faire évoluer sa réécriture de liens",
"faites face à un changement dans votre politique de liens sans perdre votre référencement"
),
(
NULL, 3,
"http://g-rossolini.developpez.com/tutoriels/logiciels/phpedit-2-6/",
"PHPEdit",
"un IDE complet pour développer en PHP (coloration syntaxique, complétion de code, aide intégrée, nombreux plugins, etc.)"
),
(
NULL, 4,
"http://g-rossolini.developpez.com/tutoriels/logiciels/alterner-entre-plusieurs-php/",
"Alterner entre plusieurs versions d'Apache et de PHP",
"utilisez plusieurs versions de PHP pour tester vos scripts dans toutes les conditions"
),
(
NULL, 4,
"http://g-rossolini.developpez.com/tutoriels/php/expressions-regulieres/",
"Les expressions régulières",
"les expressions rationnelles (ou encore regex) expliquées avec des exemples concrets"
),
(
NULL, 4,
"http://g-rossolini.developpez.com/tutoriels/php/site-dynamique/",
"Site dynamique et classes d''abstraction",
"reprise du cours de Pierre-Baptiste pour utiliser un système de gabarits (templates) et PDO"
),
(
NULL, 4,
"http://g-rossolini.developpez.com/tutoriels/php/les-formulaires-et-php5/",
"Les formulaires et PHP",
"toutes les informations sur la gestion des formulaires en PHP"
),
(
NULL, 5,
"http://g-rossolini.developpez.com/reportages/conference-internationale-php-2006/",
"Conférence Internationale PHP 2006",
"à Frankfurt-Mörfelden (Allemagne)"
);

Il pourrait y avoir un travail supplémentaire sur les relations entre les tables, mais ce n'est pas notre propos ici. Cette base de données sera suffisante.

Par ailleurs, le script suivant est inclus par les divers scripts qui ont besoin de la base de données :

offline\sites\comparatifs\db-connect.php

1. <?php

3. mysql_connect('localhost', 'root', '') or die(mysql_error());

4. mysql_select_db('developpez') or die(mysql_error());

6. ?>

II - ModeliXe

II-A - Présentation

Le projet

ModeliXe est un projet français qui a été transmis à Rémy Trichard en 2004. La liste des contributeurs est longue et j'y ai retrouvé des noms que je connaissais : Sébastien Hordeaux (créateur de Waterpoof/PHPEdit) et Frédéric Bouchery (ancien blogueur aux billets intéressants).

Après un long séjour sur le site personnel de Rémy, le projet a maintenant migré vers SourceForge.

Installation

Arborescence du projet :

- C:\Web\offline\shared\ModeliXe\ : La bibliothèque et son gestionnaire d'erreurs (ModeliXe.php et ErrorManager.php) ;

- C:\Web\offline\sites\comparatifs\modelixe\ : La classe personnalisée et le script de configuration (MyModeliXe.php et Mxconf.php) ;

- C:\Web\online\comparatifs\ : Les scripts à charger par le navigateur (modelixe-pear-nocache.php, modelixe-xml-nocache.php, modelixe-pear-cache.php et modelixe-xml-cache.php).

Avec la panoplie de contributeurs, je m'attendais à une installation sans douleur. Il n'en a malheureusement pas été ainsi…

Voici comment j'ai procédé : j'ai déclaré dans mon code les deux constantes conseillées, j'ai désactivé les vérifications automatiques et j'ai utilisé les constantes de manière explicite.

MyModeliXe.php

define('MX_GENERAL_PATH', realpath(dirname(__FILE__)).'/');
define('MX_ERROR_PATH', 'C:/Web/offline/shared/ModeliXe/');

ModeliXe.php - Original

//Initialisation de l'include_path
$os = ((stristr(getenv('SERVER_SOFTWARE'), 'win') || stristr(getenv('SERVER_SOFTWARE'), 'microsoft'))? ';' : ':');
$path = ((defined('MX_GENERAL_PATH'))? MX_GENERAL_PATH : '').$os.((defined('MX_ERROR_PATH'))? MX_ERROR_PATH : '').$os.ini_get('include_path');
ini_set('include_path', $path);

//Inclusion du fichier de configuration et de Error Manager
require_once('Mxconf.php');
require_once('ErrorManager.php');

ModeliXe.php - Correction

//Initialisation de l'include_path
//$os = ((stristr(getenv('SERVER_SOFTWARE'), 'win') || stristr(getenv('SERVER_SOFTWARE'), 'microsoft'))? ';' : ':');
//$path = ((defined('MX_GENERAL_PATH'))? MX_GENERAL_PATH : '').$os.((defined('MX_ERROR_PATH'))? MX_ERROR_PATH : '').$os.ini_get('include_path');
//ini_set('include_path', $path);

//Inclusion du fichier de configuration et de Error Manager
require_once(MX_GENERAL_PATH.'Mxconf.php');
require_once(MX_ERROR_PATH.'ErrorManager.php');

Depuis PHP 5.2, les expressions rationnelles classiques POSIX ne sont plus incluses qu'en PECL (afin de laisser la place à l'extension PCRE), ce qui rend indisponible la fonction ereg() utilisée par la classe de gestion d'erreurs de ModeliXe, ErrorManager. Il faut donc apporter la modification suivante :

ErrorManager::SetErrorOut() - Original

ereg('http://', $url)

ErrorManager::SetErrorOut() - Correction

preg_match('`http://`', $url)

Il est possible que tout le monde n'ait pas de problème avec la version originale de ModeliXe ; seulement, que j'aie rencontré un souci signifie que toutes les configurations ne supportent pas ModeliXe et qu'il est donc préférable d'apporter les modifications mentionnées ici. D'autre part, Julian Dolby serait de cet avis également…

Héritage de la classe

Voici la classe que je vous propose d'utiliser :

MyModeliXe.php

1. <?php

3. //

4. // Constantes requises par ModeliXe

5. //

6. define('MX_GENERAL_PATH', realpath(dirname(__FILE__)).'/');

7. define('MX_ERROR_PATH', 'C:/Web/offline/shared/ModeliXe/');

9. require(MX_ERROR_PATH.'ModeliXe.php');

11. class MyModeliXe extends ModeliXe

12. {

13. var $caching;

15. function MyModeliXe($template, $file, $caching)

16. {

17. if($caching)

18. {

19. $this->caching = TRUE;

20. parent::ModeliXe($file, '', '', 180);

21. }

22. else

23. {

24. $this->caching = FALSE;

25. parent::ModeliXe($file, '', '', 0);

26. }

28. //

29. // Le balisage du gabarit : "pear" ou bien "xml"

30. //

31. switch($template)

32. {

33. case 'pear':

34. case 'xml':

35. $this->SetMxFlagsType($template);

36. break;

38. default:

39. $this->SetMxFlagsType('xml');

40. break;

41. }

43. //

44. // Configuration

45. //

46. $this->SetMxOutputType('xhtml');

47. $this->SetMxTemplatePath(MX_GENERAL_PATH.'templates/'.$template.'/html/');

48. $this->SetMxCachePath(MX_GENERAL_PATH.'templates/'.$template.'/cache/');

49. $this->SetMxCompress('off');

50. $this->SetMxModRewrite('off');

51. $this->SetMxSignature('off');

53. //

54. // Initialisation

55. //

56. $this->SetModeliXe();

57. }

60. function getHtml($string)

61. {

62. return htmlentities($string, ENT_QUOTES, 'ISO-8859-1');

63. }

64. }

66. ?>

Le constructeur attend le nom du gabarit (dans mon exemple, il s'agit de « pear » ou bien « xml »), du nom du fichier de gabarit (« *.tpl ») et enfin d'un booléen déterminant si l'on souhaite utiliser le cache.

Le code suivant permet de créer l'objet dans nos scripts :

modelixe.php

1. <?php

3. define('MX_TEMPLATE', 'pear'); // 'pear' ou 'xml'

4. define('MX_CACHING', TRUE);

6. require('C:/Web/offline/sites/comparatifs/modelixe/MyModeliXe.php');

8. $template = new MyModeliXe(MX_TEMPLATE, 'home.tpl', MX_CACHING);

10. //

11. // Envoi de données au gabarit

12. //

14. $template->MxWrite();

16. ?>

II-B - Utilisation

ModeliXe propose deux manières d'écrire les éléments du gabarit : en suivant le standard proposé par le groupe PEAR (Smarty) ou bien selon son format natif : XML. C'est pour cette raison que j'ai mis en place deux gabarits.

Je vous présenterai systématiquement les deux notations (les deux gabarits). Les tests seront faits sur les deux versions également.

Les deux versions s'utilisent de la même manière dans le code PHP, grâce à notre classe dérivée.

Variables

Les variables sont envoyées dans des éléments « text » du gabarit au moyen de la méthode MxText().

header.tpl (XML)

<meta name="generator" content="<mx:text id="meta_generator"/>" />
<meta name="description" content="<mx:text id="meta_description"/>" />
<meta name="keywords" content="<mx:text id="meta_keywords"/>" />
<meta name="MS.LOCALE" content="<mx:text id="meta_mslocale"/>" />

header.tpl (PEAR)

<meta name="generator" content="{text id="meta_generator"}" />
<meta name="description" content="{text id="meta_description"}" />
<meta name="keywords" content="{text id="meta_keywords"}" />
<meta name="MS.LOCALE" content="{text id="meta_mslocale"}" />

header.php

$template->MxText('meta_generator', $template->getHtml($meta['generator']));
$template->MxText('meta_description', $template->getHtml($meta['description']));
$template->MxText('meta_keywords', $template->getHtml($meta['keywords']));
$template->MxText('meta_mslocale', $template->getHtml($meta['mslocale']));

Blocs

home.tpl (XML)

<mx:bloc id="subject">
<div class="bloc_cours">
<div class="titre_cours"><mx:text id="title"/></div>
</div>
<br />
<br />
</mx:bloc id="subject">

home.tpl (PEAR)

{start id="subject"}
<div class="bloc_cours">
<div class="titre_cours">{text id="title"}</div>
</div>
<br />
<br />
{end id="subject"}

home.php

$sql = 'SELECT id, title
FROM subject';
$subjects = mysql_query($sql) or die(mysql_error());
while($subject = mysql_fetch_assoc($subjects))
{
$template->MxText('subject.title', $template->getHtml($subject['title']));

$template->MxBloc('subject', 'loop');
}

Imbrication

home.tpl (XML)

<mx:bloc id="subject">
<div class="bloc_cours">
<div class="titre_cours"><mx:text id="title"/></div>
<mx:bloc id="category">
<div class="categorie_cours"><mx:text id="title"/></div>
<div class="liste_cours">
<ul>
<mx:bloc id="tutorial">
<li>
<a href="<mx:text id="uri"/>"
><mx:text id="title"/></a> : <mx:text id="description"/>
</li>
</mx:bloc id="tutorial">
</ul>
</div>
<hr />
</mx:bloc id="category">
</div>
<br />
<br />
</mx:bloc id="subject">

home.tpl (PEAR)

{start id="subject"}
<div class="bloc_cours">
<div class="titre_cours">{text id="title"}</div>
{start id="category"}
<div class="categorie_cours">{text id="title"}</div>
<div class="liste_cours">
<ul>
{start id="tutorial"}
<li>
<a href="{text id="uri"}"
>{text id="title"}</a> : {text id="description"}
</li>
{end id="tutorial"}
</ul>
</div>
<hr />
{end id="category"}
</div>
<br />
<br />
{end id="subject"}

home.php

$sql = 'SELECT id, title
FROM subject';
$result = mysql_query($sql) or die(mysql_error());
while($subject = mysql_fetch_assoc($result))
{
$template->MxText(
'subject.title',
$template->getHtml($subject['title']));

$sql = 'SELECT id, title
FROM category
WHERE subject_id = '.$subject['id'];
$categories = mysql_query($sql) or die(mysql_error());
while($category = mysql_fetch_assoc($categories))
{
$template->MxText(
'subject.category.title',
$template->getHtml($category['title']));

$sql = 'SELECT id, uri, title, description
FROM tutorial
WHERE category_id = '.$category['id'];
$tutorials = mysql_query($sql) or die(mysql_error());
while($tutorial = mysql_fetch_assoc($tutorials))
{
$template->MxText(
'subject.category.tutorial.uri',
$template->getHtml($tutorial['uri']));

$template->MxText(
'subject.category.tutorial.title',
$template->getHtml($tutorial['title']));

$template->MxText(
'subject.category.tutorial.description',
$template->getHtml($tutorial['description']));

$template->MxBloc('subject.category.tutorial', 'loop');
}

$template->MxBloc('subject.category', 'loop');
}

$template->MxBloc('subject', 'loop');
}

Alternance

Il nous faut utiliser la méthode déjà mentionnée ci-dessus du modulo :

home.tpl (XML)

<a href="<mx:text id="uri"/>"
><mx:text id="title"/></a> :
<span style='color: <mx:text id="style"/>;'><mx:text id="description"/></span>

home.tpl (PEAR)

<a href="{text id="uri"}"
>{text id="title"}</a> :
<span style="color: {text id="style"};">{text id="description"}</span>

home.php

if($i % 2)
{
$style = 'green';
}
else
{
$style = 'blue';
}

$template->MxText(
'subject.category.tutorial.style',
$style);

Segmentation du gabarit

ModeliXe a opté pour une solution similaire à celle de Smarty. En effet, le fichier de gabarit « maître » contient des blocs (en notation PEAR ou XML) qui indiquent à quel endroit il faut fusionner le gabarit externe :

home.tpl (notation PEAR) :

{start id="header"}{end id="header"}

home.tpl (notation XML) :

<mx:bloc id="header"></mx:bloc id="header">

Le code PHP doit ensuite savoir à quel endroit il peut trouver le gabarit à fusionner :

modelixe-pear-nocache.php

$template = new MyModeliXe(MX_TEMPLATE, 'home.tpl', MX_CACHING);
$template->MxBloc('header', 'modi', $template->mXTemplatePath.'header.tpl');
$template->MxBloc('footer', 'modi', $template->mXTemplatePath.'footer.tpl');

Le paramètre « modi » signifie que l'on souhaite remplacer le bloc du gabarit par un gabarit externe.

Les variables du gabarit fusionné sont décalées dans un espace de noms interne (le handle version ModeliXe), ici « header » :

$template->MxText('header.meta_generator', $template->getHtml($meta['generator']));
$template->MxText('header.meta_description', $template->getHtml($meta['description']));
$template->MxText('header.meta_keywords', $template->getHtml($meta['keywords']));
$template->MxText('header.meta_mslocale', $template->getHtml($meta['mslocale']));

Dans le gabarit, cette distinction est automatique. Seul le code PHP est affecté.

Gestion du cache

ModeliXe gère automatiquement le cache des informations dynamiques du gabarit, il n'y a donc pas à trop s'en occuper.

Cependant, il ne peut pas éviter les appels à la base de données. Il peut seulement éviter de fusionner le gabarit et les variables PHP en réutilisant le fichier mis en cache. Cela n'évite pas les appels à la base de données, or c'est l'opération la plus coûteuse.

Il nous faut donc utiliser un système permettant de savoir si le fichier est déjà en cache. Selon le cas, nous nous connecterons à la base de données ou nous réutiliserons le document existant.

Cela se fait au moyen de la méthode MxCheckCache() :

if($template->MxCheckCache())
{
//
// Mettre ici les requêtes de récupération des données, l'envoi des données au gabarit, etc.
//
}

Cet appel est le même pour tous les segments du document, puisqu'il n'y a qu'un seul fichier de cache pour tout le document final. Cela ne nous empêche pas de pouvoir appeler cette méthode dans chacun de nos scripts, si nécessaire.

ModeliXe ne produit qu'un seul fichier de cache qui est le document complet final. Il ne semble pas en mesure de gérer le cache par parties.

II-C - Mon avis

Avantages

De nombreuses personnes connues ont participé à l'élaboration de ModeliXe, ce qui donne un a priori favorable.

Il a une gestion du cache.

Inconvénients

La configuration de ModeliXe repose sur des constantes du scope global plutôt que sur des constantes de classes.

Le gabarit en XML est trompeur, dans la mesure où des variables doivent parfois être insérées dans le document à l'intérieur des propriétés des balises du gabarit : pourquoi écrire ces variables avec une syntaxe XML à cet endroit ?

Syntaxe XML douteuse :

<meta name="generator" content="<mx:text id="meta_generator"/>" />

Conclusion

ModeliXe me semble peu intéressant, dans la mesure où il n'apporte pas de nouveauté par rapport aux autres solutions, le support est à l'abandon et la communauté est inexistante.

Un résumé est disponible en fin d'article.

III - phpBB2 (PHP Base Library - phplib)

III-A - Présentation

Les projets

PHP Base Library est l'une des premières bibliothèques permettant d'utiliser un système de gabarits.

L'idée de la phplib est d'utiliser des fichiers de gabarit (l'extension classique est « .ihtml ») qui contiennent le code HTML utilisé. Ces gabarits sont de véritables patrons, ils définissent uniquement une structure ou des éléments de structure. C'est un moule qui ne comporte aucun élément de logique.

phpBB2 est un script de forum très populaire.

Il y a de cela plusieurs années, le phpBB Group a effectué un très gros travail autour du template de la PHPLib afin de le rendre plus performant. C'est la version phpBB2 (dont le script de forum est actuellement en bêta) que je vais utiliser ici, plutôt que la version PHPLib qui manque de maturité. L'extension de fichier est « .tpl ».

Installation

Extraire le script offline/template.php de l'archive de la dernière version de phpBB2, puis le déplacer dans un répertoire accessible par les utilisateurs du serveur (mais situé hors de la racine du serveur Web). La classe utilise ne fonction globale de phpBB, il faut donc s'en défaire : remplacez « phpbb_realpath » par « realpath ».

Arborescence du projet :

- C:\Web\offline\shared\phpBB2\ : La bibliothèque (template.php) ;

- C:\Web\offline\comparatifs\phpbb2\ : La classe personnalisée (MyTemplate.php) ;

- C:\Web\online\comparatifs\ : Le script à charger par le navigateur (phpbb2.php)

Héritage de la classe

Pour reprendre la méthode utilisée avec Smarty, je vous propose de déclarer notre propre classe qui étend Template en la configurant selon nos besoins. Dans cette démonstration, nous n'aurons pas besoin de certaines possibilités offertes par Template, nous pouvons donc simplifier les appels aux méthodes set_filenames() et pparse(). De plus, nous allons ajouter une méthode pour simplifier la conversion du texte en entités HTML.

MyTemplate.php

1. <?php

3. require('C:/Web/offline/shared/phpBB2/template.php');

5. class MyTemplate extends Template

6. {

7. function MyTemplate($template_name)

8. {

9. $basedir = str_replace('\\', '/', dirname(__FILE__))

10. .'/templates/'.$template_name;

12. $this->set_rootDir($basedir);

13. }

16. function getHtml($string)

17. {

18. return htmlentities($string, ENT_QUOTES, 'ISO-8859-1');

19. }

20. }

22. ?>

Le constructeur attend uniquement le nom du fichier de gabarit.

Le code suivant permet de créer l'objet dans nos scripts :

phpbb2.php

define('DIR_OFFLINE', 'C:/Web/offline/comparatifs/phpbb2/');
define('PHPBB2_TEMPLATE', 'default');

require(DIR_OFFLINE.'MyTemplate.php');

$template = new MyTemplate(PHPBB2_TEMPLATE);

//
// Série d'appels à $template->set_filenames() suivis d'envois de données et de $template->pparse()
//

III-B - Utilisation

Le moteur utilise des commentaires HTML pour faire la liaison entre PHP et les blocs à reproduire. Les variables sont encadrées par des accolades, ce qui ne nous dépayse pas des autres moteurs du marché.

Pour utiliser le gabarit, il faut commencer par dire quel fichier de gabarit nous souhaitons utiliser. Cela se fait au moyen de la méthode set_filenames() :

phpbb2.php

$template->set_filenames(array('header' => 'header.tpl'));

// Mettre ici l'envoi des données au gabarit

$template->pparse('header');

Cette méthode recherche un fichier de gabarit du même nom que le script PHP l'appelant (mais avec une extension .tpl au lieu de .php).

Variables

L'envoi de variables au fichier de gabarit se fait généralement à l'aide de la méthode assign_vars(). Par convention, les noms des variables du gabarit s'écrivent en majuscules :

header.php

$template->assign_vars(
array
(
'CHARSET' => $template->getHtml($header['charset']),

'META_GENERATOR' => $template->getHtml($meta['generator']),
'META_DESCRIPTION' => $template->getHtml($meta['description']),
'META_KEYWORDS' => $template->getHtml($meta['keywords']),
'META_MSLOCALE' => $template->getHtml($meta['mslocale']),

'CSS_MAINSTYLE' => $template->getHtml($css['mainstyle']),
'CSS_PRINTER' => $template->getHtml($css['printer']),
'CSS_RIDAN' => $template->getHtml($css['ridan']),

'TITLE' => $template->getHtml($header['title'])
)
);

Du côté du gabarit, voici comment cela se traduit :

header.tpl

...

<head>
<title>{TITLE}</title>
<meta http-equiv="Content-Type" content="text/html; charset={CHARSET}" />

<meta name="generator" content="{META_GENERATOR}" />
<meta name="description" content="{META_DESCRIPTION}" />
<meta name="keywords" content="{META_KEYWORDS}" />
<meta name="MS.LOCALE" content="{META_MSLOCALE}" />

<link rel="stylesheet" type="text/css" href="{CSS_MAINSTYLE}" />
<link rel="stylesheet" type="text/css" media="print" href="{CSS_PRINTER}" />
<link rel="stylesheet" type="text/css" media="screen" href="{CSS_RIDAN}" />
</head>

...

C'est une syntaxe plus pénible à écrire que dans le cas de Smarty. En contrepartie, nous avons davantage le contrôle sur ce qu'il se passe. Nous ne sommes pas contraints de préparer toutes les données à l'avance, nous pouvons les envoyer au fur et à mesure (même si ce n'est pas conseillé).

Blocs

Les blocs sont des portions du gabarit que l'on peut ignorer, afficher ou bien dupliquer comme on le souhaite. Par convention, les noms des blocs sont écrits en minuscules et les blocs conditionnels (affiché/caché) sont préfixés de « switch_ ».

Ici, nous n'utiliserons pas de bloc conditionnel.

L'affectation d'un bloc se fait au moyen de la méthode assign_block_vars() dans le code PHP :

home.php

foreach($subjects as $subject)
{
$template->assign_block_vars
(
'subject', // le nom du bloc
array('TITLE' => $template->getHtml($subject['title'])) // le contenu
);
}

Dans le gabarit, on retrouve le même nom de bloc dans un commentaire HTML. Un commentaire BEGIN et un autre END permettent de délimiter notre bloc. La notation pointée permet d'accéder aux variables :

home.tpl

<!-- BEGIN subject -->
<div class="bloc_cours">
<div class="titre_cours">{subject.TITLE}</div>
</div>
<br />
<br />
<!-- END subject -->

Il est possible d'accéder à une variable de niveau supérieur : c'est ce qui justifie la présence du nom de bloc devant le nom de la variable.

Imbrication

Le moteur de gabarit de phpBB2 permet d'imbriquer les blocs de la manière suivante :

home.php

$sql = 'SELECT id, title
FROM subject';
$result = mysql_query($sql) or die(mysql_error());
while($subject = mysql_fetch_assoc($result))
{
$template->assign_block_vars
(
'subject',
array('TITLE' => $template->getHtml($subject['title']))
);

$sql = 'SELECT id, title
FROM category
WHERE subject_id = '.$subject['id'];
$categories = mysql_query($sql) or die(mysql_error());
while($category = mysql_fetch_assoc($categories))
{
$template->assign_block_vars
(
'subject.category',
array('TITLE' => $template->getHtml($category['title']))
);

$sql = 'SELECT id, uri, title, description
FROM tutorial
WHERE category_id = '.$category['id'];
$tutorials = mysql_query($sql) or die(mysql_error());
while($tutorial = mysql_fetch_assoc($tutorials))
{
$template->assign_block_vars
(
'subject.category.tutorial',
array
(
'URI' => $template->getHtml($tutorial['uri']),
'TITLE' => $template->getHtml($tutorial['title']),
'DESCRIPTION' => $template->getHtml($tutorial['description'])
)
);
}
}
}

home.tpl

<!-- BEGIN subject -->
<div class="bloc_cours">
<div class="titre_cours">{subject.TITLE}</div>
<!-- BEGIN category -->
<div class="categorie_cours">{subject.category.TITLE}</div>
<div class="liste_cours">
<ul>
<!-- BEGIN tutorial -->
<li>
<a href="{subject.category.tutorial.URI}"
>{subject.category.tutorial.TITLE}</a> :
{subject.category.tutorial.DESCRIPTION}
</li>
<!-- END tutorial -->
</ul>
</div>
<hr />
<!-- END category -->
</div>
<br />
<br />
<!-- END subject -->

Alternance

Avec le moteur de phpBB2, tout ce qui est « logique » est conservé dans le code PHP, fut-ce un test conditionnel. Ce qui se trouve dans le gabarit n'est rien de plus qu'un moule. Par conséquent, les alternances sont plus complexes à gérer qu'avec Smarty.

Il existe cependant une méthode pour arriver à un résultat similaire :

home.php

foreach($category['tutorials'] as $i => $tutorial)
{
$template->assign_block_vars
(
'subject.category.tutorial',
array
(
'URI' => $template->getHtml($tutorial['uri']),
'TITLE' => $template->getHtml($tutorial['title']),
'DESCRIPTION' => $template->getHtml($tutorial['description'])
)
);

if($i % 2)
{
$template->assign_block_vars('subject.category.tutorial.switch_odd', array());
}
else
{
$template->assign_block_vars('subject.category.tutorial.switch_even', array());
}
}

home.tpl

<!-- BEGIN tutorial -->
<li>
<a href="{subject.category.tutorial.URI}"
>{subject.category.tutorial.TITLE}</a> :
<!-- BEGIN switch_even -->
<span style="color: blue;">{subject.category.tutorial.DESCRIPTION}</span>
<!-- END switch_even -->
<!-- BEGIN switch_odd -->
<span style="color: green;">{subject.category.tutorial.DESCRIPTION}</span>
<!-- END switch_odd -->
</li>
<!-- END tutorial -->

Notez comme la variable DESCRIPTION du bloc subject.category.tutorial est affichée dans les blocs subject.category.tutorial.switch_even et son confrère sans que cela pose de souci.

Une autre méthode utilise une variable :

home.php

foreach($category['tutorials'] as $i => $tutorial)
{
if($i % 2)
{
$style = 'green';
}
else
{
$style = 'blue';
}

$template->assign_block_vars
(
'subject.category.tutorial',
array
(
'URI' => $template->getHtml($tutorial['uri']),
'TITLE' => $template->getHtml($tutorial['title']),
'DESCRIPTION' => $template->getHtml($tutorial['description']),
'STYLE' => $style
)
);
}

home.tpl

<!-- BEGIN tutorial -->
<li>
<a href="{subject.category.tutorial.URI}"
>{subject.category.tutorial.TITLE}</a> :
<span style="color: {subject.category.tutorial.STYLE};">{subject.category.tutorial.DESCRIPTION}</span>
</li>
<!-- END tutorial -->

Cette version est plus lisible, mais elle a l'inconvénient de contenir une valeur CSS dans le code PHP. En général, c'est quelque chose que nous aimerions éviter.

Segmentation du gabarit

Le moteur de phpBB2 permet d'inclure plusieurs fichiers de gabarit dans un seul objet Template. Il n'est pas nécessaire d'appeler la méthode pparse() pour un handle spécifique, ce qui permet une gestion souple comme par exemple envoyer le contenu d'un gabarit dans une variable d'un autre gabarit.

Dans le cas qui nous occupe ici, seule la segmentation nous intéresse : le moyen d'avoir des gabarits externes que l'on peut inclure au gabarit de la page actuelle.

Chaque gabarit chargé au moyen de la méthode set_filenames() étant indépendant grâce à son handle unique, nous pouvons charger plusieurs fichiers de gabarit en une fois :

$template = new MyTemplate(PHPBB2_TEMPLATE);

// Déclaration des gabarits utilisés + affectation à des handles
// Note : il est possible d'appeler set_filenames() à plusieurs reprises
$template->set_filenames(array(
'header' => 'header.tpl',
'home' => 'home.tpl',
'footer' => 'footer.tpl'));

//
// À mettre ici : envoi des données au handle "header"
//

// Affichage du handle "header"
$template->pparse('header');

//
// À mettre ici : envoi des données au handle "home"
//

// Affichage du handle "home"
$template->pparse('home');

//
// À mettre ici : envoi des données au handle "footer"
//

// Affichage du handle "footer"
$template->pparse('footer');

III-C - Mon avis

Avantages

Ce moteur utilise PHP et les commentaires HTML, ce qui le rend très facile d'accès (car le développeur et le graphiste n'ont pas besoin d'apprendre un nouveau langage ésotérique).

Ce moteur permet de faire la distinction entre la logique applicative et la couche de présentation.

Inconvénients

PHP < 5

Les messages d'erreurs pourraient être plus explicites.

Conclusion

J'aime beaucoup cette manière d'utiliser les templates. C'est simple à utiliser et l'idée des commentaires HTML est excellente, car il n'y a pas besoin d'apprendre une nouvelle syntaxe.

Un résumé est disponible en fin d'article.

IV - Smarty

IV-A - Présentation

Le projet

Smarty est un projet du framework PEAR, qui est connu pour sa formidable quantité d'extensions et de classes PHP.

L'idée est de déporter la logique d'affichage depuis le script PHP vers le gabarit. Smarty se charge de tout ce qui se rapproche de près ou de loin de l'affichage du contenu dans la page finale, ce qui inclut certains détails de programmation comme les boucles (par exemple pour parcourir les tableaux).

Smarty

Installation

Copiez le répertoire de Smarty dans un répertoire de préférence accessible par tous vos utilisateurs et situé hors de la racine du serveur Web.

Arborescence du projet :

- C:\Web\offline\shared\Smarty\ : La bibliothèque Smarty et ses dépendances ;

- C:\Web\offline\comparatifs\smarty\ : La classe personnalisée (MySmarty.php) ;

- C:\Web\offline\comparatifs\smarty\templates\default\cache : Les fichiers mis en cache par Smarty (CHMOD 0755) ;

- C:\Web\offline\comparatifs\smarty\templates\default\compiled : Les fichiers compilés par Smarty (CHMOD 0755) ;

- C:\Web\offline\comparatifs\smarty\templates\default\configuration : La configuration de Smarty (CHMOD 0555) ;

- C:\Web\offline\comparatifs\smarty\templates\default\html : Les fichiers de gabarit (CHMOD 0555) ;

- C:\Web\online\comparatifs\ : Les scripts à charger par le navigateur (smarty-nocache.php et smarty-cache.php).

Héritage de la classe

Il est préférable de dériver la classe Smarty pour la personnaliser selon notre projet.

MySmarty.php

1. <?php

3. require('C:/Web/offline/shared/Smarty/Smarty.class.php');

5. class MySmarty extends Smarty

6. {

7. function MySmarty($template, $caching)

8. {

9. $basedir = dirname(__FILE__).'/templates/'.$template;

11. $this->template_dir = $basedir.'/html';

12. $this->cache_dir = $basedir.'/cache';

13. $this->compile_dir = $basedir.'/compiled';

14. $this->config_dir = $basedir.'/configuration';

16. $this->caching = $caching;

18. if($this->caching)

19. {

20. $this->cache_lifetime = 180;

21. }

22. else

23. {

24. $this->_CachePath = NULL;

25. $this->_CacheLifeTime = NULL; // En secondes

26. }

27. }

28. }

30. ?>

Le code suivant permet de créer l'objet dans nos scripts :

smarty-nocache.php

define('DIR_OFFLINE', 'C:/Web/offline/comparatifs/smarty/');
define('SMARTY_TEMPLATE', 'default');
define('SMARTY_CACHING', FALSE);

require(DIR_OFFLINE.'MySmarty.php');

$template = new MySmarty(SMARTY_TEMPLATE, SMARTY_CACHING);

//
// Envoi de données au gabarit
//

$template->display('home.tpl');

IV-B - Utilisation

Variables

header.php

$template->assign('title',
"Guillaume Rossolini - Cours Web &amp; PHP -"
." Club d'entraide des développeurs francophones"
);

header.tpl

<title>{$title}</title>

Ce code indique à Smarty qu'il doit remplacer la variable $title du gabarit par la valeur qui lui est assignée (attention aux confusions, $title dans le gabarit n'est pas $title dans le script).

Avec le code suivant, Smarty convertit les chaînes en entités HTML (notez comme le &amp; est devenu &) :

header.php

$template->assign('title',
"Guillaume Rossolini - Cours Web & PHP -"
." Club d'entraide des développeurs francophones"
);

header.tpl

<title>{$title|escape:"htmlall"}</title>

Il est également possible d'envoyer des tableaux en utilisant la même méthode :

header.php

$template->assign('meta',
array
(
'generator' => 'developpez-com',
'description' => 'Guillaume Rossolini - Cours Web & PHP',
'keywords' => '',
'mslocale' => 'fr-FR'
)
);

header.tpl

<meta name="generator" content="{$meta.generator|escape:"htmlall"}" />
<meta name="description" content="{$meta.description|escape:"htmlall"}" />
<meta name="keywords" content="{$meta.keywords|escape:"htmlall"}" />
<meta name="MS.LOCALE" content="{$meta.mslocale|escape:"htmlall"}" />

Finalement, dans le code de notre script header.php, nous avons toute une série d'assignations aveugles :

Envoi de variables scalaires et de tableaux :

$template->assign('charset', $charset);
$template->assign('meta', $meta);
$template->assign('css', $css);
$template->assign('title', $title);

Blocs

Smarty propose deux manières d'utiliser des boucles dans le gabarit. Dans les exemples qui suivent, j'utiliserai strictement le même code PHP :

home.php

$template->assign('subjects', $subjects);

Dans un premier temps, voyons la section.

home.tpl

{section name=subject loop=$subjects}
<div class="bloc_cours">
<div class="titre_cours">{$subjects[subject].title|escape:"htmlall"}</div>
</div>
<br />
<br />
{/section}

Avec ce code, Smarty comprend qu'il doit parcourir le tableau PHP $subjects afin de créer un bloc name=subject du gabarit à chaque itération. L'attribut name de la boucle est la variable utilisée comme offset du tableau $subjects.

Il ne faut pas confondre les trois variables : $subjects en PHP, $subjects en Smarty et subject pour l'indice du tableau. Ce n'est pas ma méthode préférée, à cause de cette confusion.

Une autre solution consiste à utiliser une structure foreach : c'est une manière plus simple de parcourir les tableaux qu'avec une section.

home.tpl

{foreach from=$subjects item=subject}
<div class="bloc_cours">
<div class="titre_cours">{$subject.title|escape:"htmlall"}</div>
</div>
<br />
<br />
{/foreach}

Ici, nous avons quelque chose de plus semblable à ce dont nous avons l'habitude en PHP. L'inconvénient est que cela ne permet pas d'utiliser les options de la structure section (même si je n'en ai présenté aucune ici, sachez qu'elles existent).

Imbrication

Avec les deux exemples qui suivent, nous obtenons le même code HTML.

home.php

//
// Récupération des données
//

$sql = 'SELECT id, title
FROM subject';
$result = mysql_query($sql) or die(mysql_error());
$i = 0;
while($subject = mysql_fetch_assoc($result))
{
$subjects[$i] = $subject;
$sql = 'SELECT id, title
FROM category
WHERE subject_id = '.$subject['id'];
$categories = mysql_query($sql) or die(mysql_error());
$j = 0;
while($category = mysql_fetch_assoc($categories))
{
$subjects[$i]['categories'][$j] = $category;
$sql = 'SELECT id, uri, title, description
FROM tutorial
WHERE category_id = '.$category['id'];
$tutorials = mysql_query($sql) or die(mysql_error());
$k = 0;
while($tutorial = mysql_fetch_assoc($tutorials))
{
$subjects[$i]['categories'][$j]['tutorials'][$k] = $tutorial;
++$k;
}

++$j;
}

++$i;
}

//
// Envoi au gabarit
//

$template->assign('subjects', $subjects);

Voici section :

home.tpl

{section name=subject loop=$subjects}
<div class="bloc_cours">
<div class="titre_cours">{$subjects[subject].title|escape:"htmlall"}</div>
{section name=category loop=$subjects[subject].categories}
<div class="categorie_cours">{$subjects[subject].categories[category].title|escape:"htmlall"}</div>
<div class="liste_cours">
<ul>
{section name=tutorial loop=$subjects[subject].categories[category].tutorials}
<li>
<a href="{$subjects[subject].categories[category].tutorials[tutorial].uri|escape:"htmlall"}"
>{$subjects[subject].categories[category].tutorials[tutorial].title|escape:"htmlall"}</a> :
{$subjects[subject].categories[category].tutorials[tutorial].description|escape:"htmlall"}
</li>
{/section}
</ul>
</div>
<hr />
{/section}
</div>
<br />
<br />
{/section}

Voici maintenant foreach :

home.tpl

{foreach from=$subjects item=subject}
<div class="bloc_cours">
<div class="titre_cours">{$subject.title|escape:"htmlall"}</div>
{foreach from=$subject.categories item=category}
<div class="categorie_cours">{$category.title|escape:"htmlall"}</div>
<div class="liste_cours">
<ul>
{foreach from=$category.tutorials item=tutorial}
<li>
<a href="{$tutorial.uri|escape:"htmlall"}">{$tutorial.title|escape:"htmlall"}</a> :
{$tutorial.description|escape:"htmlall"}
</li>
{/foreach}
</ul>
</div>
<hr />
{/foreach}
</div>
<br />
<br />
{/foreach}

Alternance

Dans de nombreuses situations, il est souhaitable de pouvoir alterner entre deux ou trois valeurs. C'est majoritairement le cas des listes : alterner entre deux couleurs de fond permet de faciliter la lecture.

Smarty propose une solution simple : une fonction permet de le faire au sein du gabarit. Le code PHP n'est pas affecté par cet aspect de présentation, tout est laissé à la charge du graphiste.

Voici un exemple qui s'applique au code précédent :

home.tpl

{foreach from=$category.tutorials item=tutorial}
<li>
<a href="{$tutorial.uri|escape:"htmlall"}">{$tutorial.title|escape:"htmlall"}</a> :
<span style='color: {cycle values="blue,green"}'>{$tutorial.description|escape:"htmlall"}</span>
</li>
{/foreach}

En situation réelle, il faut utiliser des classes CSS et non l'attribut HTML style.

Segmentation du gabarit

Smarty, puisqu'il déporte tout ce qui est relatif à l'affichage vers le gabarit, préfère laisser PHP inclure les scripts de configuration et se charger d'inclure les fichiers de gabarit. C'est à cela que sert la fonction {include}.

Dans notre exemple, nous aurons donc ceci dans le gabarit home.tpl plutôt que dans les scripts PHP :

home.tpl

{include file="header.tpl"}

Gestion du cache

Smarty peut gérer la mise en cache du gabarit produit lors de l'exécution des scripts PHP. Utiliser la mise en cache permet d'accélérer la génération de la page puisque le document n'est pas reconstruit à partir de zéro, il suffit de réutiliser le document mis en cache.

Cependant, Smarty ne gère que l'affichage de notre page. Il ne gère ni la connexion à la base de données ni les requêtes SQL, qui consomment plus de temps processeur que la fusion des variables dans le gabarit source. Lorsque nous avons un document mis en cache, il serait donc intéressant de trouver un moyen de le charger sans exécuter les requêtes SQL (dont le retour ne sera pas utile, dans la mesure où le document est déjà prêt).

Cela se fait au moyen de la méthode is_cached() :

$template->is_cached('home.tpl')
{
//
// Mettre ici les requêtes de récupération des données, l'envoi des données au gabarit, etc.
//
}

Il faut utiliser le nom du fichier de gabarit lors de cet appel : Smarty gère la mise en cache au niveau du gabarit, chacun ayant son fichier de cache indépendant.

Smarty conserve des informations amusantes dans les fichiers de cache : je ne les ai pas étudiées par manque de temps, elles ressemblent à des variables PHP sérialisées.

IV-C - Fonctionnalités avancées

Je vais présenter ici les plugins qui me semblent les plus intéressants avec Smarty. Bien entendu, vous pouvez parfaitement développer les vôtres afin d'étendre les capacités du moteur…

Plugin {mailto} : Protection d'adresses e-mail

Note : Les exemples suivants sont tirés de la documentation officielle du projet Smarty.

La syntaxe fondamentale n'est pas très différente du HTML :

*.tpl

{mailto address="me@example.com" text="Envoyez-moi un e-mail"}

HTML généré

<a href="mailto:me@example.com" >Envoyez-moi un e-mail</a>

Cela semble inintéressant, car trop similaire au HTML…

Cependant, regardez maintenant :

*.tpl

{mailto address="me@example.com" encode="javascript_charcode"}

HTML généré

<script type="text/javascript" language="javascript">
<!--
{document.write(String.fromCharCode(60,97, ... snipped ...60,47,97,62))}
//-->
</script>

Il va sans dire que cela peut s'avérer très pratique pour protéger une adresse e-mail contre les robots spammeurs, tant qu'ils n'interprètent pas le JavaScript des pages Web.

Plugin {debug} : Débogage

Smarty permet de déboguer votre gabarit grâce à la propriété $debugging. Il suffit de la mettre à TRUE dans votre classe personnalisée pour voir apparaître une console JavaScript lors du chargement de la page. Vous pouvez ainsi parcourir la liste des gabarits utilisés ainsi que des variables assignées.

Une autre méthode de débogage consiste à utiliser la fonction {debug} dans le gabarit lui-même, ce qui permet d'avoir les informations relatives à un gabarit particulier.

Plugin {html_table} : Tableaux HTML

Avec le plugin {html_table}, Smarty peut construire un tableau HTML à partir d'une variable PHP de type array.

Extrait de la documentation (*.php) :

$smarty->assign(
'data',
array(1, 2, 3, 4, 5, 6, 7, 8, 9)
);

Extrait de la documentation (*.tpl) :

{html_table loop=$data cols=4}

Cet exemple produira un tableau comme celui-ci :

1

2

3

4

5

6

7

8

9

Plugin {insert} : En tête du document de sortie

Une bonne pratique en PHP est de systématiquement envoyer le Content-Type avec votre document, ainsi éventuellement que d'autres headers.

Smarty vous permet de le gérer au niveau du gabarit.

Repris de la documentation (*.php) :

function insert_header($params)
{
if(empty($params['content']))
{
die('Le paramètre "content" est obligatoire');
}

header($params['content']);
return;
}

Repris de la documentation (*.tpl) :

{insert name=header content="Content-Type: application/xml"}

Il faut définir une fonction dont le nom commence par « insert_ ». À noter qu'un seul paramètre est ici envoyé à la fonction, mais que l'on peut évidemment gérer davantage de situations.

Plugin {include} : Combination de gabarits

cf. ci-dessus.

Filtre de sortie

Puisque Smarty gère tout le document de sortie avec la fonction eval(), il lui est très facile d'appliquer une fonction globale à la manière des fonctions de cache de PHP.

Si l'on souhaite appliquer un post traitement à un document, par exemple la méthode décrite dans mon cours de réécriture dynamique de liens, il est préférable d'utiliser ce que Smarty appelle un « Output Filter » :

Exemple de filtre de sortie :

function rewrite_uris($html, &$smarty)
{
//
// Réécrire ici les liens contenus dans $html
//

return $html;
}

$smarty->register_outputfilter('rewrite_uris');

IV-D - Mon avis

Avantages

La classe fondamentale est simple à réutiliser. Il suffit de placer le répertoire de Smarty dans un dossier accessible par tous les utilisateurs d'un même serveur, puis de dériver la classe Smarty pour chaque site Web hébergé sur ce serveur.

Le code PHP est épuré de toute présentation. Cela le rend bien plus lisible, c'est indéniable.

Il est évident que Smarty est prévu pour utiliser une mise en cache.

Ce moteur permet de faire la distinction entre la logique applicative et la logique de présentation.

Inconvénients

PHP < 5

Pour être efficace, il faut apprendre à utiliser le langage de script de Smarty.

Le code du gabarit contient à la fois du HTML et une certaine logique applicative (celle liée à la présentation).

Conclusion

Une fois habitué à la syntaxe de Smarty, le développement va vite. Je me demande encore pourquoi avoir déplacé dans le gabarit ce qui ressemble à de la logique applicative (les boucles), mais cela a son utilité, quoique je n'en parle pas ici.

L'objectif principal est atteint : la logique applicative (récupération et traitement des informations) est séparée de la logique de présentation (affichage des informations). Le souci que je vois est que, pour y parvenir, Smarty est obligé d'utiliser du code dans les fichiers de gabarit. À mon sens, un gabarit est quelque chose de complètement passif, un outil utilisé par un autre outil, un simple moule. Avec Smarty, ce moule est actif.

Smarty dispose de plugins intéressants, mais il pourrait aller bien plus loin. Par exemple, le plugin de sélection de date pourrait (devrait) produire une boîte JavaScript/AJAX sous forme d'un calendrier : ce serait une solution bien plus ergonomique que les trois <select> actuels. Je ne doute pas qu'un tel plugin ait été développé par un tiers : je ne parle pas ici des contributions qui ne sont pas intégrées dans la classe principale.

Ce qui me gêne avec Smarty est la forte similitude entre le code de void/home.php et celui de smarty/home.php (cf. plus haut). Certes, le code lié à la présentation des données n'est plus dans un script PHP… Mais y a-t-il une réelle différence ? Le graphiste de l'équipe acceptera-t-il d'apprendre la syntaxe de Smarty pour composer les interfaces ?

Il faut absolument préparer toutes les informations dans des tableaux associatifs avant de commencer à appeler la méthode assign(), sans quoi l'imbrication de boucles avec Smarty peut être un problème. Ce n'est pas un problème en soi, il faut juste y penser.

Un résumé est disponible en fin d'article.

V - TinyButStrong

V-A - Présentation

Le projet

TinyButStrong

L'idée avec TinyButStrong est de réutiliser autant que possible tout le code existant, que ce soit du PHP ou du HTML.

L'auteur, Skrol29, est quelqu'un de très ouvert et toujours disponible pour les utilisateurs de TBS. Je ne saurai trop le remercier pour sa patience après sa lecture de la première version de ce comparatif. C'est toujours un plaisir de voir qu'une communauté conserve son personnage moteur.

Skrol29 a minutieusement révisé toute la partie « TinyButStrong » de ce comparatif. J'ai conservé certains de mes commentaires, mais le côté technique est corrigé. Il ne subsiste pas d'erreurs.

Installation

Extraire le script tbs_class_php5.php comme vous en avez maintenant l'habitude (hors de la racine du serveur Web).

Arborescence du projet :

- C:\Web\offline\shared\TBS\ : La bibliothèque (tbs_class_php5.php) ;

- C:\Web\offline\shared\TBS\plug-ins\ : Le plugin de cache (tbs_plugin_cache.php) ;

- C:\Web\offline\comparatifs\tbs\ : La classe personnalisée (MyTinyButStrong.php) ;

- C:\Web\online\comparatifs\ : Les scripts à charger par le navigateur (tbs-nocache.php et tbs-cache.php).

Héritage de la classe

Voici la classe que je vous propose d'utiliser :

MyTinyButStrong.php

1. <?php

3. require('C:/Web/offline/shared/TBS/tbs_class_php5.php');

5. class MyTinyButStrong extends clsTinyButStrong

6. {

7. public $Caching;

9. public $_BasePath;

10. public $_PluginsPath;

11. public $_TemplatePath;

12. public $_CacheLifeTime;

14. public function __construct($template, $caching)

15. {

16. parent::__construct();

18. //

19. // Configuration de l'objet

20. //

21. $this->NoErr = TRUE;

22. $this->Caching = $caching;

23. $this->HtmlCharSet = 'ISO-8859-1';

24. $this->_ChrOpen = '{$';

25. $this->_ChrClose = '}';

26. $this->_ChrVal = '{$val}';

28. $this->_BasePath = dirname(__FILE__);

29. $this->_PluginsPath = 'C:/Web/offline/shared/TBS/plug-ins/';

30. $this->_TemplatePath = $this->_BasePath.'/templates/'.$template.'/html/';

32. if($this->Caching)

33. {

34. $this->_CachePath = $this->_BasePath.'/templates/'.$template.'/cache/';

35. $this->_CacheLifeTime = 180; // En secondes

37. //

38. // Installation et démarrage du plugin de cache

39. //

40. require_once($this->_PluginsPath.'tbs_plugin_cache.php');

41. $this->PlugIn(TBS_INSTALL, TBS_CACHE, $this->_CachePath, '*.cache');

42. }

43. else

44. {

45. $this->Render = TBS_OUTPUT; // TBS_NOTHING ou TBS_OUTPUT ou TBS_EXIT

46. $this->_CachePath = NULL;

47. $this->_CacheLifeTime = NULL; // En secondes

48. }

49. }

51. public function LoadTemplate($file, $merge = FALSE)

52. {

53. if($merge=='+')

54. {

55. parent::LoadTemplate($this->_TemplatePath.'/'.$file, '+');

56. }

57. else

58. {

59. if($this->Caching)

60. {

61. $this->PlugIn(TBS_CACHE, $file, $this->_CacheLifeTime);

62. }

64. parent::LoadTemplate($this->_TemplatePath.'/'.$file, $this->HtmlCharSet);

65. }

66. }

67. }

69. ?>

Le code suivant permet de créer l'objet dans nos scripts :

tbs-nocache.php

define('DIR_OFFLINE', 'C:/Web/offline/comparatifs/tbs/');
define('TBS_TEMPLATE', 'default');
define('TBS_CACHING', FALSE);

require(DIR_OFFLINE.'MyTinyButStrong.php');

$template = new MyTinyButStrong(TBS_TEMPLATE, TBS_CACHING);

//
// Série d'appels à $temlate->LoadTemplate() suivis d'envoi de données au gabarit
//

$template->Show();

Le constructeur attend le nom du fichier de gabarit et un booléen déterminant si l'on souhaite utiliser le cache.

Notez que j'ajoute une propriété $_TemplatePath à la classe. Je la laisse en accès public par souci d'homogénéité avec la classe de départ, mais, si cela n'avait tenu qu'à moi, elle aurait été protected.

J'ai également apporté une amélioration afin d'éviter de devoir répéter le chemin d'accès au gabarit à chaque chargement de fichier de gabarit.

En temps normal, la méthode LoadTemplate() s'adapte automatiquement : la première utilisation définit le répertoire des gabarits, puis les utilisations suivantes peuvent se contenter de donner le nom du fichier de gabarit sans évoquer le chemin d'accès complet.

Je pars ici du principe que seul le premier appel à LoadTemplate() construit le gabarit à partir de rien, tous les autres étant a priori pour y ajouter du code HTML, c'est pourquoi ces appels ont le paramètre '+' lorsque la propriété Source est non vide.

Enfin, TBS permet de configurer les marqueurs de début et de fin de bloc. J'ai pris le parti de les changer. Par défaut, TBS utilise des crochets [], or je commence à m'habituer aux accolades et au dollar {$} des autres moteurs, j'ai donc utilisé ces caractères.

TBS peut configurer ces marqueurs de plusieurs manières. J'ai adopté ici la méthode la plus explicite, mais le constructeur peut le faire également.

V-B - Utilisation

Variables

TBS sait lier automatiquement les variables locales PHP avec le gabarit. Pour cela, il faut que les variables du gabarit respectent une syntaxe spéciale : {$var.nom_de_la_variable}.

Cependant, cela demanderait des manipulations peu pratiques et peu explicites. Il est préférable d'utiliser la méthode MergeField(), dont les intentions sont limpides.

Syntaxe générique :

$template->MergeField('variable_du_gabarit', 'valeur_php');

header.php

$result = mysql_query('SELECT name, value FROM header') or die(mysql_error());
while($meta = mysql_fetch_assoc($result))
{
$template->MergeField('meta_'.$meta['name'], $meta['value']);
}

header.tpl

<meta name="generator" content="{$meta_generator}" />
<meta name="description" content="{$meta_description}" />
<meta name="keywords" content="{$meta_keywords}" />
<meta name="MS.LOCALE" content="{$meta_mslocale}" />

Le comportement par défaut de TBS est de convertir les valeurs en entités HTML à l'aide de la fonction htmlspecialchars().

Blocs

TinyButStrong a une manière très particulière de gérer les répétitions de portions de gabarit, qu'il appelle « blocks ». La méthode MergeBlock() s'adapte en fonction des paramètres qui lui sont passés, par exemple ici une ressource MySQL ou un tableau de valeurs :

home.php (utilisant un tableau PHP)

$subjects = array();
$result = mysql_query($sql['subjects']);
while($subject = mysql_fetch_assoc($result))
{
$sujects[] = $subject;
}
$template->MergeBlock('subject', $subjects);

home.php (utilisant une requête SQL)

$template->MergeBlock('subject', $resource, 'SELECT id, title FROM subject')

home.tpl

{$subject;block=begin}
<div class="bloc_cours">
<div class="titre_cours">{$subject.title}</div>
</div>
<br />
<br />
{$subject;block=end}

Dans cet exemple, TBS fusionne les données directement depuis la requête SQL. Il faut bien se rappeler que, selon les bonnes règles de design d'une application, les requêtes SQL devraient retourner des informations prêtes à être affichées (hors conversion vers le charset de destination). Il ne devrait donc pas être nécessaire de récupérer les données avant de les envoyer au gabarit.

Imbriquation

home.php (utilisant des requêtes SQL)

$sql = 'SELECT id, title
FROM subject';
if($template->MergeBlock('subject', $resource, $sql) > 0)
{
$sql = 'SELECT id, title
FROM category
WHERE subject_id = %p1%';
if($template->MergeBlock('category', $resource, $sql) > 0)
{
$sql = 'SELECT id, uri, title, description
FROM tutorial
WHERE category_id = %p2%';
$template->MergeBlock('tutorial', $resource, $sql);
}
}

home.php (utilisant un tableau hiérarchique PHP)

if($template->MergeBlock('subject', 'array', 'subjects') > 0)
{
if($template->MergeBlock('category', 'array', 'subjects[%p1%][categories]') > 0)
{
$template->MergeBlock('tutorial', 'array', 'subjects[%p1%][categories][%p2%][tutorials]');
}
}

home.tpl

{$subject;block=begin}
<div class="bloc_cours">
<div class="titre_cours">{$subject.title}</div>
{$category;block=begin;p1={$subject.id}}
<div class="categorie_cours">{$category.title}</div>
<div class="liste_cours">
<ul>
{$tutorial;block=begin;p1={$subject.id};p2={$category.id}}
<li>
<a href="{$tutorial.uri}">{$tutorial.title}</a> : {$tutorial.description}
</li>
{$tutorial;block=end}
</ul>
</div>
<hr />
{$category;block=end}
</div>
<br />
<br />
{$subject;block=end}

TBS utilise des variables automatiques dans le code PHP (p1, p2 p3, etc.) pour relier les blocs imbriqués. Cette variable est remplacée à chaque itération : on utilise habituellement une variable issue du bloc parent.

Le fichier de gabarit construit cette variable dans la définition du block, tandis que le script PHP l'utilise pour filtrer les données.

Le secret de l'imbrication des boucles repose dans ces variables automatiques.

NB : Le dernier extrait de code PHP correspond au tableau hiérarchique construit lors de l'utilisation du moteur Smarty, dans le script home.php.

Alternance

Il suffit de répéter le block dans le gabarit, ce qui oblige TBS à utiliser une alternativement tous les blocks.

Le gabarit est alors dans l'esprit de ce que nous connaissons :

{$subject;block=begin}
<div class="bloc_cours">
<div class="titre_cours">{$subject.title}</div>
{$category;block=begin;p1={$subject.$}}
<div class="categorie_cours">{$category.title}</div>
<div class="liste_cours">
<ul>
{$tutorial;block=begin;p1={$subject.$};p2={$category.$}}
<li>
<a href="{$tutorial.uri}">{$tutorial.title}</a> :
<span style="color: green;">{$tutorial.description}</span>
</li>
{$tutorial;block=end}
{$tutorial;block=begin}
<li>
<a href="{$tutorial.uri}">{$tutorial.title}</a> :
<span style="color: blue;">{$tutorial.description}</span>
</li>
{$tutorial;block=end}
</ul>
</div>
<hr />
{$category;block=end}
</div>
<br />
<br />
{$subject;block=end}

Segmentation du gabarit

Comme très souvent avec TinyButStrong, il y a plusieurs manières de procéder.

L'approche décrite dans la documentation (dans la section subtemplates) permet d'avoir une gestion similaire à celle proposée par Smarty : le gabarit principal (home.tpl) définit les gabarits à inclure lors de son chargement par l'objet TBS. Dans approche, TinyButStrong permet d'exclure ce qui se trouve à l'extérieur de la balise <body> (configurable avec getbody=balise), ce qui est utile lorsque l'on utilise des sous-gabarits construits par un outil comme Dreamweaver.

Une autre approche consiste à charger les gabarits les uns après les autres par la méthode LoadTemplate() :

tbs*.php

$template = new MyTinyButStrong(TBS_TEMPLATE, TBS_CACHING);

// Chargement des différents gabarits dans le moteur
$template->LoadTemplate('header.tpl');
$template->LoadTemplate('home.tpl', '+');
$template->LoadTemplate('footer.tpl', '+');

//
// Mettre ici l'envoi des données au moteur
//

// Affichage de la page finale
$template->Show();

Gestion du cache

TBS dispose d'un plugin (CacheSystem, inclus en fourniture standard) permettant de gérer la mise en cache du document produit.

Inclusion :

require_once('tbs_plugin_cache.php');

Configuration :

$template->PlugIn(TBS_INSTALL, TBS_CACHE, $template>_CachePath, '*.cache');

Utilisation :

$template->PlugIn(TBS_CACHE, 'file.tpl, $template->_CacheLifeTime);

Dans la mesure où TinyButStrong se charge des requêtes à la base de données, il n'est pas nécessaire de vérifier la validité du cache comme avec les autres moteurs de gabarit. En effet, TBS n'effectue pas de requête s'il a déjà les résultats en cache.

V-C - Mon avis

Avantages

PHP 4 et 5.

Possibilité d'installer et de développer des plugins.

La syntaxe « relative » du gabarit peut sembler déroutante, mais en réalité elle est ingénieuse : elle permet de créer des gabarits XML depuis l'intérieur d'une application comme Office2007 ou OpenOffice. D'ailleurs, un plugin OpenOffice existe pour TBS…

TBS est prévu pour utiliser des gabarits 100% compatibles W3C, ce qui permet à un graphiste de créer les documents avec son outil favori.

Inconvénients

Cela relève du commentaire personnel, mais je n'adhère pas toujours à la manière de coder, notamment l'utilisation de constantes dans le scope global plutôt qu'à l'intérieur d'une classe (groupement de classes dans un même script, un même paramètre ou une même méthode peut avoir diverses utilisations).

Construire des requêtes SQL n'est pas la vocation de TBS (et j'adhère à ce refus), mais la classe permet néanmoins d'exécuter des requêtes. Cependant, aucune protection n'est appliquée aux requêtes SQL envoyées par MergeBlock() ; il appartient au développeur de le faire avec la bonne fonction : par exemple pour MySQL, il s'agit de mysql_real_escape_string().

Conclusion

TBS est le moteur de gabarits qu'il m'a été le plus difficile de maîtriser. J'ai finalement réussi à atteindre mon objectif (reproduire le document initial) : je le dois à l'aide patiente de son auteur, Skrol29.

Un résumé est disponible en fin d'article.

VI - Virtual Template

VI-A - Présentation

Le projet

VTemplate est un projet français.

Le principe est le même que pour phplib, à savoir utiliser des commentaires HTML dans le gabarit afin de construire un moule de document.

Installation

J'ai utilisé la version 1.3.0 « Cache Edition ».

Avant de commencer, nous allons devoir opérer quelques corrections du code téléchargé :

Trouver :

1. if (!$DEFINE_VTEMPLATE)

Remplacer par :

1. if (!isset($DEFINE_VTEMPLATE))

Trouver :

1. $this->varlist=array_merge($var[1],$var1[1]);

Remplacer par :

1. $this->varlist=array_merge($this->varlist,$var1[1]);

Trouver :

1. preg_match("(<!--VTP_([^()]+)-->)sU",$code,$reg);

3. // Tester la présence des caractères invalides dans le nom ( | et {});

4. if (count(explode("|",$reg[1]))>1 || count(explode("{",$reg[1]))>1 || count(explode("}",$reg[1]))>1) exit($this->error(1,$reg[1],1));

6. return $reg[1];

Remplacer par :

1. if (preg_match("/<!--VTP_([^()]+)-->/U",$code,$reg))

2. {

3. // Tester la présence des caractères invalides dans le nom ( | et {});

4. if (count(explode("|",$reg[1]))>1 || count(explode("{",$reg[1]))>1 || count(explode("}",$reg[1]))>1) exit($this->error(1,$reg[1],1));

5. return $reg[1];

6. }

Trouver :

1. if (count($this->sources[$zone]["fils"])){

Remplacer par :

1. if (!empty($this->sources[$zone]["fils"]) and count($this->sources[$zone]["fils"])){

Trouver :

1. if ( $this->file_cached[$handle]['data'] ){

Remplacer par :

1. if ( !empty($this->file_cached[$handle]['data']) ){

Et enfin…

Trouver :

1. if ( !is_dir($this->path.basename($path."/$file")) ){

2. if ( $sub_dir = dirname($path."/$file") ){

3. $lsd = explode("/",$sub_dir);

4. $all_dir=$this->path;

5. foreach($lsd as $subdir){

6. //$all_dir.="/$subdir";

7. if ( !is_dir($all_dir) ){

8. mkdir($all_dir, 0755);

9. clearstatcache();

10. }

11. }

12. }

13. }

14. return $this->path."/$file";

Remplacer par :

1. if( !is_dir($this->path) ) {

2. mkdir($this->path, 0755, TRUE);

3. }

4. return($this->path.$file);

Arborescence du projet :

- C:\Web\offline\shared\VTemplate\ : La bibliothèque (VTemplate.php) ;

- C:\Web\offline\sites\comparatifs\vtemplate\ : La classe personnalisée (MyVTemplate.php) ;

- C:\Web\online\comparatifs\ : Le script à charger par le navigateur (vtemplate-nocache.php).

Héritage de la classe

Voici la classe que je vous propose d'utiliser :

MyVTemplate.php

1. <?php

3. require('C:/Web/offline/shared/VTemplate/vtemplate.class.php');

5. class MyVTemplate extends VTemplate

6. {

7. var $path; // Chemin d'accès au dossier racine commun aux variables suivantes

8. var $templates_path; // Chemin d'accès aux templates

9. var $caching; // Mise en cache ?

10. var $cache_lifetime; // Durée de vie des fichiers de cache (en secondes)

12. function MyVTemplate($template, $caching)

13. {

14. $this->templates_path = dirname($this->path).'/html/';

16. if($caching)

17. {

18. $this->caching = TRUE;

19. $this->cache_lifetime = 180;

20. }

21. else

22. {

23. $this->caching = FALSE;

24. }

25. }

27. /**

28. * MyVTemplate::Open()

29. * Surcharge de la méthode Open() pour lui faire accepter un chemin relatif

30. *

31. * @param string $file

32. * @return

33. **/

34. function Open($file, $useless_1 = NULL, $useless_2 = NULL)

35. {

36. if($this->caching)

37. {

38. return parent::Open($this->templates_path.$file, TRUE, $this->cache_lifetime);

39. }

40. else

41. {

42. return parent::Open($this->templates_path.$file);

43. }

44. }

45. }

47. ?>

Le constructeur attend le nom du gabarit.

Le code suivant permet de créer l'objet dans nos scripts :

vtemplate.php

define('DIR_OFFLINE', 'C:/Web/offline/sites/comparatifs/vtemplate/');
define('VTP_TEMPLATE', 'default');

require(DIR_OFFLINE.'MyVTemplate.php');

$template = new MyVTemplate(VTP_TEMPLATE);

//
// Série d'appels à $template->Open() et $template->Display() espacés d'envois de données au gabarit
//

VI-B - Utilisation

Variables

La méthode setVar() remplit cet office :

header.php

$template->setVar($handle, 'charset', $charset);

header.tpl

<meta http-equiv="Content-Type" content="text/html; charset={#charset}" />

Blocs

Pour répéter une portion du gabarit, Virtual Template utilise ce qu'il appelle une session :

home.php

$sql = 'SELECT id, title
FROM subject';
$subjects = mysql_query($sql) or die(mysql_error());
while($subject = mysql_fetch_assoc($subjects))
{
$template->newSession($handle, 'subject');
$template->setVar($handle, 'subject.title', $subject['title']);
}

home.tpl

<!--VTP_subject-->
<div class="bloc_cours">
<div class="titre_cours">{#title}</div>
</div>
<br />
<br />
<!--/VTP_subject-->

Dans la version téléchargée pour la rédaction de ce comparatif, la méthode newSession() appelait la méthode addSession() avec trois paramètres de trop. C'est pour cela que j'ai proposé ci-dessus un correctif. Une solution alternative serait d'appeler addSession() puis closeSession().

Imbrication

VTemplate permet d'imbriquer les boucles les unes dans les autres de cette manière :

home.php

$sql = 'SELECT id, title
FROM subject';
$subjects = mysql_query($sql) or die(mysql_error());
while($subject = mysql_fetch_assoc($subjects))
{
$template->newSession($handle_home, 'subject');
$template->setVar($handle_home, 'subject.title', $subject['title']);

$sql = 'SELECT id, title
FROM category
WHERE subject_id = '.$subject['id'];
$categories = mysql_query($sql) or die(mysql_error());
while($category = mysql_fetch_assoc($categories))
{
$template->newSession($handle_home, 'category');
$template->setVar($handle_home, 'category.title', $category['title']);

$sql = 'SELECT id, uri, title, description
FROM tutorial
WHERE category_id = '.$category['id'];
$tutorials = mysql_query($sql) or die(mysql_error());
while($tutorial = mysql_fetch_assoc($tutorials))
{
$template->newSession($handle_home, 'tutorial');
$template->setVar($handle_home, 'tutorial.uri', $tutorial['uri']);
$template->setVar($handle_home, 'tutorial.title', $tutorial['title']);
$template->setVar($handle_home, 'tutorial.description', $tutorial['description']);
}
}
}

home.tpl

<!--VTP_subject-->
<div class="bloc_cours">
<div class="titre_cours">{#title}</div>
<!--VTP_category-->
<div class="categorie_cours">{#title}</div>
<div class="liste_cours">
<ul>
<!--VTP_tutorial-->
<li>
<a href="{#uri}">{#title}</a> : {#description}
</li>
<!--/VTP_tutorial-->
</ul>
</div>
<hr />
<!--/VTP_category-->
</div>
<br />
<br />
<!--/VTP_subject-->

Il est impossible d'appeler une variable d'une session parente, par exemple le titre d'un sujet dans la session d'un tutoriel. À moins, bien sûr, de dupliquer cette variable dans le gabarit.

Alternance

VTemplate ne propose pas de moyen interne permettant d'alterner les couleurs de lignes, il faut donc avoir recours au subterfuge habituel :

home.php

if($i % 2)
{
$style = 'green';
}
else
{
$style = 'blue';
}

$template->setVar($handle, 'tutorial.style', $style);

home.tpl

<!--VTP_tutorial-->
<li>
<a href="{#uri}">{#title}</a> : <span style="color: {#style};">{#description}</span>
</li>
<!--/VTP_tutorial-->

Segmentation du gabarit

Avec VTemplate, chaque gabarit est chargé dans un handle qu'il est possible de manipuler indépendamment.

Pour segmenter la page avec différents gabarits, il faut procéder ainsi :

vtemplate.php

$template = new MyVTemplate(VTP_TEMPLATE);

// Déclaration des handles
$handle_header = $template->Open('header.tpl');
$handle_home = $template->Open('home.tpl');
$handle_footer = $template->Open('footer.tpl');

//
// À mettre ici : envoi des informations aux handles
//

// Affichage des handles
$template->Display($handle_header);
$template->Display($handle_home);
$template->Display($handle_footer);

Gestion du cache

La gestion du cache se fait au moyen d'une classe VTemplate différente de la version normale.

Dans la classe disponible au moment de la rédaction de cet article (1.3 Cache Édition de mars 2002), il m'a fallu corriger une variable de la ligne 263 :

Trouver dans la méthode Session::parseVar() :

$this->varlist=array_merge($var[1],$var1[1]);

Ligne corrigée :

$this->varlist=array_merge($this->varlist,$var1[1]);

Trouver dans la méthode Cache::getFileName() :

$all_dir.="/$subdir";

Ligne corrigée :

//$all_dir.="/$subdir";

Trouver dans la méthode Cache::getFileName() :

return $this->path.$path."/$file";

Ligne corrigée :

return $this->path."/$file";

Avec ces modifications, les fichiers de cache sont créés dans le système de fichiers. Il semble rester des erreurs à d'autres endroits, mais j'avoue ne plus avoir envie de déboguer…

VI-C - Mon avis

Avantages

Inconvénients

J'ai cru lire que VTemplate 1.3.3 était écrit en PHP 5, j'ai dû faire erreur. Il s'agit plutôt d'une classe PHP 4 compatible PHP 5 (comme toutes les classes PHP 4, en fait). Je préfère ne pas savoir quelles modifications ont été apportées pour valider cette compatibilité…

Les méthodes retournent -1 en cas d'erreur. Ce n'est pas une valeur assimilable à FALSE… Impossible d'utiliser la négation if(!open()) ou la comparaison booléenne if(open() === FALSE). De plus, aucune constante n'est définie avec la valeur -1. Par conséquent, la sémantique de la gestion d'erreurs est inexistante, ce qui rend le code difficile à lire.

Le changement entre la version « sans mise en cache » et la version « avec mise en cache » est complexe et peu flexible : il faut télécharger des scripts différents, mais au nom identique depuis le site officiel. Il est possible d'utiliser en permanence la version avec gestion du cache, mais au prix d'un numéro de version (1.3.0 au lieu de 1.3.3 pour la version sans mise en cache).

Les classes utilisées par VTemplate ont des noms génériques (« Err » et « Session »), ce qui a de fortes chances de poser des collisions lors de l'intégration au projet.

La configuration se fait au moyen de constantes situées dans le scope global. Il est notamment possible de modifier le tag d'ouverture des variables du gabarit, mais pas le tag de fermeture. Des noms de constantes comme « VTP_ALL » et « VTP_CACHED » auraient été plus heureux et il aurait été encore plus adapté de les placer à l'intérieur d'une classe écrite en PHP 5, ce qui donnerait des noms comme « VTemplate::ALL » et « VTemplate::CACHED ». Enfin, le répertoire des fichiers de cache n'est pas configurable par gabarit.

Conclusion

Il m'a été difficile d'utiliser VTemplate, car je m'attendais à une classe prête à l'utilisation, or j'ai été contraint de lire son code pour la déboguer. Les bugs sont nombreux, c'est assez déplaisant.

Je n'ai généralement ni l'envie ni le temps de déboguer une classe avant de pouvoir l'utiliser, VTemplate n'a donc pas ma préférence.

Si quelqu'un a vécu une expérience différente, qu'il n'hésite pas à me le faire savoir. C'est surtout dommage pour la gestion du cache, car, au vu de la rapidité du moteur selon mes tests, je m'attendais à quelque chose d'impressionnant.

Un résumé est disponible en fin d'article.

VII - Conclusion

VII-A - Comparaison

Mes conclusions peuvent être subjectives, à vous de vous faire une idée.

Les bibliothèques

Version comparée

Qualité du code

Syntaxe du gabarit

Cache

Écrit pour PHP

Utilisé par

Licence

Smarty

2.6.14

recommandations PEAR

recommandations PEAR

oui

4

cf. liste

LGPL

phpBB2

1.10.2.5

OK + commentaires

commentaires HTML

aucun

3

les forums phpBB2

GPL

TinyButStrong

3.2.0 pour PHP >= 5.0

homogène

configurable

oui

4 adapté 5

cf. liste

LGPL

VTemplate

1.3.3 Base Edition

médiocre

peu configurable

en théorie

4

inconnu

GPL

ModeliXe

1.2.2

moyenne

XML ou PEAR

oui

4 compatible 5

inconnu

LGPL

PEAR a défini des conventions de codage.

Informations sur la licence GPL : site officiel, Wikipedia.

Informations sur la licence LGPL : site officiel, wikipedia.

Test de performances

J'ai entièrement refait les tests de performances des scripts depuis que j'ai publié ce comparatif pour la première fois.

Je vais décrire ici en détail ma méthode et fournir le script utilisé. Si vous obtenez des résultats différents, je vous serais reconnaissant de me les faire parvenir. De même, si ma méthode ne vous semble pas appropriée, merci de me le faire savoir.

J'utilise l'utilitaire Apache Bench (ab.exe), fourni en standard avec ma distribution d'Apache 2.0.59 et je tourne sous Windows.

Je doute que ma configuration système soit très utile, mais la voici tout de même :

- Système : Windows XP SP2 ;

- Processeur : Intel Pentium M cadencé à 1300 MHz ;

- RAM : 512 Mo ;

- Apache : 2.0.59 ;

- PHP : 5.2.1 (Zend Core 2.0).

Skrol 29 (TinyButStrong) m'ayant convaincu de l'inutilité de ma méthode antérieure, j'ai adapté les scripts de ce comparatif pour éviter d'utiliser la base de données. Toutes les données sont codées en dur dans le script db-connect.php et les appels à MySQL sont supprimés.

Ma démarche est constituée des actions suivantes :

1. Supprimer les fichiers de log ;

2. Vider les répertoires des fichiers de cache et de compilation des différents moteurs ;

3. Récupérer des statistiques préliminaires (ab.exe) ;

4. Calculer la moyenne pour chaque moteur ;

5. Supprimer les quelques rapports erronés générés par Apache Bench ;

6. Calculer les statistiques finales (le comparatif).

Les statistiques sont récupérées sur 5000 chargements consécutifs par Apache Bench, pour chaque moteur. L'ensemble des tests est fait 30 fois (c'est-à-dire 30 fois 5000 chargements de chaque script) afin d'obtenir une moyenne fiable. La valeur utilisée dans le rapport est le nombre de requêtes par seconde.

À la fin de l'exécution du script de test, une fois les rapports erronés supprimés, nous calculons l'écart entre le nombre de requêtes par seconde du script de référence et le nombre de requêtes par seconde de chaque autre script : résultat = (référence / moteur) * 100. Le nombre obtenu est un pourcentage : par exemple, « 150 % » signifie que le moteur considéré est parvenu à exécuter 50 % de chargements de moins par seconde, par rapport au script de référence. Une autre manière de le comprendre est : « ce moteur met 150 % du temps du script de référence pour effectuer le même nombre de chargements ».

Fichiers du test de performances :

- Le script PHP de génération des rapports [ FTP ] [ HTTP ] ;

- Les rapports qui ont donné les résultats ci-dessous [ FTP ] [ HTTP ] ;

- Le rapport final [ FTP ] [ HTTP ] ;

- Les scripts modifiés [ FTP ] [ HTTP ].

Mon expérience avec ces bibliothèques (sans gestion du cache)

Voici un tableau récapitulatif de mon expérience avec ces moteurs de gabarits. Veuillez considérer que certaines réponses sont subjectives, et que la totalité des réponses découle de la comparaison que j'ai décrite dans les pages précédentes.

Page produite

Objectif

Prise en main

Flexibilité

Configuration

Performances / référence

Débogage

Smarty

oui

non

facile

immédiate

héritage

123 %

facile

phpBB2

oui

oui

facile

rapide

aucune

189 %

classique

TinyButStrong

oui (sauf cache)

non

complexe

immédiate

héritage

245 %

complexe

VTemplate

OK (sauf cache)

oui

facile

problématique

héritage + constantes

184 %

classique

ModeliXe

OK

oui

facile

malaisée

héritage + constantes

168 % (PEAR) ou 191 % (XML)

classique

Classement (décroissant) :

1. Smarty ;

2. ModeliXe ;

3. phpBB2 / VTemplate ;

4. TinyButStrong.

Leur gestion du cache

Voici un tableau récapitulatif de mon expérience avec ceux des moteurs de gabarits (de ma liste) qui proposent une gestion du cache.

Adaptation au projet

Performances / référence

Smarty

immédiate

116 %

phpBB2

non prévu pour

n/a

TinyButStrong

malaisée

113 %

VTemplate

après débogage

144 %

ModeliXe

immédiate

117 % (PEAR) ou 114 % (XML)

Classement (décroissant) :

1. ModeliXe / Smarty / TinyButStrong ;

2. VTemplate.

VII-B - Classement (subjectif)

Le classement en fonction des performances ne me semble pas suffisant pour juger de ce type de bibliothèque.

Ces classements dépendent de mon interprétation des comparaisons précédentes : je ne saurais trop vous recommander de vous construire votre propre opinion !

Séparation entre la logique et la présentation (du plus au moins adapté) :

1. phpBB2, VTemplate et ModeliXe n'ont aucun élément de programmation au sein du gabarit, ils se contentent de définir des zones à remplir, à répéter ou à ignorer ;

2. Smarty et TinyButStrong optent pour des gabarits plus complexes afin de simplifier le code PHP.

Nous retrouvons ici les deux groupes évoqués en introduction : d'une part (en seconde position ici), les moteurs incorporant le point de fusion dans le gabarit et, d'autre part (en première position selon moi), ceux le laissant dans le code PHP.

Nous n'avons cependant pas de grand gagnant, il nous faut donc trouver un autre argument que la possibilité de séparation entre logique et présentation.

Simplicité de la syntaxe du gabarit (par ordre décroissant) :

1. phpBB2 : des commentaires HTML comportant les mots BEGIN et END ;

2. VTemplate : des commentaires HTML avec une touche de XML ;

3. ModeliXe : une adaptation de la syntaxe XML ;

4. Smarty : une nouvelle syntaxe fondée sur les accolades et comportant des éléments de programmation ;

5. TinyButStrong : une nouvelle syntaxe fondée sur les crochets et comportant des éléments de programmation.

VII-C - Épilogue

Je ne vous recommande pas la lecture de ce qui suit si vous n'avez pas étudié vous-même les différents moteurs.

Je rappelle ici l'objectif principal qui doit être atteint par un moteur de gabarits : permettre une séparation efficace du code source de présentation (par exemple HTML) et du code source de logique applicative (PHP).

Pour être honnête, un seul des cinq moteurs testés me semble convenable. Ce n'est bien sûr qu'un avis personnel et conditionné par ce que j'ai énoncé en introduction : chaque développeur devrait avoir son opinion sur ce sujet.

Smarty est le plus mature et le plus propre. Cependant, la syntaxe utilisée dans ses gabarits est tellement proche du langage PHP lui-même que la séparation logique/présentation est floue, voire inefficace. Je ne suis pas certain d'être d'accord avec le qualificatif de moteur de templates. Je ne dis pas que Smarty est inutile, mais plutôt qu'il n'est pas utile en tant que moteur de gabarits.

phpBB2 a le moteur le plus facile à utiliser, mais il ne gère pas la mise en cache. Ce n'est pas un défaut, c'est simplement un avantage qu'il n'a pas.

TinyButStrong promet de grandes choses, mais a dû faire des concessions pour accéder aux requêtes de ses utilisateurs, ce qui donne une classe très difficile à utiliser. TBS est de plus en plus « strong », mais de moins en moins « tiny ». Par ailleurs, la syntaxe de son gabarit souffre du même inconvénient que celle de Smarty.

VTemplate utilise des gabarits à la syntaxe redondante et comporte des bugs de fonctionnement. C'est dommage, car il est sur la bonne voie.

ModeliXe comporte des bugs de fonctionnement.

À choisir parmi ces moteurs, je prends celui de phpBB2. Il permet une séparation correcte entre la logique applicative et l'affichage, il est facile d'accès et il fonctionne bien. De plus, il est maintenu et utilisé par une communauté gigantesque.

Si vous utilisez l'un ou l'autre moteur dans vos sites, n'hésitez pas à nous le faire savoir et à nous donner vos impressions (cf. lien ci-dessous vers le débat sur nos forums), surtout si elles sont différentes de celles proposées ici !

VII-D - Téléchargement

Voici les scripts utilisés :

- C:\Web\offline\shared\ (les bibliothèques) [ FTP ] [ HTTP ] ;

- C:\Web\offline\sites\comparatifs\ (les classes personnalisées) [ FTP ] [ HTTP ] ;

- C:\Web\online\comparatifs\templates\ (les scripts publics) [ FTP ] [ HTTP ] ;

- Les scripts de création de la base de données [ FTP ] [ HTTP ].

VII-E - Liens

Ressources Developpez :

- Comparatif : Les systèmes de templates en PHP, par Hugo Étiévant ;

- Tutoriel : Le système de templates de phpBB, par Genova ;

- Formation : Tous nos cours sur les gabarits en PHP ;

- Débat sur nos forums : Commentez .

Liens externes :

- The no-framework PHP MVC framework, par Rasmus Lerdorf.