Revenir
en haut

Utiliser Twig avec Zend Framework 1.1x

23/11/2010 9

Cet article est un peu une suite de : Utiliser un moteur de template avec PHP, pourquoi ? que je vous invite à lire.

Comme je l’expliquais, avec les mauvaises expériences, je reviens sur mes positions concernant les moteurs de templates. Mais ce n’est pas la seule raison, si mon intérêt se porte de nouveaux sur un tel système c’est aussi grâce à l’arrivée de nouveaux moteurs de templates complets, flexibles, et surtout très performants.

Mon attention s’est récemment portée vers Twig, un moteur de templates proposé par Fabien Potencier (créateur de symfony). Fabien a fait revivre le moteur initialement créé par Armin Ronacher (pour une plateforme de blogs), a amélioré le code, et propose au moment où j’écris ces lignes, une version 0.9.9, plus qu’un pas vers une version « officiellement » stable donc.

En jouant avec le moteur, j’ai rapidement été convaincu, et je me suis vite mis à chercher si une intégration avec Zend Framework existait. J’ai trouvé quelques propositions d’intégration, certaines abandonnées, d’autres incomplètes, ou encore incompatibles avec la version actuelle de Twig.

Après quelques bidouillages, j’ai décidé de créer ma propre intégration du moteur, que je ferai évoluer en fonction des besoins.

Comment fonctionne Twig ?

Pour ce qui est des fonctionnalités du moteur, je vous laisse lire la documentation sur le site officiel.

Ce qui nous intéresse plus ici, c’est la flexibilité de la bête. Et vous ne serez pas déçus.

Après avoir passé quelques heures à lire le code source et à faire quelques tests pour comprendre le fonctionnement du parser, j’avoue que je suis surpris par la facilité d’appréhension du système. Tout est extensible, il est assez simple d’étendre le coeur du moteur et simple aussi de créer de nouveaux tags. Je ne suis pas certain d’avoir utilisé toutes les possibilités offertes pour mon intégration dans Zend Framework, mais je ne manquerai pas de faire évoluer les choses au fur et à mesure de mes découvertes.

Twig utilise un objet appelé environment qui est utilisé pour stocker la configuration, les extensions et pour charger les templates. Dans la plupart des cas, un objet environment est créé et est ensuite utilisé pour rendre tous les templates de l’application.

Pour charger les templates, un objet loader est fourni à l’environnement, il est responsable de charger les templates depuis une resource, par exemple une chaine ou un fichier.

Une extension est un ensemble de fonctionnalités ajoutées à Twig. Dans Twig, tout est extension, même le coeur du moteur. Le moteur est livré avec plusieurs extensions permettant de proposer un panel de fonctionnalités très confortable.

Pour ce qui est de la création de nouveaux tags, le fonctionnement de Twig est assez simple, il faut au minimum :

  • une extension qui sera pluggée à Twig et qui regroupe un ou plusieurs parsers
  • un ou plusieurs parser (soit un générique, soit un pour chaque tag)
  • un ou plusieurs noeuds, objets qui vont compiler le code en PHP.

Pour parser la syntaxe d’un tag, Twig utilise une « grammaire » particulière pour chaque type d’expression (arguments de type constante, tableau, variable, etc.). Chaque grammaire est associée à un parser. De cette manière, Twig est capable d’analyser automatiquement le contenu d’un tag et d’en extraire différentes expressions.

Voici un exemple de tag :

{% javascript 'js/myScript.js' with ['mode': 'prepend'] %}

Et voici la grammaire de ce tag :

<js> [with <arguments:array>]

Un tag est considéré comme un noeud contenant un ensemble d’expressions (chacune associée à une grammaire) pouvant elles-mêmes être des noeuds. Une fois que le parser a interprété un tag, il invoque un objet node qui se charge de compiler (via un objet compiler) chaque expression du noeud en PHP. Pour pouvoir être compilée, chaque classe d’expression possède une méthode compile qui se charge de retourner la représentation PHP de l’expression.

Par exemple, l’expression :

['param1': 'val1', 'param2': 'val2']

Dont la grammaire est :

<arguments:array>

Sera compilée en :

array('param1' => 'val1', 'param2' => 'val2')

Il est donc possible d’ajouter de nouvelles « grammaires » selon les besoins et de créer les parsers et expressions associés.

