Article Spherium — Pas à pas d'un jeu avec Ruby on Rails - Version imprimable +- JeuWeb - Crée ton jeu par navigateur (https://jeuweb.org) +-- Forum : Discussions, Aide, Ressources... (https://jeuweb.org/forumdisplay.php?fid=38) +--- Forum : Programmation, infrastructure (https://jeuweb.org/forumdisplay.php?fid=51) +--- Sujet : Article Spherium — Pas à pas d'un jeu avec Ruby on Rails (/showthread.php?tid=8236) |
Spherium — Pas à pas d'un jeu avec Ruby on Rails - Xenos - 14-09-2020 Page de bases et inscriptions Mettre en place Authlogic Pour faciliter ce qui concerne d'authentification, nous allons utiliser un plugin nommé Authlogic. Il nous permettra de manipuler nos sessions très simplement, à la manière d'objets ActiveRecord. Pour l'installer, ajoutons la ligne suivante à notre fichier `Gemfile` :
Ensuite, procédons à l'installation en lançant la commande suivante dans notre console :
Nous allons manipuler des sessions pour les utilisateurs, nous allons donc créer une classe `UserSession` qui étend la classe `Authlogic::Session::Base`. Créons un fichier `user_session.rb` dans le répertoire `app/models/`.
Créer un modèle user et la table users A présent, nous allons créer un modèle classe pour représenter un utilisateur. Nous appellerons cette classe `User` et la déclareront dans le fichier `app/models/user.rb`. Cependant, nous allons la générer automatiquement plutôt que la créer manuellement. Pour cela, tapons la commande qui suit dans la console.
Cette commande va nous générer quelques fichiers utiles, que nous verront un peu plus bas. Contentons-nous d'ouvrir le fichier `app/models/user.rb` crée et modifions-le pour spécifier que nous souhaitons que les utilisateurs puissent s'authentifier grâce à Authlogic.
Grâce au respect des conventions de nommage, Authlogic saura associer les modèles User et UserSession. Maintenant, nous allons créer notre table users. Pour cela, nous allons utiliser une migration. Elle a été générée pour nous par la commande précédente. Avec Ruby on Rails, on ne manipule jamais directement la structure de la base de données : on utilise des migrations, qui vous nous permettre — si on les écrit correctement — de modifier la structure et de pouvoir revenir à une version antérieure du schéma. Regardons le fichier `db/migrate/xxxxxxxxxxxxxx_create_users.rb`.
La migration dispose de deux parties, représentées par des méthodes de classes up et down. La première sera exécutée quand on lancera la migration, l'autre quand on reviendra sur une migration précédente. Ici, le lancement de la migration créera une table users, avec des colonnes de différents types. Certaines colonnes seront utilisées en interne par Authlogic, qui saura quoi en faire grâce au nommage (une fois de plus). Par exemple, à chaque fois qu'un utilisateur connecté consultera une page du site, sa colonne `last_request_at` sera mise à jour. L'appel à la méthode timestamps est un peu différent : il va créer 2 colonne de type datetime : `created_at` et `updated_at`, qui seront entretenues automatiquement par ActiveRecord. Lançons maintenant une commande dans la console pour lancer la migration.
Créer un layout et une page d'inscription Il est temps de créer une page d'accueil et une vue qui présentera le formulaire d'inscription. Nous allons créer un layout qui viendra encadrer les vues. En allant sur notre site (`http://spherium.dev:8080/`), on peut voir un message d'erreur : l'application ne sait pas quoi faire quand on tente de consulter sa racine. Pour lui spécifier un comportement, on va définir une route `root` dans le fichier `config/routes.rb` (qui contient des commentaires très intéressants que je vous invite à lire et à conserver).
La route ajoutée peut se traduire par : quand on accède à la racine de l'application, exécute l'action home du contrôleur ApplicationController (une classe définie dans le fichier `app/controllers/application_controller.rb`). Allons définir cette méthode (les actions sont les méthodes publiques des contrôleurs) dans le fichier `controllers/application_controller.rb` :
Si on consulte notre page, l'erreur a changé, elle indique maintenant que Rails ne trouve pas le fichier de vue associé à l'action. Créons le fichier `app/views/application/home.html.erb`. Petite explication concernant le chemin et le nom du fichier. Il s'agit d'une vue, on la met donc dans le répertoire des vues. Ensuite, on a un répertoire `application/` qui va contenir toutes les vues utilisées par le contrôleur ApplicationController. Enfin, on a un fichier `home.html.erb`. On aurai pu ne garder que la première partie du nom en appelant la vue `home`. Seulement, on fait les choses proprement (ça nous servira plus tard) et on spécifie que cette vue contient du HTML interprété par le moteur de template Erb (car il en existe d'autres). A nouveau, on profite des conventions de nommage. Plus tard, on verra qu'on peut tout à fait demander à une action de rendre du Javascript, ou même du CSS ou des images ! C'est pour ça qu'il est important de spécifier ce que contient la vue. Remplissons ce fichier crée par un message de bienvenue et un lien vers la page d'inscription. On a un problème… On n'a pas de page d'inscription… Créons-la. L'inscription consiste à créer un utilisateur, on va donc créer un contrôleur UsersController avec une action new qui affichera le formulaire. Nous pourrions le créer à la main, mais nous allons le générer en lançant cette commande dans la console.
Cette commande crée le contrôleur UsersController avec une action new et la vue HTML associée. Quelques autres fichiers sont générés, mais nous n'y toucheront pas tout de suite. Nos routes ont également été modifiées !
L'URL de notre page de création sera donc `http://spherium.dev:8080/users/new`. Quand on appellera cette URL avec la méthode GET, Rails exécutera l'action new du contrôleur UsersController. C'est cohérent mais pas très joli, on va plutôt faire en sorte que l'URL soit `http://spherium.dev:8080/register`, mais que l'action exécutée reste la même.
Pour voir les routes disponibles, on peut lancer la commande suivante :
Allons-donc consulter notre URL et là, on voit que la requête semble bien routée, on nous parle de l'action Users#new. En fait, c'est un contenu par défaut de la vue générée, allons la modifier dans le fichier `app/views/users/new.html.erb`.
Ah, du nouveau ! Et de nouveaux problèmes avec. On a donc ici utilisé un helper (une méthode qui vise à nous aider à générer du code dans les vues) nommé form_for() et qui prend un argument étrange… En fait, `@user` est un attribut (une variable d'instance) du contrôleur. C'est comme ça que nous transmettrons des variables du contrôleur vers la vue. Le problème c'est qu'on ne l'a pas défini dans notre contrôleur UsersController. Corrigeons cela.
Et voilà, notre attribut `@user` est défini. Il contient une instance de la classe User. Et vous saurez pourquoi on utilise ça dans quelques instants. Actualisons notre formulaire et… Erreur ! Aucune route ne correspond à l'action create du contrôleur users. C'est vrai. Nous devons-donc remédier à ce problème en créant la route manquante. Je vais vous aider un peu car à moins d'avoir lu la documentation, vous ne pouvez pas savoir ce qui suit. Rails utilise une architecture REST par défaut (que nous utiliserons partiellement). En suivant ces préceptes, il faut envoyer une requête POST à l'URL `http://spherium.dev:8080/users` (avec les données qui conviennent) quand on veut créer un nouvel utilisateur. On peut donc modifier notre fichiers de routes ainsi (et n'oubliez pas de jeter un coup d'œil aux routes avec la commande vue plus haut) :
Notez que le choix du nom create est arbitraire, j'aurais pu mettre jambon, mais ça aurait été moins cohérent. Au moins, on sait que l'action create du contrôleur UsersController sert à créer un utilisateur. À présent, le formulaire s'affiche. Regardons un peu le code source généré. Et plus précisément le formulaire (nous reviendrons sur tout le code qui n'est pas de nous juste après). On peut voir que l'URL d'action du formulaire correspond à ce que je vous avais indiqué, de même que la méthode. On peut aussi voir un champ caché contenant une longue chaîne. Ceci est un mécanisme de protection contre les attaques <a href=“http://en.wikipedia.org/wiki/Cross-site_request_forgery”>CSRF</a>, qui feront l'objet d'une annexe. On remarque que le formulaire _sait_ qu'un utilisateur sera créé si on valide le formulaire. On aura sensiblement le même code pour modifier un utilisateur. On aura l'occasion de voir que les helpers fournis dans Rails sont plutôt intelligents. Bon, voyons maintenant pourquoi il y a du code HTML autre que ce qu'on a mis dans la vue ? Et bien, une fois de plus, les conventions de nommage on frappé ! Si on regarde dans le dossier des vues, on trouve un répertoire `layouts/` contenant un fichier `application.html.erb`. Quand Rails rend une vue HTML, il recherche un layout pour le contrôleur appelé, puis une vue pour l'action appelée. Ici, c'est UsersController qui est appelé. Aucun layout ne correspond, donc Rails cherche un layout pour la classe parente : ApplicationController. Et ce layout existe bien : `application.html.erb`. On va le modifier et l'étudier un peu.
On peut voir que ce layout fait appel à 4 helpers. Le rôle de `stylesheet_link_tag` et `javascript_include_tag` est limpide, inutile de s'y attarder (mais nous les modifierons plus tard). `csrf_meta_tag` est également un mécanisme de protection contre les attaques CSRF, les balises meta qu'il génère seront utilisées dans les requêtes Ajax (l'_Ajaxisation_ de notre jeu fera l'objet d'un chapitre). Enfin, `yield` sert à intégrer dans ce layout le HTML rendue par la vue de l'action demandée, en l'occurrence, notre formulaire. Bon, maintenant reprenons la vue de notre page d'accueil (`app/views/application/home.html.erb`) et ajoutons-y un titre et un lien vers la page d'inscription. On changera pour quelque chose de plus sympa au grès de notre avancement. <p><%= link_to 'Inscription', register_url %></p> Voici un nouveau helper, `link_to`. Son premier argument est un intitulé, le second est une URL. En lisant la documentation de ce helper, on peut voir que ses arguments sont très permissifs, mais généralement nous utiliserons cette forme simple. `register_url` est un autre helper, il est défini dynamiquement à partir des routes. Quand vous affichez les routes (avec la commande `rake routes`), la colonne de gauche présente un libellé (root, register et users, pour le moment) que vous pouvez utiliser en le suffixant de `_path` (pour produire une URL relative) ou de `_url` (pour produire une URL absolue). Notez d'ailleurs que le helper `form_for` que l'on a utilisé dans le formulaire a utilisé le helper `users_path` pour renseigner son attribut `action`. Le helper avait deviné qu'il devait chercher l'URL pour les utilisateurs puisqu'on lui avait transmis une instance de la classe User. Implémenter les actions new et create du contrôleur users Revenons à notre formulaire d'inscription et définissons les actions à effectuer quand on le soumet. Cela se passe dans la méthode create de notre contrôleur UsersController. Je ne montre que la méthode qui change.
Vous noterez que je ne donne pas le code source entier de chaque script, sinon on ne s'en sort plus. Tout d'abord, on instancie un nouvel objet User, comme dans l'action new. Mais ici, on lui passe un tableau associatif (un hash) contenant les informations saisies sur le formulaire. Pour récupérer les paramètres envoyées dans la requête (que ce soit dans l'URL ou non), on utilise le tableau params disponible dans le contrôleur. Si vous observez le fichier de log (`log/development.log`), vous pouvez voir quelque chose comme :
On constate que les paramètres sont rassemblés dans un tableau, et que la clé user de ce tableau contient elle même un tableau. C'est lié à l'utilisation de crochets dans le nom des champs du formulaire (n'hésitez pas à jeter un coup d'œil à la source HTML produite). Notez que les mots de passes sont automatiquement filtré par Rails grâce à une ligne du fichier `config/application.rb`. Une fois l'objet instancié, on tente de l'enregistrer.
L'enregistrement échoue mais on ne sait pas pourquoi… Affichons donc les erreurs. Dans le formulaire d'inscription (`app/views/users/new.html.erb`), ajoutons donc une boucle qui liste les messages d'erreur (dans le cas où il y en a, bien entendu).
Bon, les messages d'erreurs sont clairs, mais en anglais. L'internationalisation (i18n) de l'application viendra plus tard, contentons-nous de l'anglais pour les parties générées. Authlogic défini des règles de validation pour nous. Nous pourrions bien sûr les modifier ou les supprimer, mais nous aurons bien d'autres occasions de nous amuser avec la validation des données. Avant de créer un compte, il reste quelque chose à faire. Notre jeu sera compartimenté en univers pouvant accueillir un certain nombre de joueurs (et pourquoi pas à l'avenir créer des parties privées, etc.). Nous allons donc créer notre modèle Universe et sa table, puis faire en sorte qu'un univers soit crée à la volée quand c'est nécessaire. Créer un modèle Universe et sa table Comme nous l'avions fait pour le modèle User, on va générer celui-ci avec la commande suivante (à la différence près qu'on utilise l'alias g plutôt que le nom complet generate) puis lancer la migration.
On ouvre ensuite le modèle Universe (`app/models/universe.rb`) et on va ajouter quelques lignes.
Ici, on déclare une constante `DEFAULT_MAX_USERS_COUNT`, qui a pour valeur 100 (fixé arbitrairement). Ça veut dire que par défaut, un maximum 100 joueurs pourra jouer sur chaque univers. J'ai choisi de définir une constante plutôt que d'écrire le nombre là où j'en ai besoin, c'est plus robuste. La deuxième ligne est une association. Elle va permettre à mes instances de la classe Universe de disposer de plusieurs méthodes, comme la méthode users, qui renverra les joueurs inscrits dans un univers sous forme d'un un tableau d'objets User. Comme d'habitude, cette magie opère grâce aux convention de nommage. Pour récupérer les utilisateurs lié à l'univers, ActiveRecord va chercher une colonne universe_id (puisque l'association est déclarée dans le modèle Universe) dans la table users. Ensuite, on défini la méthode de classe `find_or_create_open_universe` qui cherchera un univers ouvert et, si elle n'en trouve pas, en créera un. Le corps de la fonction peut se lire par : retourne ce qui est à gauche du OU (`||`) à moins que ça ne retourne `false` ou `nil`, auquel cas retourne ce qui est à droite du OU. Cette méthode permettra de créer des univers à la volée selon les besoins. Enfin, on enregistre deux callbacks : des méthodes qui seront appelées à un moment du cycle de vie d'un objet. En l'occurrence, les méthodes `setup_max_users_count` et `generate_name` seront respectivement appelées avant et après la création d'un objet Universe. Nous définissons ces méthodes avec une portée privée, puisqu'elle ne devront pas être appelées ailleurs. Les méthodes sont simples, chacune d'elle on affecte un attribut. Vous vous demanderez peut-être pourquoi on ne donne pas de nom à l'univers avant de l'enregistrer, puisque dans notre cas, une requête UPDATE va être nécessaire. C'est simplement parce qu'avant d'être enregistré dans la base de données, l'ID est nul, donc le nom serait toujours “Spherium #0000”. Ici, on génère le nom une fois que l'ID est généré (automatiquement par la base de données), puis on lance la sauvegarde en appelant la méthode `save!`. La différence entre les méthodes `save` (qu'on a utilisé dans le contrôleur UsersController) et `save!` se situe dans le comportement en cas d'échec de l'enregistrement :
Dans notre contrôleur UsersController, on voulait effectivement savoir si l'enregistrement avait échoué afin d'afficher le formulaire avec les erreurs ou de rediriger. Mais cette fois ci, ça <strong>doit</strong> fonctionner ! Passons maintenant au modèle User (`app/models/user.rb`) sur lequel nous allons apporter quelques modifications.
Ça ressemble beaucoup au travail effectué sur le modèle Universe. On découvre tout de même `belongs_to` qui également une association. En fait, elle agit à l'inverse du `has_many` vu plus haut qui permettait de récupérer les utilisateurs associés à un univers. Ici, cette association nous permettra de récupérer l'univers de l'utilisateur. Une fois de plus, les convention de nommage rendent ça très simple : pour récupérer l'univers de l'utilisateur, ActiveRecord cherchera simplement un univers qui a pour ID la valeur présente dans la colonne `universe_id`. Le nom de la table (universes) sera déterminé en mettant au pluriel le nom indiqué. L'option `:counter_cache` permettra de mettre automatiquement en cache le nombre d'utilisateurs inscrit à un univers (plutôt que d'avoir à faire un COUNT à chaque fois). Là aussi, Rails se sert des convention de nommage pour déterminer le nom de la colonne de la table `universes` qui sera utilisée (`users_count`). Et voilà, nous n'avons plus qu'à remplir correctement notre formulaire correctement et nous prendre sereinement une erreur de route manquante dans la tronche. Avant de clôturer ce chapitre, un petit coup d'œil au log nous permet de voir ce qui vient de se passer :
On peut voir que plusieurs test on été fait sur les colonnes email et username, c'est Authlogic qui a intégré des contraintes d'unicité sur ces deux colonnes. Si vous tentez de créer un autre utilisateur avec le même nom ou email, le formulaire affichera une erreur ! On voit aussi la requête SQL effectuée par notre méthode `find_or_create_open_universe`. De toute évidence, elle n'a rien trouvé puisqu'un territoire est insérée juste après, puis son nom est modifié et l'utilisateur est enfin crée. On remarque aussi que le compteur d'utilisateurs associé à un univers est bien mis à jour. Enfin, on constate également que le tout est emballé dans une transaction. Voilà, ce chapitre est terminé ! Si vous n'aviez pas l'habitude de travailler avec un framework MVC, vous devez vous voir que cette architecture permet de conserver des couches simples (et donc faciles à maintenir). Dans notre contrôleur, on sauvegarde juste un utilisateur et toute une cascade de choses est fait en arrière plan (l'affectation d'un univers crée à la volée au besoin, la génération d'un nom pour cet univers, etc.). |