SpiderMonkey : Exécuter du Javascript côté serveur, depuis PHP

4 juin 2009Javascript, php, php-5.3, spidermonkey
 Cet article a été rédigé il y a plusieurs années et peut ne plus être tout à fait à jour…

Cet article va nous montrer comment utiliser l'extension PHP SpiderMonkey -- encore expérimentale -- pour exécuter du Javascript côté serveur, depuis PHP.

Sommaire de cet article :


SpiderMonkey : exécuter du code Javascript

Qu'est-ce que SpiderMonkey ?

Avant tout, << SpiderMonkey >> est le nom de code pour l'implémentation en C par Mozilla d'un moteur exécutant du code JavaScript.

SpiderMonkey est le moteur Javascript utilisé par Mozilla Firefox -- du moins jusqu'à la version 3.0 -- ce qui explique sa notoriété.
Ce moteur est aussi utilisé par d'autres logiciels, dont, pour ne citer qu'eux, Adobe Acrobat, ou le Yahoo! Widget Engine (anciennement nommé << Konfabulator >>).

Pour ceux d'entre vous qui sont un peu curieux, vous pouvez aussi jeter un coup d'oeil sur Rhino, qui est un autre moteur Javascript développé par la Fondation Mozilla, mais en JAVA, cette fois.


Extension PHP SpiderMonkey

Depuis mi-février et la sortie de l'extension pecl::spidermonkey développée par Christophe Robin, vous pouvez utiliser le moteur SpiderMonkey depuis PHP, pour exécuter du code Javascript sur le serveur, au sein de votre application PHP.

Quelques points à noter avant d'aller plus loin, tout de même :

  • Cette extension est encore << jeune >> : sa première version est sortie mi-février 2009 !
    • Elle est encore qualifiée de << Beta >>, et souffre encore de quelques problèmes de stabilité, en particulier lorsque PHP tourne en module Apache.
    • Et n'est donc bien entendu pas à utiliser en production !
  • Cette extension ne fonctionne que sous PHP 5.3 !
    • Lui-même actuellement en phase de Release Candidate[1],
    • Et donc probablement pas à utiliser non plus sur un environnement de production.


Compilation et installation

Pour commencer, je me permet d'insister : l'extension spidermonkey ne fonctionne que sur PHP >= 5.3 !

Au niveau de l'installation, deux possibilités s'offrent à vous...


Installation depuis PECL

La première possibilité est d'installation l'extension spidermonkey depuis PECL, à l'aide de la commande pecl :

Pour la recherche du paquet :

$ pecl search spidermonkey
Retrieving data...0%Matched packages, channel pecl.php.net:
=======================================
Package      Stable/(Latest) Local
spidermonkey 0.1.2 (beta)          JavaScript engine for PHP

Et pour son installation :

$ sudo pecl install spidermonkey
downloading spidermonkey-0.1.2.tgz ...
Starting to download spidermonkey-0.1.2.tgz (14,367 bytes)
.....done: 14,367 bytes
6 source files, building
running: phpize
Configuring for:
PHP Api Version:         20041225
Zend Module Api No:      20090115
Zend Extension Api No:   220090115

...
...
...

Build process completed successfully
Installing '/usr/local/lib/php/extensions/no-debug-non-zts-20090115/spidermonkey.so'
install ok: channel://pecl.php.net/spidermonkey-0.1.2
configuration option "php_ini" is not set to php.ini location
You should add "extension=spidermonkey.so" to php.ini


Compilation de la dernière version depuis le SVN

La seconde possibilité est de récupérer les sources de l'extension depuis le SVN du projet, et de les compiler. De la sorte, vous serez certain d'avoir la toute dernière version de l'extension, ce qui peut être utile pour tester une correction de bug, par exemple.

Le principe est semblable à ce que vous avez sans doute déjà pu faire pour d'autres extensions :

