Il s'agit d'une version statique de l'intranet d'A. Jonquet, certaines fonctionnalités sont donc potentiellement non fonctionnelles.
Rejoindre la version dynamique 🔒
R401
Navigation

Ajout de l'évaluation des bookmarks

Vous allez à présent mettre en place les ressources et actions permettant aux utilisateurs d'évaluer les bookmarks.

Pour commencer vous allez créer une table permettant de stocker les notes données par les utilisateurs. Vous gérerez ensuite de manière appropriée la sécurité et la validation des actions disponibles sur les notes. Vous en profiterez pour affecter dynamiquement des valeurs par défaut aux propriétés soumises par l'utilisateur. Et finalement, vous ajouterez et maintiendrez à jour une propriété rateAverage d'un bookmark afin de ne pas avoir à re-calculer la moyenne lors de consultation des données.

Remarque importante

Les notes des TP seront obtenues à partir de vos dépôts Git, vous devrez donc prendre grand soin de la qualité de ces dépôts et de leurs « commits ».

De manière similaire, les descriptions de vos « commits » devront être claires et informatives, afin de permettre l'évaluation de la progression de votre travail.

Objectifs de la séance

  • Gestion de relations entre les tables
  • Validation des données soumises
  • Gestion des événements d'interaction avec la base de données

Création d'une table Rating

Afin de stocker les informations relatives à l'évaluation d'un bookmark par un utilisateur, vous allez créer une nouvelle table dans la base de données : Rating.

Comme précédemment, vous commencerez par utiliser le maker de Symfony, pour créer une entité :

bin/console make:entity

Vous l'appellerez Rating et en ferez une ressource API Platform. Vous ajouterez ensuite les propriétés suivantes :

  • bookmark une relation vers l'entité Bookmark. N'hésitez pas à utiliser l'assistant en saisissant le type relation comme type de propriété.

    La relation est de type ManyToOne, ne pouvant pas être null. Vous ajouterez un attribut ratings dans l'entité Bookmark et vous ajouterez la suppression des orphelins de la table Bookmark à votre entité.

  • user une autre relation ManyToOne vers l'entité User, ne pouvant pas être null. Vous ajouterez un attribut ratings dans l'entité User ainsi que la suppression des orphelins de la table User à votre entité.
  • value, la valeur de la note, de type smallint et ne pouvant pas être null.

Il ne vous reste plus qu'à mettre à jour votre schéma de base de données. Vous commencerez par contrôler la validité de la commande SQL produite :

bin/console doctrine:schema:update --complete --dump-sql

Puis, vous réaliserez la mise à jour de la base de données à travers l'utilisation d'une migration :

bin/console make:migration
suivie de :
bin/console doctrine:migrations:migrate

La table devrait maintenant être créée dans la base, vous pouvez le vérifier à l'aide de phpMyAdmin ou en essayant de réaliser une requête sur la table :

bin/console dbal:run-sql "SELECT * FROM rating"

Vous devriez obtenir ce résultat :

 [OK] The query yielded an empty result set.

Vous relancerez vos jeux de tests pour constater que les tests de la classe BookmarkGetCest.php ne sont plus valides, en effet, la propriété ratings doit être ajoutée à la liste des propriétés attendues. Vous corrigerez le problème pour que l'ensemble des tests soit valide.

Vous constaterez dans la documentation de l'API que les exemples des propriétés bookmark et user sont des URL étrange, vous prendrez soin de les corriger à l'aide de l'attribut ApiProperty.

Travail réalisé dans cette partie
  • Création de l'entité Rating.
  • Création de la table Rating dans la base de données.
  • Correction des exemples de relation dans la documentation.
  • Correction des tests.

Validation des données

Évidement, nous ne souhaitons pas qu'un utilisateur puisse évaluer plusieur fois un même bookmark, vous allez donc ajouter une contrainte d'unicité de l'évaluation à l'aide des contraintes de validation de Symfony.

Vous commencerez par créer une nouvelle classe de tests : RatingPostDataValidationCest contenant le test ratingUnicityTest correspondant au scenario suivant :

  • Création d'un signet et d'un utilisateur.
  • Authentification de l'utilisateur.
  • Création d'une évaluation pour le signet et l'utilisateur.
  • Contrôle de la création de l'évaluation.
  • Création d'une nouvelle évaluation pour le même signet et le même utilisateur.
  • Contrôle du refus de création de l'évaluation.