Je ne vais pas m’étaler plus sur le fonctionnement interne du moteur. D’une part parce que je n’arriverai pas à couvrir toutes les possibilités dans un article et ensuite parce que c’est bien plus compliqué à expliquer qu’à utiliser. La meilleure façon pour vous de comprendre le fonctionnement est de parcourir les sources, de vous amusez avec le moteur et de tenter de créer vos propres extensions.

Intégration avec Zend Framework

Il existe deux points difficiles pour l’intégration avec ZF :

  • conserver le fonctionnement de Zend_Layout
  • Proposer une syntaxe pour invoquer les aides d’actions.

Il n’y a pas de magie, certaines aides d’actions devront être spécifiquement implémentées dans une extension pour pouvoir fonctionner. Pour cette première intégration, j’ai créé des tags pour certaines aides d’actions indispensables natives au framework. A vous de compléter mon travail en fonction de vos besoins.

Utilisation de Zend_Layout ou non ?

C’est une question de choix, et surtout de goûts.

Twig propose nativement un système d’héritage des templates. Vous pouvez vous passer complètement de Zend_Layout en utilisant cette fonctionnalité. La différence est que dans Twig, les templates enfants sont rendus après les templates parents. Certaines choses qui vous semblaient peut-être naturelles ne le seront plus.

Exemples

Sans Zend Layout et avec l’héritage de templates :

views/layouts/layout.twig

<!DOCTYPE HTML>
<html>
    <head>
        <title>{% block title %}{% endblock%}</title>
        {% block metas %}
            {% metaHttpEquiv 'Content-Type' with 'text/html; charset=utf-8' %}
        {% endblock %}
        {% block javascripts %}
            {% javascript 'js/jquery.js' %}
        {% endblock %}
        {% block stylesheets %}
            {% stylesheet 'css/layout.css' %}
        {% endblock %}

        {% metas %}
        {% javascripts %}
        {% stylesheets %}
    </head>
    <body>
        <h1>{% block 'title1' %}Titre par défaut{% endblock %}</h1>
        {% block content %}{% endblock %}
    </body>
</html>

views/index/twig-zf.twig

{% extends 'layouts/layout.twig' %}

{% block title 'Anonymation - Twig for Zend Framework' %}

{% block metas %}
    {% parent %}
    {% metaName 'description' with 'My super twig description for SEO' %}
{% endblock %}
{% block javascripts %}
    {% parent %}
    {% javascript 'js/twig.js' %}
{% endblock %}
{% block stylesheets %}
    {% parent %}
    {% stylesheet 'css/twig.css' %}
{% endblock %}

{% block 'title1' 'Some help about Twig' %}

{% block content %}
    <div id="more-information">
        <p>
            Helpful Links: <br />
            <a href="http://www.twig-project.org/documentation">Twig documentation</a> |
            <a href="http://github.com/benjamindulau/Ano_ZFTwig">Ano_ZFTwig source code</a>
        </p>
    </div>
    <a href="{% route 'default' with ['controller': 'index', 'action': 'index'] %}">
        &lt; Back to homepage
    </a>
{% endblock %}

Avec Zend Layout :

Le même exemple en utilisant Zend Layout, sachant que la vue est rendue avant le layout, donc impossible d’utiliser les block. L’utilisation de Zend Layout implique aussi l’utilisation de certains tags spécifiques que vous retrouverez dans le package Ano_ZFTwig.

views/layouts/layout.twig

<!DOCTYPE HTML>
<html>
    <head>
        {% title %}
        {% metaHttpEquiv 'Content-Type' with 'text/html; charset=utf-8' %}
        {% javascript 'js/jquery.js' with ['mode': 'prepend'] %}
        {% stylesheet 'css/layout.css' with ['mode': 'prepend'] %}
        <base href="{% hlp 'serverUrl' %}/{% hlp 'baseUrl' %}" />
        {% metas %}
        {% javascripts %}
        {% stylesheets %}
    </head>
    <body>
        <h1>{% holder 'title1' %}</h1>
        {% layout 'content' %}
    </body>
</html>

views/index/twig-zf.twig

