Haxe [1] est un langage de programmation libre crée en 2005 par Nicolas Canasse et la société Motion Twin [2], une société éditrice de jeux vidéos implantée à Bordeaux. Il est multi-plateformes et indépendant de l'environnement d'exécution : il peut être compilé notamment en C++, C#, PHP, Javascript, Flash, Java et Python ; ainsi, le code source d'un jeu Flash est portable pour le Web (HTML5), Android, IOS, …
Haxe utilise une syntaxe qui sera familière aux développeurs C, Java, Javascript, … C'est un langage orienté objet, avec typage strict et inférence de type.
L'intérêt de Haxe pour le développement d'applications Web est d'utiliser le même langage côtés serveur et client, ce qui permet la réutilisation de code. De plus, le fait que ce soit un langage compilé facilite également la mise au point du programme (de nombreuses erreurs sont détectées dès de la compilation).
1. Installation des outils
Le développement d'une application Web en langage Haxe suit le processus suivant :
1) le développeur code son programme en langage Haxe (fichiers *.hx) et HTML (pour les gabarits) ;
2) le compilateur génère du PHP et / ou du Javascript ;
3) le code généré est déployé sur le serveur Web.
Pour installer Haxe manuellement (cette application requiert la version 3.x), télécharger les binaires (http://haxe.org/download), extraire les fichiers de l'archive puis les installer avec les commandes suivantes :
# cd haxe-3.2.0/
# mkdir /usr/lib/haxe
# mv * /usr/lib/haxe/
# ln -sf /usr/lib/haxe/haxelib /usr/bin/
# ln -sf /usr/lib/haxe/haxe /usr/bin/
Pour l'hébergement de l'application Web, il suffit d'un serveur Web supportant le PHP (Apache par exemple) et les bases de données MySQL / MariaDB ou Sqlite3.
Pour éditer le code source, Geany supporte le langage Haxe par défaut. Pour utiliser un autre éditeur ou environnement de développement intégré (IDE), se référer à [3].
2. L'application de gestion de contacts
L'application sera développée en s'inspirant de l'architecture MVC (Modèle-Vue-Contrôleur) et du style d'architecture ReST (Representational State Transfer).
2.1 Organisation des fichiers
L'organisation des fichiers est à la discrétion du programmeur, mais dans le cadre de ce tutoriel, il est proposé de mettre en place l'arborescence suivante :
├── compile.hxml
├── source/
│ ├── Index.hx
│ ├── controlers/
│ │ └── CtrlContact.hx
│ └─── models/
│ └── Contact.hx
└── tpl/
├── styles.css
├── contact-edit.html
└── contacts-list.html
Le fichier compile.hxml contient les directives de compilation ; son contenu est le suivant :
-cp source
-main Index
-php export
-resource tpl/contact-edit.html@contact-edit
-resource tpl/contacts-list.html@contacts-list
La directive cp indique l'emplacement du code source, la directive main définit le nom de la classe contenant le point d'entrée du programme et la directive php le type et le nom du dossier contenant le code compilé.
Dans le cadre d'une application Web, la directive resource permet quant-à elle de lister les gabarits HTML utiles au programme ; d'autres types de ressources seraient également envisageables, comme des fichiers de langue par exemple (pour internationaliser l'application) ; les feuilles de style ne sont pas des ressources car le code du programme Haxe n'a pas besoin d'y faire référence. La syntaxe pour définir une ressource est : chemin_fichier@identifiant (voir [4] pour la documentation détaillée).
2.2 Le modèle
C'est le rôle du modèle de prendre en charge la persistance des données. Ici, elles seront stockées dans une base de données (MySQL / MariaDB ou SQLite3 au choix), en utilisant le mappage relationnel-objet via la bibliothèque SPOD (Simple Persistent Object Database) de Haxe (voir [5] pour la documentation détaillée).
Le mappage relationnel-objet
Le mappage relationnel-objet (ORM – Object Relationnal Mapping) est une technique de programmation qui concilie le paradigme de la programmation orientée objet avec les bases de données relationnelles (SGBD-R) en établissant des correspondances entre classes et propriétés avec tables et champs.
Le développeur se focalise sur les concepts de la programmation orientée objet, sans se soucier de la base de données sous-jacente, l'ORM gérant automatiquement les requêtes SQL (Structured Query Language).
Des métadonnées permettent de spécifier des informations comme la clé primaire de la table, les clés étrangères, etc.
Avec Haxe, la mise en œuvre de l'ORM consiste à :
- créer une classe qui décrit les caractéristiques d'un objet (ici un contact a un identifiant, un nom, un prénom et une adresse électronique) ;
- hériter de la classe Object (du paquetage sys.db) ;
- préciser des métadonnées pour les concepts relevant du SGBD (comme la clé primaire).
package models;
import sys.db.Object;
import sys.db.Types;
using StringTools;
@:id(id) //métadonnée définissant le champ clé primaire de la table
class Contact extends Object {
public var id(default,null) : SId; //'Int' auto-incrémenté
public var firstname(default,set) : String;
public var lastname(default,set) : String;
public var email(default,set) : String;
public function new(firstname : String, lastname : String, ?email : String = "") {
super();
this.firstname = firstname;
this.lastname = lastname;
this.email = email;
}
function set_firstname(firstname : String) : String {
if (null == id || ("" != firstname && null != firstname))
this.firstname = firstname.htmlEscape();
return this.firstname;
}
function set_lastname(lastname : String) : String {
if (null == id || ("" != lastname && null != lastname))
this.lastname = lastname.htmlEscape();
return this.lastname;
}
function set_email(email : String) : String {
var regexp = ~/^[a-z0-9._%-]+@[a-z0-9.-]+\.[a-z][a-z]+$/i;
if ("" == email || regexp.match(email)) this.email = email;
return this.email;
}
}
La classe Object (dont hérite la classe Contact) définit les méthodes insert, update et delete (implémentant les requêtes SQL correspondantes) et la propriété à portée classe (statique) manager (de la classe sys.db.Manager) ; cette dernière dispose notamment des méthodes all et get permettant respectivement d'obtenir la liste des enregistrements de la table ou seulement un seul (à partir de sa clé primaire). C'est le contrôleur qui fait usage de ces méthodes.
Remarquez que le constructeur (fonction new) de la classe n'initialise pas l'identifiant (id) : ce dernier l'est automatiquement lors de l'insertion de l'enregistrement dans la base de données.
2.3 Routage et contrôleur
Le modèle à lui seul ne fait rien ; c'est le contrôleur qui pilote le programme en traitant les requêtes HTTP.
2.3.1 Point d'entrée du programme
Le fichier source/Index.hx contient le point d'entrée du programme (la fonction main), dont les actions consistent (généralement) à :
- initialiser SPOD (le mappage relationnel-objet) et se connecter à la base de données ;
- créer les tables si nécessaire (avec ici un jeu d'essai) ;
- déclencher le routage en fonction des paramètres de l'URL (voir [6] pour la documentation détaillée).
Note sur la déclaration des propriétés en Haxe
En Haxe, lorsque une propriété est publique, les paramètres entre parenthèses permettent d'affiner les droits d'accès et de définir des accesseurs en lecture et écriture :
- le premier restreint la visibilité en consultation et peut avoir pour valeur null (consultation interdite), default (consultation autorisée) ou get (la valeur est retournée par la fonction get_nom_propriété) ;
- le second restreint la visibilité en modification et peut avoir pour valeur null, default ou set.
Notez que public var nom_propriété(default,default) est identique à public var nom_propriété.
Dans l'exemple, la propriété lastname peut être consultée telle qu'elle mais sa modification est contrôlée par une fonction qui force une valeur non nulle dès lors que le contact a été enregistré dans la base de données.
Voici donc le code du fichier source/Index.hx :
import sys.db.Manager;
import php.db.PDO;
import sys.db.TableCreate;
import php.Web;
import haxe.web.Dispatch;
import haxe.ds.StringMap; //tableau associatif
import models.Contact;
class Router {
public function new() {} //il n'y a rien de particulier à faire ici
function doDefault() {
Web.redirect("?/contacts");
}
function doContacts(?id : Int = null) {
new controlers.ContactCtrl(id);
}
}
class Index { //cf paramètre '-main' du fichier 'compile.hx'
public static function main() {
Manager.initialize(); //initialisation de SPOD
//connexion à la base de données; choisir entre SQLite / MySQL:
Manager.cnx = PDO.open("sqlite:" + Web.getCwd() + "data/db.sqlite");
//~ Manager.cnx = PDO.open("mysql:host=localhost;dbname=nom_bd", "login", "mdp");
if (! TableCreate.exists(Contact.manager)) { //création des tables
TableCreate.create(Contact.manager);
var c : Contact;
c = new Contact("Leto", "Atreides", "leto@atreides.dune");
c.insert(); //insert into Contact ...
c = new Contact("Liet", "Kynes");
c.insert();
}
try {
//déclenchement du routage; on passe en paramètre:
//- les paramètres de l'URL (ce qui suit le '?'), cf 'Web.getParamsString()',
//-et une instance de la classe chargée du routage, ici 'Router':
Dispatch.run(Web.getParamsString(), new StringMap(), new Router());
} catch (e : DispatchError) {
Web.setReturnCode(400); //bad request
}
manager.cnx.close();
}
}
La classe Router (qui pourrait avoir un autre nom) est ici responsable du routage ; le routage dépend du premier paramètre transmis à la méthode run de la classe Dispatch ; il s'agit en général (et en particulier si on s'inspire du style d'architecture ReST) des paramètres de l'URL (ce qui suit le ?) ;
- la requête http://www.monserveur.org/index.php (chaîne vide après le ?) déclencherait l'exécution de la fonction doDefault,
- avec la requête index.php?/contacts/5, c'est /contacts/5 qui est transmis au routeur, ce qui déclenche l'exécution de la méthode doContacts (attention à la casse) et le passage de 5 dans le paramètre (facultatif) id ; remarquez que les paramètres de l'URL ne sont pas au format x-www-form-urlencoded.
Une exception est levée si, par exemple, l'utilisateur passe en paramètre de l'URL :
- /contacts/duncan car duncan devrait être de type entier (cf profil de la méthode),
- /agenda/… car il n'y a pas de méthode doAgenda …
2.3.2 Gestion des requêtes concernant les contacts
C'est le constructeur de la classe CtrlContact (dont le nom a été choisi arbitrairement) qui prend en charge les requêtes concernant les contacts. Plusieurs types de requêtes sont possibles (cf CRUD, pour « Create, Retrieve, Update, Delete »), les plus courants étant l'affichage de la liste ou d'un élément ainsi que la création, la mise à jour et la suppression d'un élément. Il faut par conséquent un moyen de distinguer ces différentes requêtes ; la solution retenue ici (mais d'autres sont possibles) consiste à utiliser la valeur de l'identifiant, la présence ou non de données dans le corps de la requête HTTP et la méthode HTTP utilisée (POST pour les modifications et GET pour toutes les requêtes qui permettent d'obtenir des informations). Le schéma du routage proposé est le suivant :
Requête utilisateur | Méthode HTTP et URL | Corps requête HTTP |
Afficher la liste des contacts | GET ?/contacts | |
Saisir un nouveau contact | GET ?/contacts/ | |
Afficher (et saisir) un contact | GET ?/contacts/id_contact | |
Créer un nouveau contact | POST ?/contacts/0 | firstname,lastname, email |
Mettre à jour un contact | POST ?/contacts/id_contact | firstname,lastname, email |
Supprimer un contact | POST ?/contacts/id_contact |
La distinction entre mise-à-jour et suppression se fait donc selon l'absence ou la présence de données dans le corps de la requête.
Ce contrôleur traitera les requêtes de la façon suivante :
- dans le cas d'une consultation : récupération du ou des contact(s) à partir du modèle et transmission à la vue (ici un moteur de gabarits) ;
- dans le cas d'une modification : récupération des données de la requête, mise à jour des enregistrements de la table Contact (via le modèle) et redirection de l'utilisateur vers une page de consultation (ce qui pourrait à juste titre être considéré comme un gaspillage de ressources).
Le corps de la requête est une chaîne au format x-www-form-urlencoded de la forme firsname=Guerney&lastname=Halleck&email= ; cette chaîne, comme en PHP, est analysée et convertie en tableau associatif (cf variable $_POST) par la méthode getParams de la classe Web. Pour accéder aux différents éléments de ce tableau associatif (un StringMap en Haxe), on utilise la méthode get avec le nom du champ en paramètre (exemple : data.get("firstname")).
Le code du fichier source/controlers/ContactCtrl.hx est par conséquent le suivant :
package controlers;
import haxe.Template;
import haxe.Resource;
import haxe.ds.StringMap; //tableaux associatifs
import php.Web;
import models.Contact;
class ContactCtrl {
public function new(id : Int) {
switch (Web.getMethod()) {
case "GET":
if (null == id) retrieveAll();
else retrieveItem(id);
case "POST":
if (0 == id) create(Web.getParams());
else { //'Web.getPostData' est le corps « brut » de la requête :
if (0 == Web.getPostData().length) delete(id);
else update(id, Web.getParams());
}
}
}
function create(data : StringMap<String>) {
var c = new Contact(data.get("firstname"), data.get("lastname"), data.get("email"));
if (0 == c.firstname.length || 0 == c.lastname.length) {
Web.setReturnCode(400); //bad request
} else {
c.insert(); //insert into Contact …
Web.redirect("?/contacts");
}
}
function retrieveItem(id : Int) {
var data : Contact;
if (0 == id) data = new Contact("", "");
else data = Contact.manager.get(id); //select * from Contact where id = …
if (null == data) Web.setReturnCode(404); //not found
else { //initialisation du moteur de gabarits avec le code HTML du fichier
//'tpl/contact-edit.html' (cf ressource définie dans 'compile.hxml')
var tpl = new Template(Resource.getString("contact-edit"));
Sys.println(tpl.execute(data)); //fusion des données avec le HTML
}
}
function retrieveAll() {
var data : List<Contact> = Contact.manager.all(); //select * from Contact
var tpl = new Template(Resource.getString("contacts-list"));
//on nomme la liste des contacts pour pouvoir y faire référence dans le gabarit:
Sys.println(tpl.execute({ theContacts : data }));
}
function update(id : Int, data : StringMap<String>) {
var c : Contact = Contact.manager.get(id);
if (null == c) Web.setReturnCode(404); //not found
else {
c.firstname = data.get("firstname"); //(ne fait rien si la chaîne est vide)
c.lastname = data.get("lastname");
c.email = data.get("email"); //(ne fait rien si non valide)
c.update(); //update Contact … where id = …
Web.redirect("?/contacts");
}
}
function delete(id : Int) {
var c : Contact = Contact.manager.get(id);
if (null == c) Web.setReturnCode(404); //not found
else {
c.delete(); //delete from Contact where id = …
Web.redirect("?/contacts");
}
}
}
2.4 Les gabarits (templates)
Il ne manque plus que la vue, constituée de gabarits HTML pour terminer l'application ; deux écrans sont prévus : le premier pour afficher la liste des contacts, le second pour afficher et éditer un contact. Cette partie est très simple et les codes seront présentés puis rapidement expliqués.
2.4.1 Liste des contacts
Contenu du fichier tpl/contacts-list.html :
<!DOCTYPE html>
<html lang="fr"><head>
<title>Gestion des contacts</title><meta charset="UTF-8"/>
<link rel="stylesheet" type="text/css" href="styles.css"/>
</head><body>
<header><h1>Liste des contacts</h1></header>
<section>
<table><thead>
<tr>
<th>Prénom</th><th>Nom</th>
<th><a class="button" href="?/contacts/0">Nouveau</a></th>
</tr>
</thead><tbody>
::foreach theContacts:: <!-- cf CtrlContact.retrieveAll -->
<tr>
<td>::firstname::</td><td>::lastname::</td>
<td>
<a class="button" href="?/contacts/::id::">Détail</a>
<form method="POST" action="?/contacts/::id::"
onsubmit="return confirm('Supprimer ::firstname:: ::lastname::')">
<input type="submit" value="Supprimer"/>
</form>
</td>
</tr>
::end::
</tbody></table>
</section>
</body></html>
Les directives destinées au moteur de gabarits (voir [7] pour la documentation détaillée) sont ici :
- ::foreach nomListe:: code HTML ::end:: qui permet de dupliquer le code HTML pour chaque élément de la liste en paramètre ; à l'intérieur de cette boucle, l'élément traité est un objet de la classe Contact.
- ::nom_propriété:: remplacé par la valeur de la propriété de l'objet.
2.4.2 Affichage et édition d'un contact
Contenu du fichier tpl/contact-edit.html :
<!DOCTYPE html>
<html lang="fr"><head>
<title>Gestion des contacts</title><meta charset="UTF-8"/>
<link rel="stylesheet" type="text/css" href="styles.css"/>
</head><body>
<header><h1>Édition d'un contact</h1></header>
<section>
::if (null == id)::
<form action="?/contacts/0" method="post">
::else::
<form action="?/contacts/::id::" method="post">
::end::
<fieldset>
<label for="firstname">Prénom :</label>
<input id="firstname" name="firstname" type="text"
required="required" value="::firstname::"/>
</fieldset>
<!--idem pour 'lastname'-->
<fieldset>
<label for="email">Mél :</label>
<input id="email" name="email" type="email" value="::email::"/>
</fieldset>
<fieldset>
<a class="button" href="?/contacts">Retour</a>
<input type="submit" value="Enregistrer">
</fieldset>
</form>
</section>
</body></html>
Il est possible d'effectuer des tests conditionnels à l'aide d'une instruction de la forme ::if (condition):: ... ::else:: ... ::end::.
2.4.3 Feuille de style
Cette feuille de style est minimaliste ; elle permet notamment d'afficher les liens sous la forme de boutons :
table, th, td { border: 1px solid; border-collapse: collapse; }
td form { display: inline; }
h1 { font-size: 14pt; text-decoration: underline; }
a.button { border-radius: 3px; border: 1px solid #aaaaaa; padding: 3px 10px;
background: linear-gradient(#eeeeee, #dddddd); color: #222222;
text-decoration: none; font-family: sans; font-size: 80%; font-weight: normal;
display: inline-block; }
a.button:hover { background: linear-gradient(#ffffff, #eeeeee); }
fieldset { border: none; padding: 0.25em 0; }
label { display: inline-block; width: 100px; }
2.5 Compilation et déploiement
Tous les fichiers nécessaires à l'application étant terminés, il est temps de la compiler et de la tester :
$ haxe compile.hxml
$ mkdir export/data; chmod a+w export/data #uniquement si la base de données est SQLite
$ cp -f tpl/styles.css export/
L'étude du contenu du dossier export (à téléverser sur le serveur Web) permet de comprendre le travail réalisé par le compilateur :
- le fichier index.php fait référence à la méthode main de la classe Index, point d'entrée du programme ;
- le dossier lib contient le code de l'application et des classes Haxe utilisées (compilé en PHP) ;
- le dossier res contient les fichiers ressources.
Conclusion
Ce premier article sur le développement Web avec Haxe a permis de découvrir le fonctionnement de la bibliothèque standard pour le mappage relationnel-objet, le routage d'URL et le moteur de gabarits. Ce qui est appréciable, c'est la rigueur du langage (le typage strict notamment), et l'étape de compilation qui révèle notamment les erreurs de noms de variables (ou d'utilisation de variables non initialisées).
La prochaine étape serait de mettre en œuvre le principe de l'amélioration progressive pour exploiter AJAX (Asynchronous Javascript And XML) si le navigateur le permet, sans changer de langage de programmation.
Références
[1] Site du langage Haxe : http://haxe.org
[2] Site de la société Motion-Twin : http://motion-twin.com
[3] Editeurs et IDE pour le langage : http://haxe.org/documentation/introduction/editors-and-ides.html
[4] Documentation sur l'utilisation des ressources : http://haxe.org/manual/cr-resources.html
[5] Documentation sur les macros SPOD : http://old.haxe.org/manual/spod
[6] Documentation sur le répartiteur d'URL : http://old.haxe.org/manual/dispatch
[7] Documentation sur le moteur de gabarits : http://haxe.org/manual/std-template.html