Pour créer la classe de tests, vous pourrez utiliser le generator de Codeception :

    php vendor/bin/codecept generate:cest Api Rating\\RatingPostDataValidationCest

Afin d'éviter les doublons, vous auriez pu utiliser une clé primaire composée de l'entité, mais pour garder le code simple, vous utiliserez une contrainte d'unicité sur le couple user/bookmark afin d'interdire les doublons.

Une fois que le test précédent est valide, vous allez ajouter une contrainte sur la valeur de la note afin de vous assurer que la valeur de la note soit compris entre 0 et 10 inclus. Vous ajouterez un nouveau test valueValidation à la classe RatingPostDataValidationCest permettant de valider ces contrôls.

Travail réalisé dans cette partie
  • Ajout d'une contrainte d'unicité sur le couple user/bookmark.
  • Ajout d'une contrainte d'intervalle sur value.
  • Ajout de tests de validation des contrôles.

Génération de données

Afin que votre API soit plus agréable à explorer, vous allez remplir votre table de score avec des données que vous allez générer.

Vous commencerez par créer une nouvelle forge RatingFactory qui donnera une note aléatoire entre 1 et 10 à value et une nouvelle instance de BookmarkFactory et UserFactory pour, respectivement, bookmark et user, conformément à la documentation de Foundry.

bin/console make:factory

Vous pourrez ensuite créer une nouvelle classe PHP de génération de données : RatingFixtures.

bin/console make:fixtures

La génération des notes suppose que vous ayez des utilisateurs et des bookmarks dans la base de données, vous devez donc vous assurer que les fixtures pour les utilisateurs et les bookmarks soient exécutées avant les notes. Vous préciserez donc des dépendances pour ces deux fixtures.

Ensuite, vous compléterez la méthode load pour produire des données dans votre table. Pour chaque utilisateur de la base de données, que vous pourrez récupérer à l'aide du Proxy Repository correspondant, vous créerez de 3 à 7 notes pour des bookmarks choisis aléatoirement (voir ModelFactory::randomRange() dans le lien de documentation précédent).

Les données sont générées à l'aide du script Composer db :

composer db

Encore une fois, vous pouvez vérifier que tout fonctionne correctement en réalisant une requête :

bin/console dbal:run-sql "SELECT user_id, COUNT(*) AS ratings FROM rating GROUP BY user_id"

Vous devriez obtenir un résultat similaire à :

 --------- --------- 
  user_id   ratings  
 --------- --------- 
  1         6        
  2         7        
  3         3        
  4         7        
  5         3        
  6         6        
  7         7        
  8         4        
  9         6        
  10        3        
  11        3        
  12        6        
  13        3        
  14        4        
  15        5        
  16        5        
  17        3        
  18        5        
  19        3        
  20        4        
  21        7        
  22        3        
  23        4        
 --------- --------- 

N'hésitez cependant pas à regarder le contenu de vos tables à l'aide de phpMyAdmin afin de vérifier précisément la qualité des données générées.

Travail réalisé dans cette partie
  • Création d'un script de génération d'entité Rating : RatingFactory.
  • Création d'un script de génération de données : RatingFixtures.
  • Génération automatique de contenu pour la table Rating.

Accès aux ressources

Vous allez contrôller le bon fonctionnement de votre API en ajoutant une classe de tests RatingGetCest contenant les tests suivants :

  • Un utilisateur anonyme peut accéder à la liste des notes et les valeurs de la réponse correspondent.
  • Un utilisateur anonyme peut accéder au détail d'une note et les valeurs de la réponse correspondent.
Remarque importante

API Platform retient la première route en GET comme étant celle de l'identifiant d'une ressource. Veillez donc à l'ordre de vos opérations dans User pour que vos relations soient cohérentes.

Travail réalisé dans cette partie
  • Mise en place de la classe de tests « tests/Api/Rating/RatingGetCest.php ».

Création d'une note

En vous inspirant des tests précédents, vous ajouterez une classe de tests « RatingCreateCest », qui contiendra les tests suivants :

  • Un utilisateur anonyme ne peut pas créer une note, le code HTTP de la réponse doit être 401, vous devrez probablement restreindre les règles du pare-feu de Symfony.
  • Un utilisateur authentifié peut créer une note et les valeurs de la réponse correspondent.

Vous contrôlerez que votre code valide les tests en apportant les modifications nécessaires, notamment en ajoutant des contraintes de sécurité sur les actions.

