7.2 KiB
Conception
Organisation
Notre projet est divisé en plusieurs parties:
src/API
, qui définit les classes qui implémentent les actions de l’apisrc/App
, qui définit les contrôleurs et les vues de l’application websrc/Core
, définit les modèles, les classes métiers, les librairies internes (validation, http), les gateways, en somme, les élements logiques de l’application et les actions que l’ont peut faire avec.sql
, définit la base de donnée utilisée, et éxécute les fichiers sql lorsque la base de donnée n’est pas initialisée.profiles
, définit les profiles d’execution, voirDocumentation/how-to-dev.md
pour plus d’infofront
contient le code front-end react/typescriptci
contient les scripts de déploiement et la définition du workflow d’intégration continue et de déploiement constant vers notre staging server (maxou.dev//public/).public
point d’entrée, avec :public/index.php
point d’entrée pour la webapppublic/api/index.php
point d’entrée pour l’api.
Backend
Validation et résilience des erreurs
Motivation
Un controlleur a pour but de valider les données d'une requête avant de les manipuler.
Nous nous sommes rendu compte que la vérification des données d'une requête était redondante, même en factorisant les différents types de validation, nous devions quand même explicitement vérifier la présence des champs utilisés dans la requête.
public function doPostAction(array $form) {
$failures = [];
$req = new HttpRequest($form);
$email = $req['email'] ?? null;
if ($email == null) {
$failures[] = "Vous n'avez pas entré d'adresse email.";
return;
}
if (Validation::isEmail($email)) {
$failures[] = "Format d'adresse email invalide.";
}
if (Validation::isLenBetween($email, 6, 64))) {
$failures[] = "L'adresse email doit être d'une longueur comprise entre 6 et 64 charactères.";
}
if (!empty($failures)) {
return ViewHttpResponse::twig('error.html.twig', ['failures' => $failures]);
}
// traitement ...
}
Nous sommes obligés de tester à la main la présence des champs dans la requête, et nous avons une paire condition/erreur par type de validation, ce qui, pour des requêtes avec plusieurs champs, peut vite devenir illisible si nous voulons être précis sur les erreurs.
Ici, une validation est une règle, un prédicat qui permet de valider une donnée sur un critère bien précis (injection html, adresse mail, longueur, etc.).
Bien souvent, lorsque le prédicat échoue, un message est ajouté à la liste des erreurs rencontrés, mais ce message est souvent le même, ce qui rajoute en plus
de la redondance sur les messages d'erreurs choisis lorsqu'une validation échoue.
Schéma
Toutes ces lignes de code pour définir que notre requête doit contenir un champ nommé email
, dont la valeur est une adresse mail, d'une longueur comprise entre 6 et 64.
Nous avons donc trouvé l'idée de définir un système nous permettant de déclarer le schéma d'une requête,
et de reporter le plus d'erreurs possibles lorsqu'une requête ne valide pas le schéma :
public function doPostAction(array $form): HttpResponse {
$failures = [];
$req = HttpRequest::from($form, $failures, [
'email' => [DefaultValidators::email(), DefaultValidators::isLenBetween(6, 64)]
]);
if (!empty($failures)) { //ou $req == null
return ViewHttpResponse::twig('error.html.twig', ['failures' => $failures])
}
// traitement ...
}
Ce système nous permet de faire de la programmation déclarative, nous disons à quoi ressemble la forme de la requête que l'on souhaite,
plustot que de définir comment réagir face à notre requête.
Ce système permet d'être beaucoup plus clair sur la forme attendue d'une requête, et de garder une lisibilité satisfaisante peu importe le nombre de
champs que celle-ci contient.
Nous pouvons ensuite emballer les erreurs de validation dans des ValidationFail
et FieldValidationFail
, ce qui permet ensuite d'obtenir
plus de précision sur une erreur, comme le nom du champ qui est invalidé, et qui permet ensuite à nos vues de pouvoir manipuler plus facilement
les erreurs et facilement entourer les champs invalides en rouge, ainsi que d'afficher toutes les erreurs que l'utilisateur a fait, d'un coup.
HttpRequest, HttpResponse
Nous avons choisi d'encapsuler les requêtes et les réponses afin de faciliter leur manipulation.
Cela permet de mieux repérer les endroits du code qui manipulent une requête d'un simple tableau,
et de garder à un seul endroit la responsabilitée d'écrire le contenu de la requête vers client.
src/App
définit une ViewHttpResponse
, qui permet aux controlleurs de retourner la vue qu'ils ont choisit.
C'est ensuite à la classe src/App/App
d'afficher la réponse.
index.php
Il y a deux points d'entrés, celui de la WebApp (public/index.php
), et celui de l'API (public/api/index.php
).
Ces fichiers ont pour but de définir ce qui va être utilisé dans l'application, comme les routes, la base de donnée, les classes métiers utilisés,
comment gérer l'authentification dans le cadre de l'API, et une session dans le cadre de la web app.
L'index définit aussi quoi faire lorsque l'application retourne une réponse. Dans les implémentations actuelles, elle délègue simplement
l'affichage dans une classe (IQBall\App\App
et IQBall\API\API
).
API
Nous avons définit une petite API (src/Api
) pour nous permettre de faire des actions en arrière plan depuis le front end.
Par exemple, l'API permet de changer le nom d'une tactique, ou de sauvegarder son contenu.
C'est plus pratique de faire des requêtes en arrière-plan plustot que faire une requête directement à la partie WebApp, ce qui
aurait eu pour conséquences de recharger la page
Frontend
Utilisation de React
Notre application est une application de création et de visualisation de stratégies pour des match de basket.
L’éditeur devra comporter un terrain sur lequel il est possible de placer et bouger des pions, représentant les joueurs.
Une stratégie est un arbre composé de plusieurs étapes, une étape étant constituée d’un ensemble de joueurs et d’adversaires sur le terrain,
aillant leur position de départ, et leur position cible, avec des flèches représentant le type de mouvement (dribble, écran, etc) effectué.
les enfants d’une étape, sont d’autres étapes en fonction des cas de figures (si tel joueur fait tel mouvement, ou si tel joueur fait telle passe etc).
Pour rendre le tout agréable à utiliser, il faut que l’interface soit réactive : si l’on bouge un joueur,
il faut que les flèches qui y sont liés bougent aussi, il faut que les joueurs puissent bouger le long des flèches en mode visualiseur etc…
Le front-end de l’éditeur et du visualiseur étant assez ambitieux, et occupant une place importante du projet, nous avons décidés de l’effectuer en utilisant le framework React qui rend simple le développement d’interfaces dynamiques, et d’utiliser typescript parce qu’ici on code bien et qu’on impose une type safety a notre code.