Le livrable se compose de deux parties :
- une API RESTful développée en PHP, accessible à l'URL
/api
, qui permet de manipuler des articles et des paragraphes ; - une application Web développée en Javascript (à l'aide de jQuery), offrant une interface utilisateur pour visualiser et éditer les données renvoyées par l'API.
Une documentation d'installation est également disponible, dans le fichier README.md
.
L'historique du code est géré par git, et hébergé sur un dépôt Github pour permettre une collaboration facile entre les développeurs. L'ensemble du code écrit est placé sous licence MIT.
Une instance de test de l'API ainsi que de l'interface est hébergée chez Heroku, à l'adresse https://warm-mesa-18064.herokuapp.com/. (Une description d'Heroku est disponible plus bas)
Le backend consiste en une API RESTful développée en PHP. Le schéma décrivant cette API est disponible dans le fichier swagger_api.yaml
, et peut être chargé sur l'éditeur Swagger pour une lecture plus aisée. Il est même possible de tester directement sur Swagger l'API.
Nous avons utilisé PHP 7.1, principalement pour la possibilité de typer les paramètres des fonctions et leur valeur de retour. On peut par exemple écrire
public static function getArticleById(Database $db, int $article_id)
{
...
}
Ce typage a deux avantages :
- PHP vérifie au runtime que les arguments passés à une fonction ainsi que son retour sont bien du type renseigné, ce qui permet de détecter plus tôt certains bugs ;
- les IDE peuvent utiliser ces informations pour proposer une autocomplétion plus intelligente, avec les méthodes correspondant aux objets passés.
Le backend utilise un mécanisme de routeur : toutes les requêtes, peu importe l'URL d'appel, seront dirigées vers le fichier index.php
, qui pourra les traiter spécifiquement. On découple ainsi l'emplacement des fichiers sources sur le disque et les URL utilisées.
Comme indiqué dans le README, il y a deux possibilités pour router les URL vers l'index :
- sur un serveur Apache, le fichier
.htaccess
fourni réécrit les URL commençant par/api
vers le fichierindex.php
, et les autres URL sont résolues comme des fichiers classiques dans le dossierpublic/
(c'est dans ce dossier que se trouve le fichierindex.html
compilé, ainsi que les CSS et JS nécessaires) ; - avec le serveur de développement PHP (
php -S 127.0.0.1:8080 -t public $(pwd)/index.php
), on indique à PHP d'utiliser le fichierindex.php
pour toutes les requêtes, et c'est lui qui dans son code décide s'il faut charger l'API (ligne 12) ou s'il faut essayer de résoudre l'URL comme un fichier normal (ligne 14) en cherchant dans le dossierpublic/
grâce au paramètre-t
.
Le cœur du routeur est dans le fichier api.php
. On y charge d'abord la configuration, permettant de se connecter à la base de données, puis on découpe l'URL appelée selon les /
. S'ensuit alors une cascade de switch/case
permettant de charger la bonne action en fonction de la route appelée (/api/articles/1
par exemple), ainsi que de la méthode utilisée (GET
, POST
, PATCH
ou DELETE
). Chacune des méthodes des classes Article.php
et Error.php
définissent le code de statut HTTP correspondant au résultat. Les dernières lignes du fichier (lignes 138 à 147) servent à autoriser les requêtes Ajax provenant d'autres domaines que celui hébergeant l'API (ici uniquement https://editor.swagger.io
). C'est nécessaire pour tester l'API depuis Swagger, à cause d'une sécurité implémentée par les navigateurs.
Le code des classes Article.php
et Error.php
est assez simple, consistant en grande partie de requêtes SQL préparées grâce à la librairie PDO. Ces requêtes préparées permettent d'interroger la BDD en utilisant des paramètres fournis par l'utilisateur (donc dangereux car non validés) sans risque, car ils sont automatiquement échappés.
Nous avons utilisé le gestionnaire de dépendances Composer pour quelques librairies :
Dotenv
pour charger les paramètres depuis un fichier.env
dans les variables d'environnement (ce qui permet pendant le développement en local de créer ce fichier.env
contenant les informations nécessaires à la connexion à la BDD, et lors du déploiement sur Heroku (qui n'autorise pas à créer de fichier manuellement) de passer ces informations via les variables d'environnement directement)PHP_CodeSniffer
pour vérifier la conformité du code aux standards PSR-1 et PSR-2 (à lancer avec la commande./vendor/bin/phpcs --standard=PSR1,PSR2 --extensions=php src/
)
Ce gestionnaire de dépendances est le plus utilisé dans l'environnement PHP, et utilisé de facto par toutes les librairies et frameworks (comme Symfony, Zend, Laravel ou CakePHP).
Nous avons également utilisé le mécanisme d'autoloading de PHP : un code spécifique chargé en début de projet, et qui saura exécuter le bon fichier en fonction des imports effectués. On charge cet autoloader dans le fichier index.php
, ligne 8, soit le plus tôt possible dans le projet. Ensuite, il suffit de faire
use EBM\Routes\Article;
(par exemple dans api.php
) pour importer directement le fichier src/Routes/Article.php
. Ce mécanisme d'autoloader est configuré par défaut pour les librairies installées grâce à Composer ; pour les fichiers de notre projet, il suffit de définir une clé dans le fichier composer.json
, ligne 26, indiquant que le namespace EBM\
correspond au dossier src/
. Attention, à chaque modification de cette configuration, ou après l'installation d'une nouvelle dépendance, il faut regénérer l'autoloader avec la commande composer dump-autoloader
.
Pour simplifier les interactions avec la BDD, une classe Database
a été créée, pour encapsuler la connexion PDO. On y retrouve les méthodes pour effectuer une requête normale (non préparée, donc sans variable utilisateur), une requête préparée, et une fonction utilitaire lastInsertId()
qui renvoie le dernier ID inséré, pour savoir quelle ligne vient d'être créée dans la BDD. Cette classe est inspirée de la formation PHP POO de Grafikart.
Le frontend consiste en une Single-Page Application, développée en Javascript. Cette page permet à un utilisateur d'interagir avec l'API.
Ce projet a été l'occasion de mettre en place une chaîne de compilation Javascript moderne, et de l'appliquer à un projet utilisant jQuery (ou du moins essayer, jQuery ne se prêtant pas vraiment à cette nouvelle manière de travailler).
Javascript
Le frontend a été développé principalement avec ECMAScript 6, nouvelle version du standard définissant le langage Javascript et sortie en 2015. Les principales nouveautés utilisées dans ce projet sont :
- les nouveaux mots-clé
let
etconst
, pour définir les variables de manière plus intuitive (un hoisting au niveau bloc et non au niveau fonction), et pour définir des constantes (plus performantes, et permettant de détecter certains bugs plus tôt lors de la modification de variables supposées constantes) ; - les promesses, permettant de faire de l'asynchrone sans se perdre dans une pyramide de callbacks ;
- les valeurs par défaut pour les paramètres de fonction, ce qui permet de définir une fonction ainsi
function newParagraph (content = '') {
}
- les fonctions fléchées, qui définissent le
this
interne en fonction du contexte d'appel de la fonction, et qui permettent une syntaxe raccourcie - les modules et les exports, qui permettent de découper son code en plusieurs fichiers de façon modulaire, et de n'importer que les fonctions nécessaires
SCSS
Les feuilles de style du projet sont générées grâce à SCSS, un préprocesseur CSS qui ajoute plusieurs fonctionnalités au CSS :
- des variables CSS réutilisables (permettant de définir par exemple une couleur, ou une dimension, et de l'appliquer à plusieurs endroits dans la feuille de styles) ; cette fonctionnalité est désormais standardisée dans CSS3, mais son support dans les navigateurs n'est pas encore complet ;
- un découpage en styles modulables, pour importer uniquement ce qui est utilisé : dans le fichier
article.scss
, nous n'importons que les styles nécessaires depuis le framework Bulma ; - des sélecteurs imbriqués : comme on peut le voir à la ligne 43 du
article.scss
, une syntaxe SCSS permet de définir des sélecteurs plus spécifiques à partir du sélecteur précédent, plus simplement que s'il fallait répéter les sélecteurs
Handlebars
Pour découper aussi le code HTML, nous avons utilisé le langage de templating Handlebars. Ainsi, le fichier index.hbs
inclut les templates des deux composants de notre application, la navbar et le conteneur article.
Aujourd'hui, l'écosystème Javascript s'est énormément complexifié, et nécessite désormais une chaîne d'outils pour compiler et transpiler le code. Nous nous sommes appuyés sur la formation de Grafikart pour configurer les outils.
Webpack
Webpack est la colonne vertébrale du système de build. Il sert à compiler toutes les ressources nécessaires pour un projet frontend, que ce soit des fichiers Javascript, des feuilles de styles CSS, des images, polices... Il fonctionne grâce à des loaders, qui s'appliquent à certains types de fichiers, et peuvent en comprendre la syntaxe et appliquer certaines transformations.
La configuration de Webpack s'effectue dans le fichier webpack.config.js
. Il a été commenté pour expliquer l'utilité de chaque loader et chaque option, mais nous décrirons les principaux outils ci-dessous.
Babel
Babel est un transpilateur : il prend en entrée un fichier Javascript, et va le transformer en un autre fichier Javascript. Il permet ainsi d'utiliser la syntaxe ECMAScript 6 et plus récente, tout en assurant la compatibilité avec les navigateurs plus anciens, en remplaçant par exemple les let
et const
par des var
qui respectent le scope des variables. Il utilise des tables de compatibilité indiquant quelle fonctionnalité est supportée par quelle version de navigateur, et transforme uniquement la syntaxe non comprise par ceux-ci.
Babel est configuré grâce au fichier .babelrc
.
ESLint
Pour s'assurer que les développeurs formattent leur code de la même manière, et pour éviter certaines erreurs détectables statiquement (une variable non déclarée ou non utilisée par exemple), nous vérifions tous les fichiers Javascript grâce à ESLint, en utilisant les règles de StandardJS. Elles ont l'avantage d'être strictes et sans aucune configuration nécessaire.
ESLint est configuré dans le fichier .eslintrc
.
Bulma
Afin d'obtenir une interface propre en peu de temps, nous avont utilisé le framework CSS Bulma. C'est un framework simple basé sur les Flexbox. Il est disponible sous la forme de modules SCSS, ce qui permet de n'importer que les composants nécessaires.
package.json
Le fichier package.json
contient toutes les dépendances nécessaires au développement et à la production. On y retrouve également les scripts permettant de lancer le projet en mode de développement (avec hot-reload et sans compression des ressources) ou en mode production.
Composants
Le code est découpé en deux composants : la navbar, et le conteneur de l'article. Les services permettant d'interagir avec l'API ont été extraits dans le dossier services
.
Chaque composant est découpé en différents fichiers, gérant la structure HTML, le style, et les comportements Javascript. Prenons comme exemple le composant article
.
La structure HTML est décrite dans le fichier article.hbs
.
Le fichier article.selectors.js
export les différents éléments jQuery fixes disponibles dans le composant. Cela évite de sélectionner plusieurs fois le même élément, et de perdre en performance (tout en permettant de changer à un seul endroit un sélecteur si la structure HTML change).
Le fichier article.utils.js
contient les différentes fonctions qui permettent de manipuler la page, ou de réagir à des événements.
Le fichier article.customEvents.js
permet de définir des événements spéciaux, qui seront exportés pour permettre aux autres composants de déclencher des actions dans le composant article. Cela permet un couplage faible entre les composants, par exemple la navbar va déclencher l'événement SET_ARTICLE
, qui sera intercepté par le conteneur d'article. Pas besoin de cibler une fonction particulière du composant, ou un élément.
Le fichier article.js
contient les définitions des événements affectés aux éléments du composant. C'est lui qui est importé par le JS principal index.js
.
Enfin, le fichier article.scss
contient les styles nécessaires à ce composant. Il importe les éléments depuis Bulma, et ajoute des propriétés pour certains sélecteurs. Il est importé depuis le SCSS principal main.scss
.
Le composant navbar est architecturé de la même manière.
On peut remarquer que jQuery est importé dans chaque fichier JS. Au moment de la compilation Webpack se chargera de n'inclure qu'une fois chaque dépendance, il n'y a donc pas de problème de taille du bundle. Pour les dépendances qui le supportent, il serait possible de n'importer que les fonctions nécessaires, et Webpack pourrait alors faire du tree-shaking et retirer du bundle le code inutilisé.
Services
Les services permettent de se connecter à l'API pour effectuer les opérations sur les ressources. Chaque fonction est simplement un wrapper autour de l'appel Ajax fourni par jQuery. Ce wrapper renvoie une promesse ; ce n'est pas strictement nécessaire puisque jQuery.ajax() peut être utilisé avec une syntaxe proche des promesses, mais cela permettrait de changer de librairie de requête sans modifier le reste du code (passer à l'API native fetch()
par exemple).
Heroku est un hébergeur proposant du PaaS (Platform as a Service). Il est possible d'y déployer des applications, dans différents langages supportés : JS, PHP, Ruby, Go, Python... Nous utilisons ici l'offre gratuite, parfaitement adaptée à un Proof Of Concept. Le déploiement a été configuré pour un environnement NodeJS et PHP, ce qui permet à Heroku d'installer automatiquement les dépendances Composer et NPM. Ce déploiement est intégré à git, puisqu'il suffit de pousser sur la branche spéciale ajoutée lors de la création du projet sur Heroku (git push heroku master
), et Heroku automatise le reste.
- Utiliser des regex pour les routes, probablement plus maintenable que d'exploser la route dans un tableau et de vérifier à chaque étape si l'ID est un nombre
- Créer des méthodes pour rendre le routeur plus propre. Par exemple, ne pas avoir une cascade de
switch/case
qui vérifient les méthodes, mais créer une classeRouter
qui a des méthodesget()
,post()
,put()
etdelete()
, et qui vérifie quelle route match - Utiliser un micro-framework comme Zend Expressive (vidéo de présentation en français ici), pour éviter de recoder ces éléments très classiques
- Utiliser un framework pour le frontend. jQuery peut être utile pour des besoins simples (et encore, avec les avancées des navigateurs, et le travail de standardisation du W3C et de l'ECMAScript, beaucoup des choses qui nécessitaient autrefois jQuery peuvent être faites nativement (cf http://youmightnotneedjquery.com/), évitant ainsi de charger 90ko de JS tout en ayant de meilleurs performances => Ajax avec
fetch()
, sélection avecdocument.getElementById()/document.querySelector()
), mais dès qu'une application devient un peu complexe le code ressemble à un plat de spaghetti, avec des événements bindés depuis un peu partout, et une impossibilité de séparer les fichiers proprement - Rédiger plus de PHPdoc et de JSdoc, pour aider le développeur à comprendre l'utilité des fonctions, et aider les IDE à proposer les bons types et les bonnes complétions