{# layout override #}

{% headTitle 'Anonymation - Twig for Zend Framework' %}
{% metaName 'description' with 'My super twig description for SEO' %}
{% javascript 'js/twig.js' %}
{% holder 'title1' with 'Some help about Twig' %}

{# content #}
   <div id="more-information">
        <p>
            Helpful Links: <br />
            <a href="http://www.twig-project.org/documentation">Twig documentation</a> |
            <a href="http://github.com/benjamindulau/Ano_ZFTwig">Ano_ZFTwig source code</a>
        </p>
    </div>
    <a href="{% route 'default' with ['controller': 'index', 'action': 'index'] %}">
        &lt; Back to homepage
    </a>

Vous remarquez ici les options prepend pour l’ajout de JS et de CSS, comme la vue est rendue avant, les scripts ajoutés dans le layout doivent aller au dessus de la pile, exactement comme si vous utilisiez le moteur de templates par défaut de Zend Framework.

Vous voyez aussi que les tags javascripts & co n’ont pas besoin d’être dans un block, comme le layout est rendu après la vue, les stacks seront prêts au moment de l’affichage.

Installation et configuration

Pour installer la librairie :

autoloaderNamespaces[] = "Ano_"
pluginPaths.Ano_Application_Resource = APPLICATION_PATH "/../library/Ano/Application/Resource"

Pour configurer et activer Twig ça se passe dans application.ini :

resources.twig.options.charset = "utf-8"
resources.twig.options.strict_variables = 0
resources.twig.options.cache = APPLICATION_PATH "/../var/cache/twig"
resources.twig.options.auto_reload = 1
resources.twig.options.debug = 0
resources.twig.options.trim_blocks = 1
resources.twig.options.viewSuffix = twig
resources.twig.viewPaths[] = APPLICATION_PATH "/views/layouts"
resources.twig.viewPaths[] = APPLICATION_PATH "/views/scripts"
resources.twig.helperPath.My_View_Helper_ = "My/View/Helper"

Pour les options de Twig je vous laisse lire la doc officielle pour les développeurs.

Il faut penser à bien ajouter tous les chemins vers les scripts de vues, y compris les chemins vers les layouts si vous n’utilisez pas Zend_Layout.

Si vous souhaitez utiliser Zend_Layout, il suffit d’ajouter sa configuration :

resources.layout.layout = "layout"
resources.layout.layoutPath = APPLICATION_PATH "/views/layouts"
resources.layout.viewSuffix = "twig"

Et c’est tout :)

Utilisation

Vous n’avez rien à changer dans vos contrôleurs pour utiliser Twig, vous continuez à développer comme d’habitude, le viewrenderer se chargera automatiquement de rendre les vues avec le moteur de Twig.

Pour les fonctionnalités courantes du moteur je vous redirige encore vers la doc officielle (pour les designers cette fois-ci).

Les tags additionnels inclus dans le package Ano_ZFTwig :

(pas de coloration syntaxique, désolé :p)

<!-- Invoquer n'importe quelle aide de vue (mais pas leurs méthodes) -->
{% hlp 'myHelper' with ['arg1', ['key1': 'val1', 'key2': 'val2'], 'arg3'] %}

<!-- Ajouter un fichier script javascript à la pile -->
<!-- Le mode est optionel, par défaut : append -->
{% javascript 'js/shared.js' %}
{% javascript 'js/form.js' with ['mode': 'prepend'] %}

<!-- Rendre les tags javascripts (dans la section <head> par ex.) -->
{% javascripts %}

<!-- Ajouter une feuille de style (css) à la pile -->
<!-- Le mode est optionel, par défaut : append -->
{% stylesheet 'css/layout.css' %}
{% stylesheet 'css/ma-page.css' with ['mode': 'prepend'] %}

<!-- Rendre les tags link css (dans la section <head> par ex.) -->
{% stylesheets %}

<!-- Ajouter un balise meta http-equiv à la pile -->
<!-- Option : mode => append/prepend -->
{% metaHttpEquiv 'Content-Type' with 'text/html; charset=utf-8' %}

<!-- Ajouter un balise meta name à la pile -->
<!-- Option : mode => append/prepend -->
{% metaName 'description' with 'My super website SEO description' %}

<!-- Rendre les balises metas -->
{% metas %}

<!-- Changer la balise <title></title> -->
{% headTitle 'My page title' %}

<!-- Rendre la balise <title></title> -->
{% title %}

<!-- Construire une url depuis une route -->
{% route 'photo_route' with ['id': photo.id] %}

<!-- Rendre une section du layout -->
<!-- ex. équivalent à : <?php echo $this->layout()->content; ?> -->
{% layout 'content' %}

<!-- Assigner la valeur d'un placeholder -->
{% holder 'titleh1' with 'My main title for SEO' %}

<!-- Afficher un placeholder -->
{% holder 'titleh1' %}

Conclusion sur un exemple avancé

Avant de vous laisser, je vous propose de voir la version Twig de l’exemple que j’ai donné dans l’article : Utiliser un moteur de template avec PHP, pourquoi ?

Souvenez-vous, notre vue qui affiche une vidéo utilisateur :

<?php
// <title></title>
$this->headTitle($this->escape($this->video->title));

$this->headMeta()->appendName('description', $this->excerpt($this->escape($this->video->description), 200, true));

$this->headScript()->appendFile('videos.js');
$this->placeholder('title1')->set($this->escape($this->video->title));
?>

<?php /* Video owner action */ ?>
<?php if ($this->auth->isAllowed($this->currentUser, $this->video, 'edit')) : ?>
    <div class="actions">
        <ul>
            <li>
                <a href="<?php echo $this->url(array('controller' => 'videos', 'action' => 'edit', 'id' => $this->video->id), 'default'); ?>">
                    Editer
                </a>
            </li>
            <li>
                <a href="<?php echo $this->url(array('controller' => 'videos', 'action' => 'delete', 'id' => $this->video->id), 'default'); ?>">
                    Supprimer
                </a>
            </li>
        </ul>
    </div>
<?php endif; ?>

<?php /* Video <object> */ ?>
<div class="video">
    <div class="video-inner">
        <?php echo $this->getVideoObject($this->video->code); ?>
    </div>          
</div>

<?php /* Video details */ ?>
<div class="video-details">
    <div class="description">
        <?php echo nl2br($this->escape($this->video->description)); ?>
    </div>
    <div class="author">
        <a href="<?php echo $this->url(array('id' => $this->video->user->id), 'user_route'); ?>"
           title="Voir le profil de <?php echo $this->escape($this->video->user->login); ?>">
           <?php echo $this->escape($this->video->user->login); ?>
        </a>
    </div>
</div>

<?php /* List of video comments */ ?>
<div class="comments">
    <?php foreach($this->comments as $comment) : ?>
        <div class="comment-item" id="comment-<?php echo $comment->id; ?>">
            <div class="comment-author">
                <a href="<?php echo $this->url(array('id' => $comment->user->id), 'user_route'); ?>"
                   title="Voir le profil de <?php echo $this->escape($comment->user->login); ?>">
                   <?php echo $this->escape($comment->user->login); ?>
                </a>
            </div>
            <div class="comment-text">
                <?php echo nl2br($this->escape($comment->text)); ?>
            </div>
            <div class="comment-actions">
                <ul>
                    <?php if ($this->auth->isAllowed($this->currentUser, $this->video, 'comment')) : ?>
                        <li>
                            <a href="<?php echo $this->url(array('controller' => 'videos', 'action' => 'add-comment', 'id' => $this->video->id), 'default', true); ?>">
                                Ajouter
                            </a>
                        </li>
                    <?php endif; ?>
                       
                    <?php if ($this->auth->isAllowed($this->currentUser, $comment, 'edit')) : ?>
                        <li>
                            <a href="<?php echo $this->url(array('controller' => 'videos', 'action' => 'edit-comment', 'id' => $comment->id), 'default', true); ?>">
                                Editer
                            </a>
                        </li>                                                    
                    <?php endif; ?>
                </ul>
            </div>
        </div>
    <?php endforeach; ?>
</div>

La version Twig :

{# OVERRIDE #}

{% headTitle video.title %}
{% metaName 'description' with video.description|excerpt(200) %}
{% javascript 'js/videos.js' %}

{% holder 'title1' with video.title %}


{# CONTENT #}

{# Video owner action #}
{% if currentUser is allowed(video, 'edit') %}
    <div class="actions">
        <ul>
            <li>
                <a href="{% route 'default' with ['controller': 'videos', 'action': 'edit', 'id': video.id] %}">
                    Editer
                </a>
            </li>
            <li>
                <a href="{% route 'default' with ['controller': 'videos', 'action': 'delete', 'id': video.id] %}">
                    Supprimer
                </a>
            </li>
        </ul>
    </div>
{% endif %}

{# Video object #}
<div class="video">
    <div class="video-inner">
        {% hlp 'getVideoOject' with [video.code] %}
    </div>          
</div>

{# Video details #}
<div class="video-details">
    <div class="description">
        {{ video.description|nl2br }}
    </div>
    <div class="author">
        <a href="{% route 'user_route' with ['id': video.user.id] %}" title="Voir le profil de {{ video.user.login }}">
            {{ video.user.login }}
        </a>
    </div>
</div>

{# List of video comments #}
<div class="comments">
    {% for comment in comments %}
        <div class="comment-item" id="comment-{{ comment.id }}">
            <div class="comment-author">
                <a href="{% route 'user_route' with ['id': comment.user.id] %}" title="Voir le profil de {{ comment.user.login }}">
                   {{ comment.user.login }}
                </a>
            </div>
            <div class="comment-text">
                {{ comment.text|nl2br }}
            </div>
            <div class="comment-actions">
                <ul>
                    {% if currentUser is allowed(video, 'comment') %}
                        <li>
                            <a href="{% route 'default' with ['controller': 'video', 'action': 'add-comment', 'id': video.id] %}">
                                Ajouter
                            </a>
                        </li>
                    {% endif %}
                       
                    {% if currentUser is allowed(comment, 'edit') %}
                        <li>
                            <a href="{% route 'default' with ['controller': 'video', 'action': 'edit-comment', 'id': comment.id] %}">
                                Editer
                            </a>
                        </li>                                                    
                    {% endif %}
                </ul>
            </div>
        </div>
    {% endfor %}
</div>

Attention

Dans cet exemple j’utilise certaines expressions (is allowed par ex.) que vous ne retrouverez pas dans le package Ano_ZFTwig. C’est une indication pour vous donner une idée de la syntaxe équivalente avec Twig. Ces fonctionnalités sont spécifiques, à vous de les implémenter.

Je suis conscient qu’il reste encore un peu de travail pour une intégration complète dans ZF, mais je vous laisse le soin d’ajouter vos propres briques à l’édifice, je ne doute pas que vous allez jouer avec les sources et les adapter selon vos besoins.

Au plaisir !

9 commentaires :

  1. Pingback: Utiliser un moteur de template avec PHP, pourquoi ? | anonymation – blog – développement et architecture web

  2. Pingback: Introduction à Symfony 2 | anonymation – blog – développement et architecture web

  3. Hello Benjamin,

    Merci pour ton super tuto et tous mes encouragements pour continuer le travail amorcé.

    J’ai ajouté ton tuto sur Twig dans la FAQ Zend Framework.

    Cordialement, Fred

  4. mits :

    Hello,

    I testing Ano_ZFTwig and I’m delighted of this solution. I have only one question – how can I use zf helper in your adapter: headScript()->captureStart() ?> alert(123) headScript()->captureEnd() ?>

    ?

    Best Regards

    • Hi,

      This is not supported by Ano_ZFTwig for now. In order to do that, you need to implement your own extension and/or filter/function for Twig.

      It should be something like :

      {% javascript %}

      <script type="text/javascript">
          // some javascript
      </script>

      {% endjavascript %}

      You can find many code example within Twig. For the « set » tag i.e. => http://www.twig-project.org/doc/templates.html#assignments

      Maybe i will add some support for that, when ? Can’t tell :)

  5. mits :

    Hello once again,

    I have next question, how can I use my views helpers? If I use this method: {% hlp ‘messenger’ with [] %} for my helper: Core_View_Helper_Messenger extends Zend_View_Helper_Abstract

    I recive an error:

    Zend_Loader_PluginLoader_Exception: Plugin by name ‘Messenger’ was not found in the registry; used paths: Zend_View_Helper_: Zend/View/Helper/:/var/www/html/project/application/views/helpers/ thrown in /var/www/html/project/library/Zend/Loader/PluginLoader.php on line 412

    my helper is in two paths: /var/www/html/project/application/views/helpers/ and /var/www/html/project/library/Core/View/Helper

    How can I fix it ?

    • If your helpers namespace is correctly registered, any helper should work.

      Some lead according to your error message :

      • if your helper is a part of your application it should be into the application/views/helpers/ and the class namespace should be : Default_View_Helper_Messenger, if « Default » is the namespace you defined for the default module (into application.ini).
      • if your helper is a part of a third-party library, you have to register your library namespace into configuration (application.ini ), ie : autoloaderNamespaces[] = My_
        And then, you must register the helper path to the view resource, into application.ini again : resources.view.helperPath.My_View_Helper_ = My/View/Helper
        And of course, your library directory must be in the include_path.

      For further information, please refer to the ZF’s documentation, or forums.

  6. mits :

    and this: {% route ‘my_route’ with ['param1': 'value1'] %} also not working:

    Fatal error: Uncaught exception ‘Twig_Error_Syntax’ with message ‘An array element must be followed by a comma (,). Unexpected token « Twig_Token::PUNCTUATION_TYPE » of value « : » (« Twig_Token::PUNCTUATION_TYPE » expected with value « , »)

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.

*

Tags HTML autorisés : <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>
Tag code : [cc lang="langage"][/cc] (ex. [cc lang="php"][/cc])