• 1 heure
  • Facile
Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Introduction du cours

 

Comment passer des variables à JavaScript avec Twig ?

Tous les développeurs PHP, et ceux sous Symfony 2 n’y échappent pas, ont, un jour ou l’autre, souhaité mettre en œuvre une solution pour passer des données à une application JavaScript (au frontend). Et vous vous êtes sans doute posé la question de comment le faire de façon simple et efficace.

Si vous avez tenté une recherche sur Internet, étonnamment il n’y a pas beaucoup de réponses. Nous pourrions nous dire que c’est sans doute parce que c’est tellement facile que c’est quelque chose qu’un bon développeur sait par instinct. Hum pas si sûr…

Alors quelle est la meilleure façon de passer des variables à JavaScript depuis Symfony2 ?

J'ai étudié et testé plusieurs solutions (dont certaines utilisées actuellement dans des projets open-source) pour résoudre cette problématique et je les expose ci-après. Pour chacune, j'indique les qualités et inconvénients que je trouve à leur mise en œuvre.

Adieu les lignes de configuration : déclarez un service !

Le premier réflexe : ajouter des variables directement dans la vue Twig

La solution qui semble la plus évidente et celle qui vient spontanément à l’esprit du développeur est sans doute d'ajouter les variables directement dans la vue Twig 'Ressources/view/my-view.html.twig' : 

<script type="text/javascript">
    var vmConfigs = {{ configs | raw }};
</script>

(Voir la source)

Cette solution est cependant très lourde puisqu’il faut ajouter autant de lignes que de variables à passer, et deviendra donc vite quelque chose comme ça.

Il faut aussi penser à l'échappement de chaque variable en JavaScript, selon si on passe un objet, une chaîne...

De plus, d’où vient la variable configs ? Celle-ci peut être :

  • Une variable globale à l’environnement de Twig, donc déclarée danstwig.global, comme le montre cet exemple de la documentation Symfony. Il faudra cependant ajouter autant de lignes dans la configuration que de variables.

  • Ou une variable locale déclarée dans un contrôleur, qui reste donc spécifique à une seule page.

Comme vous pouvez le voir, cette solution est certes simple et presque immédiate, mais elle s'avère difficilement maintenable et qui risque d’introduire une faille de sécurité si vous l'utilisez avec AJAX (voir guidelines de l’OWASP). Vous pouvez utiliser cette solution occasionnellement dans le cas d’une validation et/ou de tests, mais ça ne peut pas être une solution pérenne.

La solution : déclarez un service pour contenir les variables

Le besoin : pouvoir passer des variables (globalement ou depuis un contrôleur) à JavaScript depuis Twig sans ajouter de ligne de configuration (ni dans Twig, ni JavaScript) pour chaque variable.

La solution : Déclarez un service !

Nous allons déclarer un service nommé par exempleacme.js_varsqui instancie unestdClassde PHP, une classe "vide". Servez vous de cette classe pour lui affecter des attributs tout simplement comme ceci, de n'importe où dans votre application :

<?php
    $this->get('acme.js_vars')->maVariable = 'Ma valeur';

C’est un service que l’on pourra donc appeler dans de nombreux endroits, notamment :

  • dans un contrôleur pour ajouter des variables dans une page précise,

  • dans un écouteur de l’évènement kernel.controller(on y reviendra juste après) pour affecter des variables globales.

Je vous propose de commencer simple : découvrez comment utiliser le service acme.js_varspour la 1re option, c’est-à-dire passer des variables dans une page précise. C’est parti !

Définissez des variables pour une page précise

 Déclarez votre service et configurez Twig

Commencez par déclarer le service acme.js_vars  dans services.yml :

# Resources/config/services.yml
services:
    acme.js_vars:
        class: stdClass

