• Contenu
  • Bas de page
logo ouidoulogo ouidoulogo ouidoulogo ouidou
  • Qui sommes-nous ?
  • Offres
    • 💻 Applications métier
    • 🤝 Collaboration des équipes
    • 🛡️ Sécurisation et optimisation du système d’information
    • 🔗 Transformation numérique
  • Expertises
    • 🖥️ Développement logiciel
    • ♾️ DevSecOps
    • ⚙️ Intégration de logiciels et négoce de licences
      • Atlassian : Jira, Confluence, Bitbucket…
      • Plateforme monday.com
      • GitLab
      • SonarQube
    • 📚​ Logiciel de CRM et de gestion
    • 🎨 UX/UI design
    • 🌐 Accessibilité Numérique
    • 🗂️​ Démarches simplifiées
    • 📝 Formations Atlassian
  • Références
  • Carrières
    • 🧐 Pourquoi rejoindre Ouidou ?
    • ✍🏻 Nous rejoindre
    • 👨‍💻 Rencontrer nos collaborateurs
    • 🚀 Grandir chez Ouidou
  • RSE
  • Ressources
    • 🗞️ Actualités
    • 🔍 Articles techniques
    • 📖 Livres blancs
    • 🎙️ Interviews Clients
Nous contacter
✕
Une application mobile dédiée à l’interne !
Une application mobile dédiée à l’interne !
3 novembre 2021
Des regex qui ont la classe !
Des regex qui ont la classe !
12 novembre 2021
Ressources > Articles techniques > Introduction à Phoenix – Découverte d’Ecto, des changesets & des forms

Introduction à Phoenix – Découverte d’Ecto, des changesets & des forms

Article écrit par Nicolas Cavigneaux

Dans l’article précédent, nous avons vu comment afficher des utilisateurs fixes grâce à l’utilisation d’un contexte, d’un contrôleur et de vues.

On peut maintenant essayer d’aller plus loin en permettant de gérer les utilisateurs de manière plus dynamique, grâce à une base de données. Comme nous avons pris soin d’isoler la gestion d’utilisateurs dans un contexte, on ne devrait avoir qu’à modifier le code à cet endroit pour que la magie opère.

Pour simplifier la communication avec la base de données, nous allons utiliser Ecto qui nous offre tout l’outillage nécessaire pour pouvoir la requêter ou y persister des données.

Ecto permet donc d’écrire des requêtes SQL à travers son propre langage, mais il permet également de gérer des changesets qui sont une encapsulation qui permet de prendre des données en entrée, les transformer et les valider.

Création d’un schéma et d’une migration

La première chose à faire pour pouvoir utiliser notre base de données, au-delà de sa configuration qu’on a faite dans l’épisode précédent, est de créer un schéma dont le but est de décrire la structure d’une table ainsi que la migration correspondante qui elle sert à créer ladite table.

On va donc commencer par modifier notre fichier lib/commentator/accounts/user.ex pour y utiliser un schéma Ecto plutôt que notre struct maison :

defmodule Commentator.Accounts.User do   use Ecto.Schema   import Ecto.Changeset    schema "users" do     field :name, :string     field :username, :string      timestamps()   end end 

Grâce à l’utilisation de use Ecto.Schema, on instruit notre modèle du fait qu’on souhaite mettre en place un schéma qui sera à la fois une table de notre base de données, mais également une structure locale de notre module, l’un étant le reflet de l’autre.

On crée donc un schéma users qui contiendra les champs name et username tous deux de type string. On demande également à ce qu’Ecto gère des champs de timestamps pour nous, à savoir inserted_at et updated_at qui seront tenus à jour automatiquement.

Par défaut, en définissant un schéma, Ecto va ajouter un champ id qui servira de clé primaire en base.

Avec ce nouveau schéma on peut continuer à créer des structures %Commentator.Accounts.User comme on le faisait avec la précédente version :