Travail réalisé dans cette partie
  • Mise en place de la classe de tests « tests/Api/Rating/RatingCreateCest.php ».
  • Ajout du test d'accès à l'action de création d'une note.
  • Ajout du test de création d'une note.
  • Ajout des tests de validité des données.

Modification d'une note

Vous ajouterez une classe de tests : RatingPatchCest, qui contiendra les tests suivants :

  • Un utilisateur anonyme ne peut pas modifier une note, le code HTTP de la réponse doit être 401.
  • Un utilisateur authentifié peut modifier une note lui appartenant et les valeurs de la réponse correspondent.
  • Un utilisateur authentifié ne peut pas modifier une note d'un autre utilisateur, le code HTTP de la réponse doit être 403.

Vous contrôlerez que votre code valide les tests en apportant les modifications nécessaires, notamment en ajoutant des contraintes de sécurité sur les actions.

Travail réalisé dans cette partie
  • Mise en place de la classe de tests « tests/Api/Rating/RatingPatchCest.php ».
  • Ajout des tests d'accès à la modification d'une note, en PATCH.
  • Ajout des tests de modification d'une note, en PATCH.

Suppression d'une note

Vous ajouterez une classe de tests : RatingDeleteCest, qui contiendra les tests suivants :

  • Un utilisateur anonyme ne peut pas supprimer une note, le code HTTP de la réponse doit être 401.
  • Un utilisateur authentifié peut supprimer une note lui appartenant, le code HTTP de la réponse doit être 204.
  • Un utilisateur authentifié ne peut pas supprimer une note d'un autre utilisateur, le code HTTP de la réponse doit être 403.

Vous contrôlerez que les tests valident en apportant les modifications nécessaires, notamment en ajoutant des contraintes de sécurité sur les actions.

Travail réalisé dans cette partie
  • Mise en place de la classe de tests « tests/Api/Rating/RatingDeleteCest.php ».
  • Ajout des tests d'accès à la suppression d'une note.
  • Ajout des tests de suppression d'une note.

Notion de sécurité, le cloisonnement des données

Pour l'instant, votre API comporte une faille de sécurité courante : un utilisateur authentifié peut accéder aux notes d'un autre utilisateur puisque vous ne contrôlez pour l'instant que l'authentification. Le processus de restriction d'accès d'un utilisateur à ses propres ressources s'appelle le cloisonnement.

Vous allez donc détecter et interdire l'accès aux notes qui ne sont pas celles de l'utilisateur connecté.

Vous commencerez par ajouter un test à la classe « RatingCreateCest », vérifiant qu'un utilisateur connecté ne peut pas créer une note pour un autre utilisateur, le code HTTP de la réponse doit être 422.

Pour l'instant le test doit échouer prouvant ainsi la présence de la faille.

Nous ne pouvans pas utiliser la stratégie précédente, car ce cas n'est pas tout à fait le même que pour les actions PATCH et DELETE. Déjà, d'un point de vue concret, nous ne disposons pas de l'objet sur lequel faire le contrôle dans la propriété security. Mais en plus conceptuellement, la situation est différente, dans le cas de PATCH ou DELETE, l'utilisateur ne peut pas accéder à une note qui ne lui appartient pas, alors que dans ce cas-là, l'utilisateur à le droit de créer une note, mais cette note ne peut pas concerner un autre utilisateur, la contrainte est donc sur la note à créer. Dans cet objectif, vous allez créer une contrainte de validation spécifique à ce cas.

Vous allez donc créer une nouvelle contrainte de validation permettant de contrôler que la propriété user de la note correspond à l'utilisateur authentifié.

Comme d'habitude, vous utiliserez le « maker » de Symfony pour générer les classes adéquates :

bin/console make:validator

