Article écrit par Nicolas Cavigneaux
Introduction
Phoenix est un framework Web écrit en Elixir. Il est conçu autour d’une architecture orientée temps réel.
Phoenix étant écrit en Elixir, il repose sur la VM Erlang qui offre un socle solide tant en termes d’outillage que de robustesse. Ces outils nous permettront d’être productif et de mettre en place des applications concurrentes et fiables.
Productivité
Avant tout, Phoenix est un framework qui favorise la productivité en fournissant les outils dont tout développeur Web aura besoin :
- un cadre d’architecture pour votre code
- un outil de manipulation des bases de données
- un routeur permettant de lier des requêtes à des actions dans votre code
- un langage de templating et des helpers pour écrire le code HTML
- une gestion performante de l’encodage / décodage du JSON pour faciliter la mise en place d’API
- des outils d’internationalisation
- tout l’outillage fourni de base avec Erlang et Elixir
Pouvoir être productif est un bon point de départ, mais il est aussi important que le langage / framework qu’on utilise nous permette d’écrire des applications maintenables.
Elixir, langage fonctionnel, encourage l’écriture de multiples fonctions simples ayant un but unique et précis. On va ensuite empiler ces fonctions pour mettre en œuvre un comportement donné. Phoenix tend à être le plus explicite possible sur les différentes couches qui composent le cheminement d’une requête. Vous aurez, par défaut, une application configurée pour répondre à la majorité des cas d’usage, mais s’il s’avère que vous devez changer un comportement par défaut, cela reste toujours possible et de manière simple.
L’une des raisons qui fait de Phoenix un framework productif sur le long terme (et donc maintenable) est qu’il se repose sur le concept d’immuabilité. Avec un langage comme Ruby par exemple, que se passe-t-il avec le code suivant :
list = [1, 2, 3] do_something(list)
Est-ce que notre variable list
contient toujours [1, 2, 3]
? Impossible de le dire avec certitude.
Avec Elixir ce cas de figure est impossible. list
contiendra forcément toujours [1, 2, 3]
après l’invocation de do_something
. Peut-être que cette fonction aura retourné un nouveau tableau, mais il n’aura pas pu modifier directement list
.
L’immuabilité permet de plus facilement comprendre le déroulement des événements et d’être plus serein quant à ce qu’il se passe dans le code. Pas d’effet de bord caché.
Chaque fonction prend en entrée ce qu’elle est amenée à manipuler. En sortie, elle pourra retourner une nouvelle structure contenant des modifications sans que cela n’affecte les données passées en entrée.
C’est quelque chose qui est particulièrement difficile de garantir avec des langages orientés objet.
Concurrence
Pouvoir mettre en place du code concurrent est un besoin de plus en plus systématique dans les applications web. Pour rendre nos applications plus réactives, plus riches, on souhaite souvent pouvoir jouer plusieurs portions de code en parallèle pour que l’une n’ait pas à attendre l’autre pour donner des retours à l’utilisateur.
Malheureusement la concurrence n’est généralement pas un sujet simple et vient avec son lot de surprises. Il est souvent difficile de synchroniser les différents processus concurrents, de s’assurer qu’ils ne se marchent pas sur les pieds, etc. On aura aussi parfois besoin d’avoir recours à des outils externes, plus ou moins lourds, pour nous mâcher le travail.
Il existe plusieurs types de concurrence. On pourra par exemple utiliser plusieurs processus système en parallèle. Disons une instance de votre application par cœur, cette solution est facile à mettre en œuvre mais elle est malheureusement très gourmande parce que les processus ne peuvent pas se partager la mémoire.
L’autre solution est d’utiliser les cœurs de manière optimale à travers des threads (user space), on ne lancera dans ce cas qu’une seule instance de l’application mais c’est elle qui se chargera de créer des threads sur les différents cœurs disponibles et de partager l’information entre eux.
Le souci de cette solution est qu’il est assez difficile pour le développeur d’écrire du code efficace tirant parti des threads sans risquer de créer des bugs très difficiles à traquer comme les race conditions ou les dead locks.
Avec Elixir, et surtout grâce à la VM Erlang, votre code sera automatiquement distribué de manière optimale sur les différents cœurs sans que vous n’ayez quoi que ce soit à faire. Vous écrivez votre code classiquement et la VM s’en occupe pour vous.
Le fait que la VM s’en occupe pour nous signifie également que tout ce que vous utiliserez au quotidien dans l’éco-système Elixir profitera de performances accrues. Les compilations du projet, jouer les tests, récupérer les dépendances, tout ça se fera de manière concurrente en utilisant au mieux les cœurs disponibles sur votre machine.
Fiabilité
Erlang apporte avec lui les concepts de processus liés, supervisés et d’arbres de supervision. C’est l’une des clés pour l’écriture d’applications fiables.
La supervision permet de s’assurer pour nous que nos processus tournent correctement. Si l’un d’entre eux venait à planter, le superviseur pourrait le relancer en restaurant son dernier état valide connu et pourrait également relancer les processus liés si besoin.
Le crash d’une portion de l’application devient donc beaucoup moins problématique puisqu’on pourra facilement y palier, de manière automatique, en faisant en sorte qu’un nouveau processus prenne sa place.
L’arbre de supervision peut entièrement être parcouru, contrôlé et analysé en temps réel, même à travers un cluster d’applications.
Premiers pas
Entrons maintenant dans le vif du sujet.
Nous allons créer notre première application. Cette première application nous permettra de découvrir comment fonctionne le cycle traditionnel requête / réponse au sein d’une application Phoenix.
Nous passerons en revue les différentes couches d’une application, apprendrons comment structurer l’application en petites fonctions s’appelant les unes, les autres. Nous verrons comment communiquer avec la base de données, puis nous construirons notre propre système d’authentification.
Nous verrons ensuite comment mettre en place des tests automatisés pour s’assurer du bon fonctionnement de notre application.
Nous allons donc créer une application assez classique mais en tirant parti des forces d’Elixir pour qu’elle soit rapide, robuste et facile à prendre en main.
Préparation de l’environnement
Commençons par installer les dépendances.
Phoenix nécessite Elixir qui lui-même nécessite Erlang. Installons-les ! Dans mon cas, je travaille sous macOS et j’utiliserais brew
pour installer les paquets. Il vous faudra adapter les commandes à votre gestionnaire de paquets :
$ brew install erlang
En ce qui me concerne, j’obtiens la version 24.0.1. On peut maintenant installer Elixir :
$ brew install elixir
J’obtiens ici la version 1.12.0.
On peut maintenant installer le gestionnaire de paquets et de tâches d’Elixir, j’ai nommé Hex
:
$ mix local.hex
Ce qui m’installe la version 0.21.2.
Dans notre application nous allons utiliser une base de données et générer des vues HTML ainsi que du JavaScript. Il nous faut donc installer PostgreSQL et Node :
$ brew install postgresql node@14 $ psql --version $ node --version
Il est important d’installer Node dans sa version 14. Aujourd’hui Phoenix dépend de node-sass
qui ne supporte pas de versions supérieures de Node. C’est en passe de changer avec une transition vers sass
mais d’ici là soyez vigilant ou vos dépendances JavaScript ne pourront pas être installées.
On peut finalement passer à l’installation de Phoenix !
Pour ce faire, on va utiliser un outil central et utilisé au quotidien en tant que développeur Elixir. Mix est un outil à mi-chemin entre Rake et Bundler. Mix permet de gérer l’installation de paquets de manière globale ou dans le cadre d’un projet donné. Il sert également à lancer des commandes pouvant aller de la tâche à jouer ponctuellement, au générateur de code ou encore au lancement de votre serveur Phoenix.
$ mix archive.install hex phx_new $ mixphx.new -v
La première commande appelle la tâche archive.install
permettant d’installer globalement un paquet. On lui précise que notre source sera hex
, le dépôt officiel de paquets Elixir et qu’on veut installer le paquet phx_new
.
La seconde commande permet, elle, d’appeler le générateur de projet Phoenix fraîchement installé.
Premier projet jetable
Le premier projet sur lequel nous allons faire nos armes sera un projet jetable. Il va principalement nous permettre de découvrir la structure d’une application Phoenix et de se faire la main avec les différentes fonctionnalités.
$ mix phx.new first
Cette commande crée un répertoire first
avec une arborescence standard pour une application Phoenix. La commande nous propose ensuite d’installer les dépendances, ce qu’on va faire.
En suivant les instructions, on voit qu’il nous reste quelques étapes :
$ # We are almost there! The following steps are missing: $ cd first $ # Then configure your database in config/dev.exs and run: $ mix ecto.create $ # Start your Phoenix app with: $ mix phx.server $ # You can also run your app inside IEx (Interactive Elixir) as: $ iex -S mix phx.server
On nous propose ensuite de créer la base de données. Je vous invite à consulter le fichier config/dev.exs
pour éventuellement changer le nom de la base ou vos identifiants. Vous pouvez ensuite lancer la commande :
$ mix ecto.create
C’est maintenant le moment de lancer le serveur et d’accéder à votre application pour la première fois !
$ mix phx.server open <http://localhost:4000>
Vous avez sur cette page de bienvenue accès à plusieurs liens pratiques comme le guide, la documentation, les différents chats et forums mais aussi au très appréciable LiveDashboard
qui nous fait une belle démonstration de LiveView
livré avec Phoenix et qui permet d’écrire des pages ultra-dynamiques (pensez React ou Vue) sans avoir à écrire de JavaScript.
Premières lignes de code
Pour mettre un peu les mains dans le code, nous allons écrire nos premières lignes.
Nous allons juste faire en sorte qu’une URL donnée nous affiche un message.
Comme pour beaucoup d’autres frameworks web, on va commencer par ajouter une route pour lier une URL à une action d’un contrôleur donné. Pour ce faire, on va modifier le fichier lib/first_web/router.ex
.
Ce fichier contient déjà plusieurs choses dont il n’est pas nécessaire de se préoccuper tout de suite. Nous y reviendrons en détail un peu plus tard. Ce qui nous intéresse c’est le bloc :
scope "/", FirstWeb do pipe_through :browser get "/", PageController, :index end
Pour commencer, un scope est défini pour signifier que les URLs commençant par un / seront gérées ici.
Le pipe_through
est un pipeline qui permet d’encapsuler toutes les opérations communes à une requête HTTP classique dans le navigateur. Nous y reviendrons.
La ligne suivante déclare que l’URL “/” fera appel à l’action index
contrôleur PageController
. C’est notre route par défaut.
On va pouvoir ajouter une nouvelle route pour développer notre première fonctionnalité. Juste en dessous on définit une nouvelle route :
get "/hello", HelloController, :world
On pourrait essayer de sauvegarder notre fichier et tenter d’accéder à l’URL, mais on obtient aussi vite une erreur.
En effet, pour le moment nous n’avons ni créé le contrôleur HelloController
, ni son action world
.
Vous vous demandez peut-être ce que signifie le FirstWeb.HelloController.init/1
, notamment la partie /1
. En Elixir, c’est ce qu’on appelle l’arité. Ce chiffre nous indique le nombre de paramètres qu’attend la fonction en question. Pour mémoire, en Elixir, il est possible de définir plusieurs fonctions ayant le même nom. Le nombre qu’on voit après le /
permet donc de connaître la version utilisée.
Pour corriger ce problème, on va créer le fichier lib/first_web/controllers/hello_controller.ex
.
Vous vous demandez peut-être pourquoi on utilise parfois l’extension .ex
et d’autres fois l’extension .exs
?
Tous les fichiers qui ont vocation à embarquer du code métier, de production utiliseront l’extension .ex
qui sont les fichiers compilés par la VM et qui sont donc optimisé. Les fichiers .exs
sont plutôt destinées à des fichiers de configuration ou des scripts qui n’auront pas une grande influence sur les performances globales et peuvent donc être interprétés à la volée.
Revenons à notre contrôleur :
defmodule FirstWeb.HelloController do use FirstWeb, :controller def world(conn, _params) do render(conn, "world.html") end end
Dans ce contrôleur, qui va a l’essentiel, on déclare un module dans lequel on utilise la macro use
pour faire savoir à notre module qu’il doit se comporter comme un contrôleur et donc mettre en place quelques fonctionnalités de base.
Ensuite, on définit notre fonction pour l’action world
. Elle prend deux paramètres, la connexion (la requête) et les paramètres qui sont passés lors de la requête.
Ici, on précède params
d’un underscore parce que nous savons que nous n’allons pas utiliser cette variable. On en informe donc le compilateur.
Cette fonction ne fait rien de plus que d’essayer de rendre la vue world.html
dans le contexte de conn
.
On pourrait à nouveau essayer d’accéder à notre page mais sans succès. Il nous reste quelques éléments à mettre en place, la vue et le template.
On commence par créer la vue, qui est une étape intermédiaire entre l’action contrôleur et le rendu du template. C’est dans la vue qu’on pourra préparer les données qu’on va afficher.
Créons donc le fichier lib/first_web/views/hello_view.ex
:
defmodule FirstWeb.HelloView do use FirstWeb, :view end
Pour notre fonctionnalité, nous n’avons aucune donnée à préparer. On va se contenter d’afficher un contenu fixe. On a donc simplement créé notre module et informé qu’il doit se comporter comme une vue grâce au use
.
Si on essaie à nouveau de charger notre page, on fera face à une nouvelle erreur qui nous indique que le template world.html
n’existe pas. Créons-le !
lib/first_web/templates/hello/world.html.eex
:
<h1>Salut tout le monde !</h1>
On sauvegarde et notre page se recharge. Cette fois-ci on a quelque chose !
Notre template EEX a été compilé en fonctions par Phoenix (c’est l’un des secrets de la rapidité des rendus), les changements détectés et la page rechargée automatiquement. Ce template est rendu dans le contexte du layout app.html.eex
, c’est le comportement par défaut.
Utilisation des paramètres
On pourrait rendre cette page un peu plus dynamique en acceptant des paramètres qu’on utiliserait pour modifier l’affichage.
Pour ce faire, on doit d’abord modifier notre route pour qu’elle accepte un paramètre :
get "/hello/:name", HelloController, :world
On a simplement ajouté le :name
qui dénote le fait que c’est un paramètre de l’URL et qui aura pour identifiant name
.
On va maintenant pouvoir, dans notre action contrôleur, se servir de ce nouveau paramètre :
defmodule FirstWeb.HelloController do use FirstWeb, :controller def world(conn, %{"name" => name}) do render(conn, "world.html", name: name) end end
Ici, notre _params
s’est transformé en un map
dans lequel on fait du pattern matching. Si vous n’êtes pas familier avec ce concept, dites-vous simplement que l’interpréteur va essayer de comparer la structure qui est passée dans les paramètres à celle que vous avez décrit dans la fonction. Si elles sont du même type et que les éléments se recoupent alors la fonction pourra être appelée sinon l’appel sera rejeté.
Ici on souhaite recevoir un map qui contient à minima une entrée ayant pour clé le terme name
.
Nous essayerons de revenir plus en détail sur le pattern matching. C’est un élément central de l’écriture de code Elixir et vous serez donc confrontés à son utilisation au quotidien.
Ensuite on fait notre render
en prenant soin de passer notre name
en argument pour qu’il soit passé à la vue.
On peut maintenant passer au template pour utiliser cette nouvelle variable :
<h1>Salut <%= String.capitalize(@name) %></h1>
Comme en ERB, cette syntaxe permet d’interpréter du code directement dans le template et donc de le dynamiser.
On peut sauver notre fichier puis aller à l’adresse http://localhost:4000/hello/synbioz
et la magie va opérer .