iex> %Commentator.Accounts.User{} %Commentator.Accounts.User{   __meta__: #Ecto.Schema.Metadata<:built, "users">,   id: nil,   inserted_at: nil,   name: nil,   updated_at: nil,   username: nil } 

On peut maintenant créer la migration pour mettre en place la table correspondante en base :

$ mix ecto.gen.migration create_users  * creating priv/repo/migrations/20210920085144_create_users.exs 

On édite ce nouveau fichier pour y indiquer nos champs :

defmodule Commentator.Repo.Migrations.CreateUsers do   use Ecto.Migration    def change do     create table(:users) do       add :name, :string       add :username, :string, null: false       add :password_hash, :string        timestamps()     end      create unique_index(:users, [:username])   end end 

On retrouve nos champs name et username, comme dans notre module. On a également les timestamps. Aussi deux choses supplémentaires qui apparaissent : la génération d’un index d’unicité sur le username pour s’assurer qu’il soit unique au niveau de la base ; et un autre champ, password_hash, qui va nous servir par la suite pour stocker le mot de passe haché de l’utilisateur qui nous sera utile pour gérer l’authentification.

On joue la migration :

mix ecto.migrate  11:02:29.965 [info]  == Running 20210920085144 Commentator.Repo.Migrations.CreateUsers.change/0 forward 11:02:29.971 [info]  create table users 11:02:30.001 [info]  create index users_username_index 11:02:30.005 [info]  == Migrated 20210920085144 in 0.0s 

On a maintenant un module User avec la bonne structure et la table correspondante qui a été créée. On va pouvoir commencer à jouer avec à travers une console IEx. On lance iex -S mix :

iex(1)> alias Commentator.Repo Commentator.Repo iex(2)> alias Commentator.Accounts.User Commentator.Accounts.User iex(3)> Repo.insert(%User{name: "Nico", username: "Bounga"}) [debug] QUERY OK db=3.8ms decode=1.3ms queue=2.8ms idle=1536.5ms INSERT INTO "users" ("name","username","inserted_at","updated_at") VALUES ($1,$2,$3,$4) RETURNING "id" ["Nico", "Bounga", ~N[2021-09-20 09:27:05], ~N[2021-09-20 09:27:05]] {:ok,  %Commentator.Accounts.User{    __meta__: #Ecto.Schema.Metadata<:loaded, "users">,    id: 1,    inserted_at: ~N[2021-09-20 09:27:05],    name: "Nico",    updated_at: ~N[2021-09-20 09:27:05],    username: "Bounga"  }} iex(4)> Repo.insert(%User{name: "Martin", username: "fuse"}) [debug] QUERY OK db=2.9ms queue=1.4ms idle=1553.3ms INSERT INTO "users" ("name","username","inserted_at","updated_at") VALUES ($1,$2,$3,$4) RETURNING "id" ["Martin", "fuse", ~N[2021-09-20 09:27:47], ~N[2021-09-20 09:27:47]] {:ok,  %Commentator.Accounts.User{    __meta__: #Ecto.Schema.Metadata<:loaded, "users">,    id: 2,    inserted_at: ~N[2021-09-20 09:27:47],    name: "Martin",    updated_at: ~N[2021-09-20 09:27:47],    username: "fuse"  }} 

Comme souvent en Elixir, on commence par aliasser les modules qu’on va utiliser pour économiser de la frappe. Ici on aliasse le Repo et le module User.

Une fois fait on ajoute deux entrées en base grâce à la fonction insert/2 qui s’attend à recevoir une structure compatible avec Ecto, ici des structures User avec leurs valeurs respectives.

On reçoit en retour un tuple avec :ok en première valeur pour nous confirmer que l’opération a réussie, le deuxième élément du tuple est le struct User qui a été généré par Ecto. En plus des valeurs qu’on a fournies, on récupère l’id généré ainsi que les timestamps.

Forts de cette nouvelle possibilité, on va pouvoir modifier notre contexte Account pour utiliser la base de données pour gérer nos utilisateurs plutôt que d’utiliser une liste en dur.

On remplace donc le contenu du fichier lib/commentator/account.ex par :

defmodule Commentator.Accounts do   @moduledoc """   Accounts context dedicated to handle users and authentication   """    alias Commentator.Repo   alias Commentator.Accounts.User    def list_users do     Repo.all(User)   end    def get_user(id) do     Repo.get(User, id)   end    def get_user_by(params) do     Repo.get_by(User, params)   end end 

Comme vous pouvez le voir, notre interface n’a pas changé, seul son fonctionnement interne a été adapté pour utiliser la base de données. On n’aura donc pas besoin de modifier le code existant qui utilisait déjà notre contexte.

Une fois encore, on a aliassé les modules utiles. On a ensuite utilisé la fonction all/2 pour récupérer l’ensemble des utilisateurs. On a utilisé get/3 pour récupérer un User sur la base de son id. Finalement on a utilisé get_by/3 pour pouvoir récupérer un User qui correspond aux paramètres fournis.

On peut tester dans notre navigateur pour s’assurer que notre application fonctionne toujours. En se rendant sur l’URL http://localhost:4000/users, on voit nos deux utilisateurs créés précédemment dans IEx :

Liste dynamique des utilisateurs

On peut également se rendre sur l’URL de détail d’un utilisateur http://localhost:4000/users/1 :

Détail d'un utilisateur dynamique

Tout fonctionne comme attendu, notre contexte a parfaitement rempli son rôle en isolant l’interface publique du fonctionnement sous-jacent. On a pu très simplement passer d’une liste d’utilisateurs fixe à une liste d’utilisateurs dynamique en modifiant uniquement quelques lignes de notre contexte.

Mise en place d’un formulaire

Maintenant que nous arrivons à stocker nos utilisateurs en base, il serait pratique de pouvoir en ajouter à la volée depuis l’interface web.

Pour pouvoir faire ça on va devoir ajouter une nouvelle action dans notre contrôleur UserController, un nouveau template, une nouvelle route ainsi qu’une nouvelle fonction dans notre contexte pouvant gérer un changeset.

Commençons par ajouter la route dans le fichier lib/commentator_web/router.ex :

scope "/", CommentatorWeb do   pipe_through(:browser)    get("/", PageController, :index)   resources("/users", UserController, only: [:index, :show, :new, :create]) end 

Plutôt que d’ajouter une nouvelle route en GET et une en POST manuellement comme précédemment, on a ici choisi d’utiliser la macro resources qui est une manière plus courte et simple de mettre en place les routes classiques pour une ressource CRUD. On a précisé qu’on ne voulait que les routes correspondant à l’index, le show, le formulaire de création et l’action de création à proprement parler.

On peut passer à l’ajout de la nouvelle action dans notre contrôleur lib/commentator_web/controllers/user_controller.ex :

def new(conn, _params) do   changeset = Accounts.change_user(%User{})    render(conn, "new.html", changeset: changeset) end 

Cette nouvelle action new fait appel à une fonction de notre contexte qu’on va écrire dans un instant, la fonction Accounts.change_user/1 dont le but est de préparer un changeset Ecto qui pourra être consommé par les fonctions d’aide à la mise en place de formulaires livrées avec Phoenix.

On rend ensuite le template new.html en s’assurant de lui fournir le changeset.

On peut maintenant créer la fonction manquante dans notre contexte Accounts :

def change_user(%User{} = user) do   User.changeset(user, %{}) end 

Cette fonction qui s’attend, par pattern matching, à recevoir une structure User ne fait qu’appeler une fonction qu’on va définir dans notre module User. Une fois encore notre contexte cherche à exposer des méthodes publiques qui pourront être utilisées à l’extérieur sans connaître le fonctionnement sous-jacent :

def changeset(user, attrs) do   user   |> cast(attrs, [:name, :username])   |> validate_required([:name, :username])   |> validate_length(:username, min: 1, max: 20) end 

Cette fonction User.changeset/2 est chargée du travail de fond. Elle s’attend à recevoir une structure (user) et des attributs (attrs).

La structure est passée à la fonction Ecto.changeset.cast/4 qui prend les attributs fournis et les nettoie en n’autorisant que ceux dont la clé est précisée dans le deuxième argument. Au passage, les valeurs des paramètres autorisés sont transformés pour correspondre aux types définis dans le schéma.

Ces valeurs nettoyées sont ensuite passées à Ecto.changeset.validate_required/3 qui s’assure que les clés qui lui sont passées en paramètres sont bien présentes dans le changeset.

Finalement, on vérifie que la longueur du username est bien comprise entre 1 et 20 caractères.

Si toutes les conditions sont remplies, on aura un changeset valide en sortie. Sinon le changeset sera marqué comme invalide.

Il ne nous manque plus que notre formulaire HTML, l’action contrôleur create et on devrait avoir une boucle complète qui nous permettra d’ajouter des utilisateurs.

On crée donc le fichier lib/commentator_web/templates/user/new.html.eex :

<h1>Nouvel utilisateur</h1>  <%= form_for @changeset, Routes.user_path(@conn, :create), fn f -> %> <div>   <%= text_input f, :name, placeholder: "Nom" %> </div> <div>   <%= text_input f, :username, placeholder: "Nom d'utilisateur" %> </div> <%= submit "Enregistrer" %> <% end %> 

Dans ce template, on mixe de l’HTML classique avec quelques fonctions de génération de code HTML. Passer par ces fonctions nous apporte quelques bénéfices comme une sécurité accrue (CSRF), du remplissage automatique des valeurs, etc.

L’élément le plus notable est la fonction form_for qui s’attend à recevoir :

  • un changeset
  • une URL qui est générée grâce à une fonction
  • une fonction anonyme dont le but est de rendre le contenu HTML à l’intérieur du form

On crée ensuite des entrées de texte en passant en premier argument la fonction anonyme, en deuxième l’attribut du changeset concerné et des options en troisième argument.

Pour finir on génère un bouton de soumission du formulaire.

On peut désormais se rendre à l’URL http://localhost:4000/users/new pour constater que notre formulaire s’affiche correctement :

Formulaire d'ajout d'un utilisateur

Ce formulaire pointant vers une action inexistante, nous allons l’ajouter, mais tout d’abord il faudrait créer une fonction dans notre contexte permettant de créer et persister un utilisateur en base :

def create_user(attrs \ %{}) do   %User{}   |> User.changeset(attrs)   |> Repo.insert() end 

Notre nouvelle fonction prend les attributs du formulaire en argument. Elle va créer une structure User vierge, y appliquer le changeset sur la base des attributs fournis puis tenter de l’insérer en base.

Il ne nous reste plus qu’à ajouter notre action contrôleur qui utilise cette fonction :

def create(conn, %{"user" => user_params}) do   {:ok, user} = Accounts.create_user(user_params)    conn   |> put_flash(:info, "#{user.name} ajouté.")   |> redirect(to: Routes.user_path(conn, :index)) end 

L’action reçoit comme toujours la connexion en premier argument, pour le deuxième on met en place du pattern matching sur les paramètres reçus. On souhaite avoir une clé user qui contiendra les informations concernant l’utilisateur à créer. On stocke ces informations dans la variable user_params.

On tente ensuite de créer notre utilisateur à l’aide de la fonction qu’on vient d’ajouter à notre contexte Accounts. Si la création est un succès, toujours par pattern matching, on stocke l’utilisateur nouvellement créé dans la variable user.

Pour finir, on ajoute un message flash à la connexion, puis on redirige sur la liste des utilisateurs grâce à une fonction de génération d’URL.

Si on essaie de créer un nouvel utilisateur, il est effectivement créé en base, on est ensuite redirigé sur la liste où on peut le voir apparaître :

Ajout d'un utilisateur

Liste avec le nouvel utilisateur

C’est une belle avancée, mais malheureusement, si on ne respecte pas les contraintes de validation, par exemple en omettant le username, alors on aura le droit à une page 500 de la part de Phoenix qui nous explique le problème rencontré :

Page d'erreur d'ajout utilisateur

Il va donc falloir modifier notre action pour gérer les cas en erreur :

def create(conn, %{"user" => user_params}) do   case Accounts.create_user(user_params) do     {:ok, user} ->       conn       |> put_flash(:info, "#{user.name} ajouté.")       |> redirect(to: Routes.user_path(conn, :index))      {:error, %Ecto.Changeset{} = changeset} ->       render(conn, "new.html", changeset: changeset)   end end 

On utilise ici l’instruction de branchement case dans laquelle on essaie de pattern matcher sur le succès de la création ({:ok, user}) ou son échec ({:error, changeset).

Dans le cas d’un échec, on récupère le changeset en erreur à l’aide du pattern matching pour pouvoir rendre à nouveau le formulaire sans pour autant perdre les informations déjà entrées par l’utilisateur.

On peut maintenant tester la soumission des informations invalides. Nous n’avons plus d’erreur, mais ce n’est pas encore parfait. La page est bien actualisée, les informations sont conservées, mais on n’a aucun retour sur les erreurs rencontrées.

Pour que l’utilisateur puisse comprendre ce qui cloche, on va modifier le formulaire pour qu’il affiche les erreurs lorsqu’il y en a :

<h1>Nouvel utilisateur</h1>  <%= form_for @changeset, Routes.user_path(@conn, :create), fn f -> %> <%= if @changeset.action do %> <div class="alert alert-danger">   <p>Des erreurs empêchent la création</p> </div> <% end %>  <div>   <%= text_input f, :name, placeholder: "Nom" %> <%= error_tag f, :name %> </div> <div>   <%= text_input f, :username, placeholder: "Nom d'utilisateur" %> <%= error_tag   f, :username %> </div> <%= submit "Enregistrer" %> <% end %> 

Deux ajouts ont été faits :

  • un bloc avec un message est affiché si des erreurs sont présentes dans le changeset ;
  • chaque input se voit complété d’un appel à error_tag qui va afficher les erreurs de validations relatives à l’attribut mentionné s’il y en a.

Formulaire avec affichage des erreurs

Notre formulaire est maintenant plus agréable à utiliser.

Dans le prochain article, nous verrons comment mettre en place un système d’authentification maison. Même si ce n’est pas la meilleure idée pour une application qui doit aller en production, cet exercice aura le mérite de nous permettre de mieux comprendre des éléments essentiels de Phoenix, notamment à propos des plugs.

À lire aussi

Fresque numérique miniature image
16 avril 2025

Fresque du Numérique

Lire la suite

intelligence artificielle Ouicommit miniature image
17 mars 2025

Ouicommit – L’intelligence artificielle en entreprise, on y est ! 

Lire la suite

Image miniature Hackathon Women in Tech
13 mars 2025

Hackathon Women in Tech :  un engagement pour une tech plus inclusive 

Lire la suite

image miniature les nouveautés Atlassian
26 février 2025

Les nouveautés Atlassian en 2025

Lire la suite

Articles associés

Fresque numérique miniature image
16 avril 2025

Fresque du Numérique


Lire la suite
intelligence artificielle Ouicommit miniature image
17 mars 2025

Ouicommit – L’intelligence artificielle en entreprise, on y est ! 


Lire la suite
Image miniature Hackathon Women in Tech
13 mars 2025

Hackathon Women in Tech :  un engagement pour une tech plus inclusive 


Lire la suite

À propos

  • Qui sommes-nous ?
  • Références
  • RSE
  • Ressources

Offres

  • Applications métier
  • Collaboration des équipes
  • Sécurisation et optimisation du système d’information
  • Transformation numérique

Expertises

  • Développement logiciel
  • DevSecOps
  • Intégration de logiciels et négoce de licences
  • Logiciel de CRM et de gestion
  • UX/UI design
  • Accessibilité Numérique
  • Démarches simplifiées
  • Formations Atlassian

Carrières

  • Pourquoi rejoindre Ouidou ?
  • Nous rejoindre
  • Rencontrer nos collaborateurs
  • Grandir chez Ouidou

SIEGE SOCIAL
70-74 boulevard Garibaldi, 75015 Paris

Ouidou Nord
165 Avenue de Bretagne, 59000 Lille

Ouidou Rhône-Alpes
4 place Amédée Bonnet, 69002 Lyon

Ouidou Grand-Ouest
2 rue Crucy, 44000 Nantes

Ouidou Grand-Est
7 cour des Cigarières, 67000 Strasbourg

  • Linkedin Ouidou
  • GitHub Ouidou
  • Youtube Ouidou
© 2024 Ouidou | Tous droits réservés | Plan du site | Mentions légales | Déclaration d'accessibilité
    Nous contacter