En appelant votre contrainte IsAuthenticatedUser, vous devriez obtenir deux classes :

  • « Validator/IsAuthenticatedUser.php » définit la contrainte comme une annotation (annotation @annotation sur la classe) et comme un attribut PHP 8 (attribut #[Attribute] sur la classe). Vous pouvez personnaliser le message d'erreur qui est la seule propriétié de cette classe héritant de « Constraint ». Vous pouvez modifier la classe pour en faire un attribut PHP 8 uniquement en supprimant l'annotation @annotation.
  • « Validator/IsAuthenticatedUserValidator.php » définit le validateur qui contrôle si la contrainte est respectée ou non. Vous allez devoir injecter le service de sécurité (Symfony\Component\Security\Core\Security) dans le constructeur et le mémoriser dans une propriété d'instance, afin de pouvoir comparer la valeur testée ($value) avec l'utilisateur authentifié.

Vous pourrez ensuite ajouter cette contrainte comme attribut de la propriété user de l'entité Rating afin de valider votre test.

Travail réalisé dans cette partie
  • Mise en place d'un test du cloisonnement de la création des notes.
  • Ajout de la contrainte IsAuthenticatedUser.
  • Ajout du validateur IsAuthenticatedUserValidator.
  • Ajout de la contrainte sous forme d'un attribut dans Rating.

Amélioration de la DX (« Developer eXperience »)

Lors des actions sur les notes, votre API attend que l'utilisateur concerné lui soit fourni, tout en imposant que celui-ci soit le même que celui authentifié. Ces informations sont redondantes, vous allez alléger le travail du développeur en lui permettant d'omettre cette propriété.

Pour parvenir à ce résultat, plusieurs solutions sont envisageables. Cependant, l'utilisation du validateur sur la propriété user de l'entité Rating impose que l'utilisateur connecté soit inséré avant la validation, une solution consiste à l'injecter lors de la dénormalisation.

Comme précédemment, vous créerez une classe RatingDenormalizer implémentant les interfaces DenormalizerInterface et DenormalizerAwareInterface et utilisant le trait DenormalizerAwareTrait.

La méthode denormalize injectera l'utilisateur connecté dans la propriété user des données transmises en paramètre.

Vous ajouterez un test à la classe RatingCreateCest, vérifiant qu'un utilisateur connecté peut créer une note pour lui-même sans fournir la propriété user.

Travail réalisé dans cette partie
  • Ajout d'un dénormalisateur pour l'entité Rating.
  • Mise en place d'un test de la fonctionnalité.

Gestion des sous-ressources

Vous allez ajouter une nouvelle route permettant d'obtenir la liste des notes d'un utilisateur. Il s'agit d'une route particulière, car la collection des notes d'un utilisateur est une sous-ressource de l'entité User.

Comme la ressource produite est une collection de Rating, vous allez ajouter une nouvelle opération GetCollection dans la classe Rating.

Vous ajouterez à cette opération deux paramètres :

  • uriTemplate avec comme valeur '/users/{id}/ratings',
  • et uriVariables, qui en vous basant sur la documentation d'API Platform, décrira id comme étant l'identifiant d'une instance de l'entité User et permettant le lien vers la propriété ratings de cette entité.

Vous ajouterez deux nouveaux tests dans la classe RatingGetByUserCest, qui contiendra les tests suivants :

  • Un utilisateur anonyme ne peut pas accéder à la liste des notes d'un utilisateur, le code HTTP de la réponse doit être 401.
  • Un utilisateur authentifié peut accéder à la liste des notes d'un autre utilisateur et il obtient le bon nombre de notes.

Et vous vous assurez que votre code est valide.

Travail réalisé dans cette partie
  • Ajout de la route '/users/{id}/ratings' permettant d'obtenir la liste des notes d'un utilisateur dans l'entité Rating.
  • Configuration de la résolution de la sous-ressource.
  • Ajout des tests correspondants.

Ressource nichée

En testant la route '/users/{id}/ratings' de votre API sur sa page de documentation, vous constaterez que vous obtenez bien la liste des notes d'un utilisateur. Mais pour connaitre le signet de chaque note, vous devez réaliser une nouvelle requête, ce qui devient vite fastidieux lors de la conception d'une application consommant votre API.

Une solution serait de pouvoir ajouter les informations du signet correspondant pour chaque note, c'est ce que l'on appelle une ressource nichée.

Dans notre cas, vous commencerez par créer un groupe pour la normalisation Rating_read de l'entité Rating et vous vous assurerez que vos tests sont toujours valides.

Ensuite, dans l'opération permettant de lister les notes d'un utilisateur, vous ajouterez un groupe de normalisation User-rating_read, que vous affecterez au propriétés id et name de l'entité Bookmark.

Vous pourrez constater sur la page de documentation de votre API que le nom et l'identifiant du signet sont bien présent pour les notes d'un utilisateur. Vous vous assurerez que vos tests sont toujours valides.

Travail réalisé dans cette partie
  • Ajout de groupes de normalisation pour l'entité Rating.
  • Affectation du groupe de normalisation approprié pour les propriétés souhaitées de l'entité Bookmark.
A. Jonquet DUT-INFO/REIMS