Vous aurez également besoin de passer ce service à twig.globalspour rendre le service disponible dans toutes les vues (c’est la seule fois où l'on touchera à la configuration, promis !).

# app/config/config.yml
twig:
    globals:
        js_vars: "@acme.js_vars"

 Déclarez vos variables à passer à Javascript depuis un contrôleur

Ça y est, maintenant que vous avez déclaré le serviceacme.js_vars, vous pouvez passer plusieurs variables avec une seule instruction depuis un contrôleur ! Par exemple, si vous voulez transmettre les variables contenant un tableau de valeurs à afficher dans Google Chart, il vous suffit d'utiliser la syntaxe suivante :

<?php
    public function indexAction()
    {
        $temperatures = array(
            ['hour', 'temperature'],
            [8,      8.5],
            [9,      9],
            [10,     11],
            [11,     13.5],
            // ...
        );
        
        $this->get('acme.js_vars')->chartData = $temperatures;
        
        // ...
    }

 Récupérez vos variables depuis une vue Twig

 Il ne vous reste plus qu’à insérer les variables dans le DOM depuis une vue Twig :

<div
   id="js-vars"
   data-vars="{{ js_vars|json_encode|e('html_attr') }}"
/>

Et les récupérer avec Javascript (en utilisant ici jQuery) :

// Récupération des variables définies dans le service js_vars
var JsVars = jQuery('#js-vars').data('vars');

// Récuperer une variable
var chartData = JsVars.chartData;

console.log(chartData);
/*

Sortie :

[['hour', 'temperature'], [8, 8.5], ...]

*/

 

Vous pouvez donc passer autant de variables que vous voulez depuis les contrôleurs ou même depuis un autre service.

Définissez des variables globales

On voudrait maintenant définir une variable Javascript qui devra être accessible sur toutes les pages.

Symfony est doté du composant EventDispatcher, qui permet de déclencher des événements à des moments précis, et d'écouter ces événements pour exécuter une fonction lorsqu'un événement est déclenché.

Or, le kernel de Symfony émet des événements à chaque étape du traitement de la requête, notamment l'événement kernel.controller, qui est émis juste avant d'exécuter le contrôleur. On va écouter cet événement en créant un nouveau service (un écouteur, ou listener) et en définissant quelle méthode sera exécutée lorsque l'événement kernel.controller est émis.

Pour accéder au service acme.js_varscréé un peu plus haut, injectez le dans l'écouteur pour pouvoir y lui ajouter des attributs à envoyer à Javascript.

Création de l'écouteur JsVarsInitializeListener  :

src/Acme/AppBundle/Listener/JsVarsInitializeListener.php

<?php

namespace Acme\AppBundle\Listener;

class JsVarsInitializeListener
{
    /**
     * @var \stdClass
     */
    private $jsVars;

    /**
     * @var boolean
     */
    private $appDebug;

    /**
     * @param \stdClass $jsVars
     * @param boolean $appDebug
     */
    public function __construct(\stdClass $jsVars, $appDebug)
    {
       $this->jsVars = $jsVars;
       $this->appDebug = $appDebug;
    }

    /**
     * Initialize js vars
     */
    public function onKernelController()
    {
        $this->jsVars->myGlobalVariable = 'global value';

        $this->jsVars->debug = $this->appDebug;

        $this->jsVars->websocket = array(
            'host' => '127.0.0.1',
            'port' => 25025,
        );
    }
}

Un écouteur est un simple service. De quoi est-il composé :

  • Le constructeur prend en paramètre unestdClass, important, nous injecterons le service acme.js_varsici. Il peut prendre aussi d'autre paramètres, par exemple ici si le mode debug de l'application Symfony est à true.

  • La méthode onKernelControlleur, son nom suit la convention de nommage des écouteur. C'est elle qu'on souhaite exécuter au moment de l'événement kernel.controlleur, car elle est charger d'initialiser des paramètres de configuration dans notre service acme.js_varspour les passer ensuite à JavaScript.

Déclarez cet écouteur en lui ajoutant un tag pour qu'il soit pris en charge par le répartisseur d'événement :

# Resources/config/services.yml
services:
    acme.listener.js_vars_initialize:
        class: Acme\AppBundle\Listener\JsVarsInitializeListener
        arguments: [ @acme.js_vars, %kernel.debug% ]
        tags:
            - { name: kernel.event_listener, event: kernel.controller, method: onKernelController }

Ainsi, vous pourrez récupérer les variableshostetportque vous avez déclarées dans le listener. Il suffit de réutiliser la variableJsVarsque nous avons déclaré un peu plus haut. Vous aurez :

var ws = new WebSocket('ws://'+JsVars.websocket.host+':'+JsVars.websocket.port);

 

‌Vous pouvez partir de cette solution pour répondre à des besoins récurrents comme traduire des pages avec JavaScript ou encore lui passer les URLs générées pour créer des liens. C’est ce qu’on va voir dans ces dernières sections ! Mais avant cela, vous allez devoir créer une classe pour ce service car notre stdClass ne sera plus suffisante...

Passez des traductions à JavaScript

Il faut cette fois créer une classe pour ce service, puis implémenter les méthodes magiques __get ,__set … pour continuer à l’utiliser de la même manière et garder une étanchéité entre les variables passées en attributs jusqu’à maintenant en faisant $jsVars->myVar = 'my value';, et les variables que nous allons utiliser par la suite.

Ces méthodes magiques seront appelées lorsque vous affecterez un attribut à la classeJsVars, et remplacera le comportement habituel de PHP. Dans notre cas, nous voulons stocker les variables dans un tableau au lieu de créer des nouveaux attributs en prenant le risque qu'ils aient le même nom qu'un autre attribut de la classe.

src/Acma/AppBundle/Service/JsVars.php

<?php

namespace Acme\AppBundle\Service;

class JsVars
{
    /**
     * @var array
     */
    private $variables = array();

    /**
     * @param string $key
     *
     * @return mixed
     */
    public function __get($key)
    {
        return $this->variables[$key];
    }

    /**
     * @param string $key
     * @param mixed $value
     *
     * @return JsVars
     */
    public function __set($key, $value)
    {
        $this->variables[$key] = $value;

        return $this;
    }

    /**
     * @param string $key
     *
     * @return boolean
     */
    public function __isset($key)
    {
        return isset($this->variables[$key]);
    }

    /**
     * @param string $key
     */
    public function __unset($key)
    {
        unset($this->variables[$key]);
    }

    /**
     * @return array
     */
    public function getVariables()
    {
        return $this->variables;
    }
}

Pour utiliser des traductions en JavaScript, je vous propose d’utiliser une méthode toute simple : passer vos chaînes traduites sous la forme clé => traduction.

Pour cela, injectez le service de traduction de Symfony (le servicetranslator), puis implémentez une méthode pour ajouter des traductions.

Première étape donc, nous allons ajouter à la classe  JsVarsun tableau de traduction en attribut, et une méthode qui permettra de le remplir.

Ici je souhaite utiliser une dépendance optionnelle pour pouvoir utiliser le service dans d’autres projets où je n’ai pas forcément besoin de passer des traductions.

<?php

namespace Acme\AppBundle\Service;

use Symfony\Component\Translation\TranslatorInterface;

class JsVars
{
    // [...]

    /**
     * @var TranslatorInterface
     */
    private $translator;

    /**
     * @var array
     */
    private $translations;

    /**
     * Dépendence optionnelle, injectez le Translator ici si besoin
     *
     * @param TranslatorInterface $translator
     *
     * @return JsVars
     */
    public function enableTranslator(TranslatorInterface $translator)
    {
        $this->translator = $translator;
        $this->translations = array();

        return $this;
    }

    /**
     * Ajoute une clé => traduction utilisant Translator.
     * Le nom de la méthode '->trans()' sera détécté
     * par l'extracteur de clé de traduction.
     *
     * @param string $key
     *
     * @return JsVars
     *
     * @throws \Exception si Translator n'a pas été injecté
     */
    public function trans($key)
    {
        if (null === $this->translator) {
             throw new \Exception('Translator must be enabled to use trans()');
        }

        $this->translations[$key] = $this->translator->trans(/** @Ignore */ $key);

        return $this;
    }

    /**
     * @return array
     */
    public function getTranslations()
    {
        return $this->translations;
    }
}

Nous allons déclarer cette nouvelle classeJsVarsen tant que service, et lui injecter le service de traduction @translator.

# Resources/config/services.yml
services:
    acme.js_vars:
        class: Acme\Service\JsVars
        calls:
            - [ enableTranslator, [ @translator ]]

 

Ajoutez le tableau de traductions dans le template :

<div
    id="js-vars"
    data-vars="{{ js_vars.variables|json_encode|e('html_attr') }}"
    data-translations="{{ js_vars.translations|json_encode|e('html_attr') }}"
/>

 

Puis créez une tchôte fonction Javascript pour utiliser la traduction.

var JsTranslations = jQuery('#js-vars').data('translations');

function t(key) {
   if (JsTranslations[key]) {
       return JsTranslations[key];
   } else {
       if (console && JsVars.debug) {
           console.warn('Translation not found: '+key);
       }

       return key;
   }
}

t('home.page');

Ainsi, t('home.page');retournera la chaîne traduite comme définie dans vos traductions côté serveur. Si la traduction demandée n'est pas trouvée, la clé est retournée. De plus, si le debug est activé, une alerte log est levée.

Utilisez des URLs générées

On pourrait aussi avoir besoin d'URLs des routes de notre application en JavaScript, comme le fait la fonctionpathde Twig. Malheureusement, les URLs de routes sont générées seulement côté serveur.

Cela tombe bien, notre service JsVars va pouvoir les générer lui-même et les passer à JavaScript !

De la même manière que pour passer des traductions, vous pouvez passer des URLs en utilisant le routeur pour les générer.

Ajoutez donc un tableau en attribut, iciroutes, qui contiendra des paires route => url (par exemple'users_list' => 'users/list'), une méthode pour le remplir, et injectez le générateur d'url de Symfony (@router).

<?php

namespace Acme\AppBundle\Service;

use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

class JsVars
{
    // [...]

    /**
     * @var UrlGeneratorInterface
     */
    private $router;

    /**
     * @var array
     */
    private $routes;

    /**
     * Dépendance optionnelle vers un UrlGeneratorInterface.
     * Injectez le service router ici.
     *
     * @param UrlGeneratorInterface $router
     *
     * @return JsVars
     */
    public function enableRouter(UrlGeneratorInterface $router)
    {
        $this->router = $router;
        $this->routes = array();

        return $this;
    }

    /**
     * Ajoute une url de route à partir de son nom.
     * Le service la stockera sous le format:
     *      'route.name' => '/route/path'
     *
     * @param string $name nom de la route
     * @param array $parameters paramètre de la route
     *
     * @return JsVars
     *
     * @throws \Exception si le routeur n'a pas été injecté
     */
    public function addRoute($name, array $parameters = array())
    {
        if (null === $this->router) {
            throw new \Exception('Router must be enabled to use addRoute()');
        }

        $this->routes[$name] = $this->router->generate($name, $parameters);

        return $this;
    }

    /**
     * @return array
     */
    public function getRoutes()
    {
        return $this->routes;
    }
}

Ensuite, tout comme on a fait pour utiliser les traductions avec JS, on va injecter le routeur dans le service JsVars :

# Resources/config/services.yml
services:
    acme.js_vars:
        class: Acme\Service\JsVars
        calls:
            - [ enableRouter, [ @router ]]

 

Ajoutez le tableau de routes dans le template :

<div
    id="js-vars"
    data-vars="{{ js_vars.variables|json_encode|e('html_attr') }}"
    data-routes="{{ js_vars.routes|json_encode|e('html_attr') }}"
/>

 

 Et créez une fonction JavaScript pour récupérer les URLs qu’on a passées :

var Router =
{
    routes: jQuery('#js-vars').data('routes'),

    /**
     * Récupère l'url à partir du nom de la route
     * 
     * @param {String} name
     * 
     * @return {String}
     */
    generateUrl: function (name)
    {
        if (Router.routes[name]) {
            return Router.routes[name];
        } else {
            if (console && JsVars.debug) {
                console.warn('Route '+name+' not passed to JsVars');
            }
            
            return '#';
        }
    }
};

On peut maintenant faire en JavaScript :

Router.generateUrl('users_list');
// Returns "users/list"

Ça y est, vous avez vu toutes les étapes pour passer des URLs à JavaScript !

Vous pouvez consulter la classe finale dans JsVars.php sur mon compte GitHub.

Vous pouvez également y retrouver tous les exemples que j’ai présentés dans ce cours (intégration sous Twig, classe de l’écouteur, etc.).

En résumé

Et voilà, vous connaissez maintenant une solution simple et maintenable pour passer des variables à JavaScript depuis un contrôleur ou un écouteur.

Vous pouvez également étendre cette solution pour récupérer des traductions et des URLs côté JavaScript.

Allez plus loin

La solution que je vous ai présentée ici est simple et pratique, cependant elle a ses limites car côté JavaScript, pour la traduction, on ne peut pas passer de paramètres ni utiliser la pluralisation

Pour utiliser toute la puissance de la traduction côté JavaScript, je vous invite à jouer avec le bundle BazingaJsTranslationBundle.

 

De même pour créer des URLs, il n’est pas possible de passer de paramètres de route avec la solution présentée ici, donc pour un besoin plus poussé, jettez un oeil do côté du bundle FosJsRoutingBundle, qui a nettement sa place dans une application Javascript qui génère plusieurs URLs.

Aussi sur OpenClassrooms

Créez des pages interactives avec JavaScript

 

 

Exemple de certificat de réussite
Exemple de certificat de réussite