Le projet est disponible Ă cette adresse:
- Compte rendu
- Mise en place d'un repository sur Gitlab
- Mise en place de Bootstrap et jQuery
- Mise en place d'une base de données MySQL contenant les articles et les paragraphes
- Mise en place d'une API REST permettant de gérer les articles et leurs paragraphes
- Mise en place d'un front-office fournissant une interface Ă l'utilisateur et requĂŞtant l'API
Au niveau du Back-End, les choix se sont portés sur:
- Apache comme serveur web
- Php 7 afin d'apporter un typage, ermettant une meilleure robustesse du code
- apidoc pour générer une documentation de l'api
- MariaDB/MySQL comme SGBD
Au niveau du Front-End :
- Bootstrap comme librairie pour la présentation
- jQuery pour l'interaction
Nous avons décidé de nous diriger vers l'utilisation d'une API Restful afin d'une part, de découpler le développement du Back-End et celui du Front-End.
Ainsi, cela nous a permis de travailler dans deux dossiers complétements séparés, nous permettant d'éviter les régression de l'un lorsque l'on modifie la structure de l'autre.
Nous avons ainsi choisi de de réaliser notre Back-End en PHP 7, en utilisant le typage des arguments de fonctions, ainsi que le typage de retour de fonction, afin d'augmenter la robustesse du code produit.
Concernant le Front-End, nous utilisont la librairie jQuery pour réaliser les interactions avec l'utilisateur, ainsi que la librairie Bootstrap pour la présentation.
Le serveur sur lequel notre projet s'execute est apache, avec une configuration que nous détaillerons ultérieurement.
Nous avons décidé d'écrire notre propre Framework afin de réaliser notre API. En effet, cela nous permettait d'avoir une bone compréhension de son fonctionement, tout en étant léger car adapté à nos besoins.
Ainsi le routage s'effectue à l'aide de regex, qui nous permet de facilement faire évoluer nos routes, mais également de facilement récupérer les paramétres éventuels nécessaire au bon fonctionnement de l'API, par exemple, dans le cas de la route:
/api/v1/articles/5/paragraphs/1
Nous voulons récupérer le 5
associé à un ID d'article, ainsi que le 1
, associé à un ID de paragraphe.
Cela est, grâce à l'utilisation de regex, facilement réalisable, par exemple en utilisant cette regex :
~^/articles/(\d+)/paragraphs/(\d+)/?$~
Ainsi, le Framework se compose de 3 classes :
- Route.php
- Router.php
- RouterUtils.php
La classe Route est un objet qui contient:
- L'expression régulière qui doit correspondre
- La méthode HTTP qui doit correspondre
- La fonction de callback utilisé lorsque la Route est apelée
La classe Router.php se charge de lister l'ensemble des routes de l'application. Elle est également appelée lors d'une nouvelle requête afin d'essayer de trouver une route qui correspond à la requête.
Cette classe permet de réaliser divers traitements sur la requete:
- Récupérer la partie intéressante de la route (retirer la partie
/api/v1
) - Récupérer leBody de la requête
- Executer le callback de la route
- Emettre la réponse au client
Une base de donnée MySQL ou MariaDB est nécessaire pour faire fonctionnet le projet. Afin d'initier le contenu, un utilisateur ainsi qu'une base sont nécessaire.
Ensuite, il suffit d'exécuter la commande suivante pour intialiser les tables:
source {/path/to/the/project}/Back/src/script.sql
Le module rewrite est nécessaire :
a2enmod rewrite
Ajouter :
AllowOverride All
Afin d'activer le .htaccess
Renommer Back/src/config.example.php en config.php
Remplir les champs:
- $host correspond à l'adresse où la base de donnée se situe (ex: localhost)
- $dbname est le nom donné à la base préalablement créé
- $username et $password informations de connexion Ă la base
La manière dont sont transmis les paramètres s'effectue de la manière suivante:
-
La classe Router récupére les paramètres issues des parenthèses capturantes de l'expression régulière.
-
Ils sont transmis dans un tableau et dans l'ordre d'apparition au sein de l'expression régulière.
-
La fonction
match()
renvoie ainsi un tableau contenant comme premier argument le callback de la route, et comme deuxième argument, un tableau des identifiants récupérés par la regex. -
Au sein de
executeRoute()
de RouterUtils, le tableau[callback, params]
et les données du Body sont transformé pour donner un tableau associatif contenant les paramètres et les données du Body.
En résumé, le callback reçoit un unique paramètre qui est un tableau associatif et qui contient;
- Dans
URL_PARAMS
, un tableau contenant les paramètres récupérés dans l'url de la requete- Dans
BODY_DATA
, un tableau associatif correspondant au json envoyé dans la requête
Le callback se charge ainsi simplement d'extraire les valeurs des paramètres et des données du Body, et au besoin, d'affecter des valeurs par défaut dans le cas d'arguments facultatifs.
- Le Callback extrait les valeurs des paramètres des données du Body, et assigne des valeurs par défaut si besoin.
- Le controlleur, appelé par le callback, vérifie la validité des arguments extraits par le callback.
- Le modèle, appelé par le controlleur, construit la requête SQL adapté, avec la éventuels paramètres.
- DBAccess, appelé par le modèle, se charge d'executer la requête.
- Le controlleur reçoit la réponse de DBAccess, vérifie sa validité, et transforme le résultat en json.
- Le callback apelle la fonction response() de RouterUtils avec comme paramètre le résultat du contrôlleur pour l'émmettre au client.
Vue la nature du travail demandé, nous avons choisi pour le Front-End de faire une Single Page Application. Le Front-End ne comprend donc qu'une seule page qui affiche dynamiquement les informations demandées.
À l'arrivée sur la page, l'utilisateur ne voit qu'un message de présentation et la barre de navigation (qui sera toujours présente), depuis laquelle il peut :
- RĂ©afficher ce "message de garde" en cliquant sur le titre de la page
- Voir la liste des articles présents dans la base de données depuis le menu déroulant
- Ajouter un article dans le formulaire prévu à cet effet
C'est l'affichage par défaut lorsqu'on sélectionne un article depuis le menu déroulant. Il affiche simplement le titre et les paragraphes concernant l'article. Il y a également un bouton pour passer en "mode édition".
C'est dans ce mode, moins adapté à la lecture, qu'on peut modifier les données concernant l'article en question. C'est aussi l'affichage par défaut de l'article après l'avoir créé.
- Il y a deux boutons, l'un pour revenir en mode vue, l'autre pour supprimer totalement l'article.
- On peut cliquer sur le titre ou sur un paragraphe pour en modifier le contenu. Il faut valider les changements en appuyant sur ENTRÉE, ou les annuler en appuyant sur ÉCHAP.
- On peut cliquer sur le bouton
+
à la fin d'un paragraphe pour ajouter un nouveau paragraphe juste après. Le fonctionnement est similaire à l'édition. - On peut cliquer sur le bouton
X
qui apparaît au survol d'un paragraphe pour le supprimer. - On peut glisser-déposer les paragraphes pour les réarranger.
- Mettre en place le HTTPS
- Mettre en place de l'Ă©criture collaborative : voir les gens en train de modifier ou de lire un article
- Intégrer des sous-titres (en tant que paragraphe spécial) à l'article
- SĂ©curiser l'API
Nous avons dans l'ensemble apprécié le module, nous donnant un aperçu des technologies front/back et du type de structure auquel on peut être amené à être confronté.
Le gros plus a été le fait de faire participer les élèves autant que possible, en leur laissant le temps de réfléchir aux petits exercices ou via le biais de ce devoir final. On apprend en codant !
On pourra cependant peut-être regretter la présentation de certaines technologies ayant mal vieilli ou de certaines pratiques n'étant plus vraiment d'actualité.
Une version web de l'apidoc est disponible Ă cette adresse:
POST /api/v1/articles
Name | Type | Description |
---|---|---|
TITLE | String | The Title of article to create |
Success-Response:
HTTP/1.1 201 OK
{
"ID": 1,
"TITLE" : "Lorem Ipsum"
}
Name | Type | Description |
---|---|---|
ID | Number | Id of the article created |
TITLE | String | Title of the article created |
GET /api/v1/articles
Success-Response:
HTTP/1.1 200 OK
[
{
"ID": 1,
"TITLE" : "Lorem Ipsum"
},
{
"ID": 2,
"TITLE" : "The game"
},
...
]
Name | Type | Description |
---|---|---|
ID | Number | Id of the article |
TITLE | String | Title of the article |
GET /api/v1/articles?paragraphs=true
Success-Response:
HTTP/1.1 200 OK
[
{
"ID": 1,
"TITLE" : "Lorem Ipsum",
"CONTENT": [
{
"ID": 1,
"CONTENT": "Lorem ipsum dolor sit amet.",
"POSITION": 1,
"ARTICLE_ID": 1
},
{
"ID": 2,
"CONTENT": "Ut enim ad minim veniam.",
"POSITION": 2,
"ARTICLE_ID": 1
},
...
},
{
"ID": 2,
"TITLE" : "The game",
"CONTENT" : [
{
"ID": 3,
"CONTENT": "Perdu !",
"POSITION": 1,
"ARTICLE_ID": 2
},
...
},
...
]
Name | Type | Description |
---|---|---|
ID | Number | Id of the article |
TITLE | String | Title of the article |
CONTENT | Object[] | List of paragraphs |
CONTENT.ID | Number | Id of the article patched |
CONTENT.TITLE | String | Title of the article patched |
CONTENT.POSITION | Number | The position of the paragraph in the article |
CONTENT.ARTICLE_ID | Number | The Id of the article associated to the paragraph |
GET /api/v1/paragraphs/:id
Success-Response:
HTTP/1.1 200 OK
{
"ID": 1,
"CONTENT": "Lorem ipsum dolor sit amet.",
"POSITION": 1,
"ARTICLE_ID": 1
}
Name | Type | Description |
---|---|---|
ID | Number | Id of the paragraph |
CONTENT | String | Content of the article |
POSITION | Number | The position of the paragraph in the article |
ARTICLE_ID | Number | The Id of the article associated to the paragraphe |
DELETE /api/v1/articles/:id
201 Success-Response:
HTTP/1.1 201 OK
{
"Response" : "Successfully deleted article with ID <code>ID</code>",
}
Name | Type | Description |
---|---|---|
ID | Number | Id of the deleted article |
Name | Type | Description |
---|---|---|
ArticleNotFound | No article with the ID |
Error-Response:
HTTP/1.1 404 Not Found
{
"Error": "No article with the ID <code>ID</code> found"
}
GET /api/v1/articles/:id
Success-Response:
HTTP/1.1 201 OK
{
"ID": 1,
"TITLE" : "Lorem Ipsum"
}
Name | Type | Description |
---|---|---|
ID | Number | Id of the article |
TITLE | String | Title of the article |
Name | Type | Description |
---|---|---|
ArticleNotFound | No article with the ID |
PATCH /api/v1/articles/:id
Name | Type | Description |
---|---|---|
TITLE | String | The Title of article to patch |
Success-Response:
HTTP/1.1 201 OK
{
"ID": 1,
"TITLE" : "Lorem Ipsum"
}
Name | Type | Description |
---|---|---|
ID | Number | Id of the article patched |
TITLE | String | Title of the article patched |
POST /api/v1/articles/:idA/paragraphs
Name | Type | Description |
---|---|---|
CONTENT | String | Content of the paragraph patched |
POSITION | Number | Optional position of the paragraph in the article (if empty, the paragraph is added at the end of the article) |
ARTICLE_ID | Number | Id of the article associated to the paragraph |
Success-Response:
HTTP/1.1 201 OK
[
{
"ID": 2,
"CONTENT": "Ut enim ad minim veniam.",
"POSITION": 2,
"ARTICLE_ID": 1
},
...
]
Name | Type | Description |
---|---|---|
ID | Number | Id of the article |
TITLE | String | Title of the article |
POSITION | Number | The position of the paragraph in the article |
ARTICLE_ID | Number | The Id of the article associated to the paragraph |
DELETE /api/v1/paragraphs/:id
201 Success-Response:
HTTP/1.1 201 OK
{
"Response" : "Successfully deleted paragraph with ID <code>ID</code>",
}
Name | Type | Description |
---|---|---|
ID | Number | Id of the deleted paragraph |
Name | Type | Description |
---|---|---|
ArticleNotFound | No paragraph with the ID |
Error-Response:
HTTP/1.1 404 Not Found
{
"Error": "No paragraph with the ID <code>ID</code> found"
}
GET /api/v1/articles/:idA/paragraphs/:pos
Success-Response:
HTTP/1.1 201 OK
[
{
"ID": 2,
"CONTENT": "Ut enim ad minim veniam.",
"POSITION": 2,
"ARTICLE_ID": 1
},
...
]
Name | Type | Description |
---|---|---|
ID | Number | Id of the article patched |
TITLE | String | Title of the article patched |
POSITION | Number | The position of the paragraph in the article |
ARTICLE_ID | Number | The Id of the article associated to the paragraph |
PATCH /api/v1/paragraphs/:id
Name | Type | Description |
---|---|---|
ID | Number | Optional Id of the paragraph patched |
CONTENT | String | Optional Content of the paragraph patched |
POSITION | Number | Optional position of the paragraph in the article |
ARTICLE_ID | Number | Optional Id of the article associated to the paragraph |
Success-Response:
HTTP/1.1 201 OK
{
"ID": 2,
"CONTENT": "Ut enim ad minim veniam.",
"POSITION": 2,
"ARTICLE_ID": 1
}
Name | Type | Description |
---|---|---|
ID | Number | Id of the article patched |
TITLE | String | Title of the article patched |
POSITION | Number | The position of the paragraph in the article |
ARTICLE_ID | Number | The Id of the article associated to the paragraph |
GET /api/v1/articles/:idA/paragraphs
Success-Response:
HTTP/1.1 201 OK
[
{
"ID": 2,
"CONTENT": "Ut enim ad minim veniam.",
"POSITION": 2,
"ARTICLE_ID": 1
},
...
]
Name | Type | Description |
---|---|---|
ID | Number | Id of the paragraph |
CONTENT | String | Content of the paragraph |
POSITION | Number | The position of the paragraph in the article |
ARTICLE_ID | Number | The Id of the article associated to the paragraph |