En premier lieu, récupération des sources depuis le SVN (Cet article a été rédigé en utilisant les révisions 50 et 51 de l'extension) :

svn co https://ookoo.org/svn/pecl-spidermonkey

Puis configuration et compilation :

cd pecl-spidermonkey
phpize
./configure
make

Et enfin, déploiement du module fraichement compilé :

sudo cp modules/spidermonkey.so /usr/local/lib/php/extensions/no-debug-non-zts-20090115/spidermonkey.so


Activation de l'extension

Enfin, il ne reste plus qu'à activer l'extension, en ajoutant la ligne qui va bien à votre fichier php.ini :

extension=/usr/local/lib/php/extensions/no-debug-non-zts-20090115/spidermonkey.so

Redémarrez Apache, et l'extension spidermonkey devrait maintenant apparaitre en sortie d'un phpinfo :

phpinfo-extrait-spidermonkey-1.png

Et en ligne de commandes :

$ php -m
[PHP Modules]
apc
...
soap
sockets
spidermonkey
SPL
...

[Zend Modules]
Xdebug


API de l'extension spidermonkey

En l'état actuel, l'API de l'extension spidermonkey est des plus simples : l'extension exporte une classe, nommée JSContext, définissant les quatre méthodes suivantes :

// Evalue une portion de code JS
public mixed evaluateScript(string $script);

// Expose une fonction, qui sera utilisable en JS
public void registerFunction(callback $callback [, string $name]);

// Expose une classe, qui sera instanciable en JS
public void registerClass(string $class_name [, string $exported_name]);

// Expose une variable, qui sera utilisable en JS
public void assign(string $name, mixed $value);

Ces quatre méthodes nous permettront d'utiliser les fonctionnalités de l'extension spidermonkey, en passant des variables/fonctions/classes PHP au code Javascript, et en exécutant celui-ci.

Pour plus d'informations, vous pouvez aussi lire le blog-post Current status and API of spidermonkey.

La suite de cet article montrera quelques exemples d'utilisation de cette API, pour exécuter du code Javascript côté serveur, depuis du code PHP.


Exécuter du code Javascript depuis PHP

(Les exemples présentés ici correspondent au répertoire << test-1 >> de l'archive jointe)

Pour commencer, nous allons exécuter une portion de code Javascript simple, en l'appelant depuis un script PHP.

Le principe est des plus simples :

  • On instancie la classe JSContext,
  • Et on appelle la méthode evaluateScript, en lui passant en paramètre le code Javascript à exécuter.

Ici, le code Javascript est enregistré dans le fichier test.js :

var fonctionJsNo1 = function () {
    return 10+32;
};

fonctionJsNo1();

Et voici le code PHP, dans test-light.php :

<?php
$ctx = new JSContext();

$js = file_get_contents(__DIR__ . '/test.js');

$a = $ctx->evaluateScript($js);
var_dump($a);

L'exécution de cette portion de code PHP donne, en CLI :

$ php test-light.php
int(42)

Au niveau des points à noter :

  • Il est extrèmement simple d'exécuter du code Javascript côté serveur, en l'appelant depuis PHP ;-)
  • La méthode evaluateScript renvoit à PHP la valeur retournée par la dernière valeur calculée côté Javascript.
    • Ici, la dernière valeur calculée côté Javascript correspond à la valeur de retour de la fonction fonctionJsNo1.
  • Nous sommes en PHP 5.3 ; j'utilise donc la nouvelle constante magique __DIR__.


Exporter des variables PHP vers le code Javascript

(Les exemples présentés ici correspondent au répertoire << assign-1 >> de l'archive jointe)

Maintenant que nous avons vu comment exécuter du code Javascript depuis PHP, allons un peu plus loin, et exportons vers notre code JS des variables définies côté PHP.

Pour cela, nous utiliserons la méthode assign de la classe JSContext.

Encore une fois, pour commencer, voici la portion de code PHP intéressante (test-assign-1.php) :

<?php
$ctx = new JSContext();

$ctx->assign('entier', 42);
$ctx->assign('liste', array(10, 20, 30));
$ctx->assign('obj', (object)array(
    'a' => 10,
    'pi' => 3.1415,
    'str' => 'Hello, World!',
));

$obj2 = (object)array(
    'first' => 'Hello ',
    'second' => 'World!',
);
$ctx->assign('obj2', $obj2);

$js = file_get_contents(__DIR__ . '/test-assign-1.js');

$str = $ctx->evaluateScript($js);
echo $str;

var_dump($obj2);  // $obj2->second vaut maintenant 'There!' !

Et la portion de code Javascript qui est appelée (test-assign-1.js) :

var str = '';

// Accès à un entier
str += 'entier = ' + entier + '\n';
str += '\n';

// Accès à une liste
var i;
str += 'liste : length = ' + liste.length + '\n';   // undefined !
for (i = 0 ; i < 3 ; i++) {
    str += '  liste[' + i + '] => ' + liste[i] + '\n';
}
str += '\n';

// Accès à un objet
str += 'obj.a => ' + obj.a + '\n';
str += 'obj.pi => ' + obj.pi + '\n';
str += 'obj.str => ' + obj.str + '\n';
str += '\n';

// Modification d'un objet depuis le code JS
obj2.second = 'There!';

// Pour le retour de la chaîne de caractères vers PHP
str;

En sortie, voici le résultat obtenu :

$ php ./test-assign-1.php
entier = 42

liste : length = undefined
  liste[0] => 10
  liste[1] => 20
  liste[2] => 30

obj.a => 10
obj.pi => 3.1415
obj.str => Hello, World!

object(stdClass)#3 (2) {
  ["first"]=>
  string(6) "Hello "
  ["second"]=>
  string(6) "There!"
}

Que faisons-nous ici ?

  • Nous commençons par instancier la classe JSContext, qui nous permet d'exécuter du code Javascript depuis PHP
  • Nous exportons ensuite vers le moteur Javascript, à l'aide de la méthode assign, quatre données PHP :
    • Un nombre entier,
    • Une liste,
    • Un premier objet,
    • Et en second objet, vers lequel nous conservons une référence côté PHP.
  • Le code Javascript est alors exécuté ; ce code :
    • Affiche les données obtenues via les trois premières assignations,
    • Et affecte une nouvelle valeur à la seconde propriété du second objet.
  • Une fois revenu côté PHP
    • On affiche la chaîne de caractères construite côté JS,
    • Et on constate que l'assignation d'objets se fait par référence : l'objet modifié côté Javascript l'a aussi été côté PHP.


Appeler une fonction PHP depuis le code Javascript

Allons maintenant un peu plus loin : nous allons définir une fonction en PHP, et nous l'appellerons depuis le code Javascript.

Premier exemple

(Les exemples présentés ici correspondent au répertoire << test-2/appel-fonction >> de l'archive jointe)

Pour ce premier exemple, voici à quoi ressemblera notre code PHP (test.php) :

<?php
$ctx = new JSContext();

function fonctionPhp1($who) {
    return "Hello, $who!";
}
$ctx->registerFunction('fonctionPhp1', 'fonctionPhp1NomJs');

$a = $ctx->evaluateScript(file_get_contents(__DIR__ . '/test.js'));
var_dump($a);

Et le code Javascript appelé (test.js) :

var test = function () {
    return fonctionPhp1NomJs('World');
};
test();

Le résultat en sortie, quant à lui, se rapproche de ce que l'on pouvait fort logiquement attendre :

$ php ./test.php
string(13) "Hello, World!"

Qu'est-ce qui est à noter ici ?

  • La méthode registerFunction de la classe JSContext permet d'exporter une fonction PHP vers Javascript.
  • Elle prend en premier paramètre un callback correspondant à la fonction PHP (ici, la fonction fonctionPhp1), et, en second paramètre, le nom sous lequel cette fonction doit être connue du code Javascript (ici, fonctionPhp1NomJs)
    • Remarque : c'est ce second nom qui est utilisé par le code Javascript.


Second exemple : avec une fonction anonyme

(Les exemples présentés ici correspondent au répertoire << test-2/appel-fonction-lambda >> de l'archive jointe)

Puisque l'extension spidermonkey ne fonctionne que sur PHP >= 5.3, pourquoi ne pas profiter des fonctionnalités que nous apporte cette nouvelle version du langage ? En particulier, ici, pourquoi ne pas essayer d'utiliser des fonctions anonymes ?

Voici ce que cela pourrait donner côté PHP (test.php) :

<?php
$ctx = new JSContext();

$ctx->registerFunction(function ($who) {
    return "Hello par closure, $who!";
}, 'fonctionPhp2NomJs');

$a = $ctx->evaluateScript(file_get_contents(__DIR__ . '/test.js'));
var_dump($a);

Et côté Javascript, où les choses ne changent guère :

var test = function () {
    return fonctionPhp2NomJs('World');
};
test();

Bien évidemment, la sortie obtenue lors de l'appel de cette portion de code reste proche de la précédente :

$ php ./test.php
string(25) "Hello par closure, World!"

Je disais plus haut que le premier paramètre attendu par registerFunction est un callback correspond à la fonction PHP à exporter vers Javascript... Une fonction anonyme est une fonction PHP parfaitement valide, à partir de PHP 5.3 ;-)


Et avec une fonction interne ?

(Les exemples présentés ici correspondent au répertoire << test-2/appel-fonction-interne >> de l'archive jointe)

Et pour en finir avec cette sous-partie, essayons la même chose, mais cette fois, avec une fonction interne de PHP, et non une fonction définie par notre application.

Côté PHP, nous exportons la fonction strlen (test.php) :

<?php
$ctx = new JSContext();

$ctx->registerFunction('strlen');

$a = $ctx->evaluateScript(file_get_contents(__DIR__ . '/test.js'));
var_dump($a);

Et en l'utilisant côté Javascript (test.js) :

var test = function (str) {
    return 'Length = ' + strlen(str);
};
test('glop');

Voici ce que nous obtenons :

$ php ./test.php
string(10) "Length = 4"

Bien évidemment, il est possible d'exporter vers le code Javascript des fonctions internes de PHP.

Et on notera au passage que le second paramètre de la méthode registerFunction (celui définissant le nom de la fonction côté Javascript) est optionnel : s'il est omis, la fonction portera le même nom côté Javascript que côté PHP -- ce qui n'est probablement pas une mauvaise chose question compréhension du code ^^


Exporter une classe PHP vers le code Javascript

Nous avons vu plus haut comment utiliser registerFunction pour mettre des fonctions PHP à disposition de notre code JS. Passons maintenant aux classes ;-)

Exemple 1 : pour une classe de notre application

(Les exemples présentés ici correspondent au répertoire << registerClass-1 >> de l'archive jointe)

De façon similaire à ce que nous avons fait plus haut avec registerFunction, nous pouvons utiliser la méthode registerClass de la classe JSContext pour exporter une classe PHP, afin de la rendre utilisable depuis le code Javascript.

Voici pour la partie PHP de l'exemple (registerClass.php) :

<?php
class MyClass {
    protected $_who;
    public function __construct($who) {
        $this->_who = $who;
    }
    public function sayHello() {
        echo "Hello, {$this->_who}!\n";
    }
}

$ctx = new JSContext();
$ctx->registerClass('MyClass');

$js = file_get_contents(__DIR__ . '/registerClass.js');
$ctx->evaluateScript($js);

Et pour le côté Javascript (registerClass.js) :

var my = new MyClass('World');
my.sayHello();

Ce qui, en sortie, nous donne :

$ php registerClass.php
Hello, World!

Que fait-on, ici ?

  • Dans notre code PHP, on déclare une classe, nommée MyClass
  • A l'aide de la méthode registerClass, nous rendons cette classe visible depuis Javascript,
  • Et nous exécutons une portion de code Javascript qui instancie cette classe, et appelle une de ses méthodes.

On notera au passage -- même si je n'ai pas exploité cette fonctionnalité ici -- que registerClass accepte en second paramètre le nom à utiliser côté Javascript pour utiliser lac la classe ; par défaut, c'est le même nom que côté PHP qui est utilisé.


Exemple 2 : Avec une classe interne de PHP

(Les exemples présentés ici correspondent au répertoire << registerClass-3 >> de l'archive jointe)

Bien entendu, il est possible d'effectuer le même genre d'export avec des classes internes à PHP.

Par exemple, si on veut écrire une portion de code Javascript pouvant travailler avec les classes DOMDocument et DOMXPath, on peut utiliser une portion de code PHP semblable à celle-ci (registerClass.php) :

<?php
$ctx = new JSContext();

$ctx->registerClass('DOMDocument');
$ctx->registerClass('DOMXPath');

$ctx->registerFunction(function ($str) {echo $str;}, 'print');  // Pour faciliter les affichages
$ctx->registerFunction('var_dump');                             // Et le debuggage

$js = file_get_contents(__DIR__ . '/registerClass.js');
$ctx->evaluateScript($js);

On notera l'export de deux fonctions supplémentaires, nommées print et var_dump :

  • La première nous permettra d'effectuer des sorties écran depuis le code JS,
  • Et la seconde facilitera le débugagge -- après tout, notre code JS manipule des objets PHP !

Et, côté Javascript, il nous est maintenant possible de travailler avec ces deux classes (registerClass.js) :

var xmlString = '<?xml version="1.0" ?>' +
    '<root>' +
    '    <user id="1">' +
    '        <name>NOM-1</name>' +
    '    </user>' +
    '    <user id="2">' +
    '        <name>NOM-2</name>' +
    '    </user>' +
    '</root>';

// Chargement du contenu XML
var dom = new DOMDocument();
dom.loadXML(xmlString);

var xpath = new DOMXPath(dom);

// Récupération du nom de l'utilisation d'id=2
var nodesList = xpath.query('//user[@id="2"]/name');

var i;
for (i = 0 ; i < nodesList.length ; i++) {
    print('name = ' + nodesList.item(i).nodeValue + '\n');
}

Pour résumer :

  • Nous déclarons une chaine de caractères contenant du XML,
  • Nous chargeons celle-ci à l'aide d'un objet DOMDocument,
  • Puis nous instancions la classe DOMXPath,
  • Et nous l'utilisons pour obtenir le nom de l'utilisateur ayant pour identifiant 2 dans notre flux XML.

Finalement, rien d'exceptionnel... Si ce n'est que nous avons écrit tout cela en Javascript, en utilisant les classes définies par PHP ;-)

A noter : je n'ai pas fouillé plus en profondeur, mais j'ai l'impression que l'extension spidermonkey ne supporte pas encore tout à fait les méthodes statiques ; typiquement, je n'ai pas réussi à appeler DOMDocument.loadXML dans mon code JS... A suivre ?


Exemple de cas réel : éviter la duplication de code

(Les exemples présentés ici correspondent au répertoire << exemple-gestion-form-1 >> de l'archive jointe)

Pour terminer cet article, nous allons voir un exemple (simple et bref) d'utilisation << réelle >> de l'extension spidermonkey.

D'aucun diraient, les formulaires sont un des fondements de nos sites Web : j'en utilise un pour rédiger cet article, vous en utiliserez peut-être pour poster des commentaires, nous en utilisons pour nous inscrire sur d'autres sites, et nous passons par eux lorsque nous commandons en ligne...

Qui dit << formulaire >> dit << validation des saisies >> effectuées par l'utilisateur. Et les bonnes pratiques veulent que ces validations soient faites deux fois :

  • Une fois côté client (en Javascript, donc, de manière générale), pour le confort de l'utilisateur et éviter un aller-retour serveur en cas d'erreur de saisie,
  • Et une fois côté serveur (dans notre cas, en PHP, donc), pour la sécurité des données -- Javascript pouvant être désactivé, et un utilisateur mal-intentionné pouvant de toute manière poster des données sans passer par le formulaire.

Suite à celà, deux questions me viennent à l'esprit :

  • N'est-il pas dommage de << dupliquer >> ces vérifications : une fois en JS, une fois en PHP...
  • Combien de fois avez-vous apporté une modification / correction sur une des deux versions, en oubliant l'autre ?

Finalement, est-ce qu'il ne serait pas pratique / judicieux de pouvoir n'écrire ces vérifications qu'une seule fois, en les exécutant à la fois côté client et côté serveur ?
Si nous les développons en Javascript, nous pouvons les exécuter côté client -- nous le faisons déjà -- ; et maintenant, en utilisant l'extension spidermonkey, nous pouvons aussi les ré-exécuter côté serveur ;-)

Pour illustrer un peu cette idée, voici un exemple de mise en place des plus simples :

  • Un formulaire avec deux champs[2] :
    • "nom" : considéré comme correctement renseigné si non-vide
    • "E-mail" : considéré comme correctement renseigné si contient un << @ >>

La page affichant le formulaire, et gérant l'affichage des messages d'erreur si une vérification PHP a échouée, est des plus simples et classique (test.php) :
(Notez que la gestion des vérifications de saisie PHP se fait en haut du fichier, au-dessus de la portion de code que je reproduis ici -- j'y reviendrai plus tard)

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
  <title>Test PECL::Spidermonkey</title>
  <link rel="stylesheet" href="test.css" type="text/css" media="screen" />
  <script type="text/javascript" src="verif-form.js"></script>
  <script type="text/javascript" src="navigateur.js"></script>
</head>
<body>
  <div id="errors">
    <p id="error-nom"
      <?php if (!isset($validation->nom->ok)
        || $validation->nom->ok) : ?>style="display: none;"<?php endif ; ?>>
        Le nom est obligatoire
    </p>
    <p id="error-email"
      <?php if (!isset($validation->email->ok)
        || $validation->email->ok) : ?>style="display: none;"<?php endif ; ?>>
        L'E-mail est obligatoire ; ou incorrect
    </p>
  </div>
  <form method="get" action="test.php" onsubmit="return verificationJsNavigateur();">
    <p>
      <label for="nom">Nom : </label>
      <input type="text" name="nom" id="nom" />
    </p>
    <p>
      <label for="email">E-Mail : </label>
      <input type="text" name="email" id="email" />
    </p>
    <p>
      <input type="submit" name="go" value="Go !" />
    </p>
  </form>
</body>
</html>

A noter, principalement :

  • Un champ de saisie nommé << nom >>,
  • Un second champ de saisie nommé << email >>,
  • Et un bouton de validation nommé << go >>,
  • Le tout pour un formulaire postant sur lui-même : test.php.
  • Et, pour la validation du formulaire, on appelle la fonction Javascript << verificationJsNavigateur >>.

Vous noterez aussi que deux fichiers Javascript sont inclus : verif-form.js, et navigateur.js.

Pour ce qui est de la CSS, ici encore, extrêmement simple et spartiate (test.css) :

@CHARSET "UTF-8";

#errors {
    color: red;
    font-weight: bold;
}

Là où les choses deviennent intéressante, c'est au niveau de la validation Javascript du formulaire : le but étant d'en réutiliser le maximum côté serveur, il nous faut faire une distinction entre :

  • D'un côté, le code qui va inter-agir avec le formulaire ; il sera responsable :
    • De la récupération des données saisies dans le formulaire,
    • Et de l'éventuel affichage des messages d'erreurs.
  • Et, d'un autre côté, le code qui validera les données.

Pourquoi cette distinction ? Parce que le code qui validera des données pourra être réutilisé côté serveur (il s'agira aussi de valider des données ^^ ), alors que le code inter-agissant avec le formulaire, lui, est dépendant du navigateur et de la structure HTML du document.
Le code d'inter-action avec le formulaire, lui, devra nécessaire exister en deux versions : une utilisée côté client, en Javascript, et une utilisé serveur, en PHP.

Pour commencer, donc, voici le code inter-aggissant avec le formulaire (navigateur.js) :

// navigateur.js
var verificationJsNavigateur = function () {

    // Récupération des données depuis le formulaire
    var fields = {
        nom: document.getElementById('nom').value,
        email: document.getElementById('email').value
    }

    // Validation des données
    var resultVerif = verificationMultiPlateforme(fields);
    var ok = true;

    // Affichage des (éventuels) messages d'erreurs
    if (resultVerif.nom.ok === false) {
        document.getElementById('error-nom').style.display = 'block';
        ok = false;
    } else {
        document.getElementById('error-nom').style.display = 'none';
    }

    if (resultVerif.email.ok === false) {
        document.getElementById('error-email').style.display = 'block';
        ok = false;
    } else {
        document.getElementById('error-email').style.display = 'none';
    }

    return ok;
};

Cette fonction, << verificationJsNavigateur >> fait trois choses :

  • Elle récupère les données saisies dans le formulaire,
  • Elle appelle la fonction de vérification de saisies, en lui passant en paramètre ces données,
  • Et en fonction des résultats renvoyés par la fonction de vérification de saisies, elle affiche des messages d'erreurs.

Cette fonction est responsable des interactions avec le document HTML et le formulaire qu'il contient ; pas de la vérification des données en elle-même.

Pour la validation des données, il faut regarder la fonction nommée verificationMultiPlateforme (verif-form.js) :

// verif-form.js
var verificationMultiPlateforme = function (fields) {
    var result = {
        nom: {
            ok: false
        },
        email: {
            ok: false
        }
    };

    if (fields.nom != '') {
        result.nom.ok = true;
    }
    if (fields.email.indexOf('@') != -1) {
        result.email.ok = true;
    }

    return result;
};

Cette fonction reçoit en paramètre les données à valider -- elle ne va absolument pas les extraire du formulaire ! --, et renvoi en retour un objet indiquant, pour chaque donnée, si elle est valide ou non.
Comme dit plus haut, les règles de << validation >> sont volontairement extrêmement simplistes, pour cet exemple ^^

L'intérêt de cette fonction est qu'elle ne travaille que sur des données :

  • En entrée, elle reçoit les données à valider
  • Et en sortie, elle renvoi des données indiquant si les entrées étaient valides.

Autrement dit, cette fonction n'est aucunement liée à la représentation HTML du formulaire, et devrait donc pouvoir être réutilisée pour effectuer les mêmes validations côté serveur ;-)

Pour ce faire, essayons de rajouter tout en haut de notre fichier test.php les quelques lignes suivantes :

<?php
if (isset($_GET['go'], $_GET['nom'], $_GET['email'])) {
    $ctx = new JSContext();
    $js = file_get_contents(__DIR__ . '/verif-form.js');

    $fields = array(
        'nom' => $_GET['nom'],
        'email' => $_GET['email'],
    );
    $data = json_encode($fields);

    $js .= "
var data = $data;
verificationMultiPlateforme(data);
";
    $validation = $ctx->evaluateScript($js);
}
?>

Que faisons nous ?

  • Si le formulaire a été soumis (je disais plus haut qu'il soumettait vers lui-même, rappelez-vous),
  • Alors on instancie la classe JSContext,
  • On lit le contenu du fichier verif-form.js, déjà utilisé côté client, pour récupérer le code de validation des données,
  • Puis on extrait les données récupérées depuis le formulaire, et on les encode sous forme d'un objet Javascript à l'aide de json_encode,
  • Après cela, on ajoute à la fin de la portion de code Javascript définissant la fonction verificationMultiPlateforme l'appel à cette même fonction, en lui passant en paramètre les données extraites du formulaire,
  • Et enfin, nous exécutons le tout.

L'objet $validation récupéré suite à l'exécution du code Javascript ressemble alors à quelque chose de ce type :

object(stdClass)[2]
  public 'nom' =>
    object(stdClass)[3]
      public 'ok' => boolean true
  public 'email' =>
    object(stdClass)[4]
      public 'ok' => boolean false

Il ne nous reste plus qu'à analyser les valeurs des champs de cet objet pour savoir si le formulaire est ou non valide, et, le cas échéant, afficher un message d'erreur.
Pour cela, je vous laisse remonter un peu plus haut, où j'ai posté le reste du fichier test.php : le formulaire en lui-même.

Au final, quelle utilité ?
Pour un code de validation de quelques lignes de long, ne pas avoir à l'écrire à la fois en Javascript et en PHP n'est probablement pas des plus important ; mais imaginez un formulaire à 15 champs (comme on en voit encore trop souvent, devrais-je dire ^^) ; est-ce que vos 300 à 500 lignes de code de validation de saisie ne gagneraient pas à être écrite une et une seule fois ? En particulier au niveau de la maintenance et de son évolution ?

A noter au passage : vouloir utiliser le même code de validation côté client et côté serveur implique que le code de validation doit être distinct du code d'interfaçage avec le formulaire -- ce qui, d'un côté, est certainement une bonne chose, même si ça n'entre pas souvent dans nos habitudes côté Javascript...


Et pour la suite ?

Pour commencer, rappelons que tout ce qui a été vu ici correspond à de l'expérimentation, et est basé sur une extension actuellement non stable -- jusqu'à la version de PHP requise, d'ailleurs[3] !

Au niveau de l'avenir, on peut espèrer que l'auteur continue à travailler sur cette extension, en particulier pour corriger les quelques bugs et plantages sur lesquels on peut encore tomber, en particulier lorsque l'extension est utilisée depuis PHP tournant en module Apache -- certains bugs ont déjà été corrigés, d'ailleurs, entre le moment où j'ai commencé à tester cette extension, et celui où je publie cet article ; donc, si vous détectez des bugs, n'hésitez pas à les reporter !

La question suivante est au niveau des usages : nous avons vu un exemple d'utilisation << réelle >> de l'extension spidermonkey ; je suis certain qu'en cherchant un peu, vous en trouverez d'autres ;-)
Pour ne citer qu'une ou deux idées qui me viennent à l'esprit : faciliter la mise en place de code << back-end >> par un développeur << front-end >> ? Ou faciliter l'écriture de templates, en utilisant un langage plus proche de ceux utilisés côté client ?


Pour finir : j'ai quelque peu pris mon temps pour rédiger cet article, et voila qu'en me levant ce matin, je dépile mes RSS, et je tombe sur un autre article parlant de SpiderMonkey : Using JavaScript in PHP with PECL and SpiderMonkey. Deux articles le même jour, donc, sur cette extension[4] ; ça a un côté rassurant : je ne suis pas le seul à être intéressé ^^
Je n'ai survolé que rapidement l'article de Vikram Vaswani, mais il ne semble pas inintéressant ; il a plus utilisé cette extension sous Apache que moi, qui ai privilégié la ligne de commande, et présente un exemple intéressant, où du code Javascript effectue des requêtes SQL à l'aide de la classe SQLite3, idée que je n'ai pas présentée dans cet article -- vous trouverez cela dit un exemple de ce type, en utilisant PDO, dans l'archive jointe ;-)


Et pour vous ? Avez-vous testé l'extension spidermonkey ? Qu'en avez-vous pensé ? Quel intérêt y voyez-vous ?


Notes

[1] Cet article a été rédigé entre mi-avril et fin main 2009, alors que PHP 5.3 était disponible en version RC1 puis RC2

[2] Les vérifications sont volontairement simplistes ^^

[3] Même si la sortie de PHP 5.3 approche ; d'ailleurs, si vous ne l'avez pas encore testée, il est plus que temps !

[4] Et non, je ne repousse pas encore une fois la publication de cet article ^^