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.
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.
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:migrationsuivie 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
.
Rating
.
Rating
dans la base de 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 :
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.
user
/bookmark
.
value
.
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
.
bin/console make:factory
Cette forge donnera une note aléatoire entre 1 et 10 à value
et conformément à la documentation de Foundry sur les relations, bookmark
et user
recevront respectivement une affectation paresseuse d'une nouvelle instance de BookmarkFactory
et UserFactory
.
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.
Rating
: RatingFactory
.
RatingFixtures
.
Rating
.
Vous allez contrôller le bon fonctionnement de votre API en ajoutant une classe de tests RatingGetCest
contenant les tests suivants :
Vous ferez en sorte d'apporter les modifications nécessaires au bon fonctionnement de ces tests.
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.
En vous inspirant des tests précédents, vous ajouterez une classe de tests « RatingCreateCest », qui contiendra les tests suivants :
401
, vous devrez probablement restreindre les règles du pare-feu de Symfony.
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.
Vous ajouterez une classe de tests : RatingPatchCest
, qui contiendra les tests suivants :
401
.
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.
Vous ajouterez une classe de tests : RatingDeleteCest
, qui contiendra les tests suivants :
401
.
204
.
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.
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 :
@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
.
Symfony\Bundle\SecurityBundle\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.
IsAuthenticatedUser
.
IsAuthenticatedUserValidator
.
Rating
.
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 si celui-ci n'est pas défini. Vous obtiendrez l'utilisateur connecté à l'aide du service Symfony\Bundle\SecurityBundle\Security
. cet utilisateur est transmis à API Platform sous forme d'un IRI que vous pourrez obtenir à l'aide du service ApiPlatform\Metadata\IriConverterInterface
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
.
Rating
.
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'
,
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
, deffinissant les tests suivants :
401
.
Et vous vous assurez que votre code est valide.
'/users/{id}/ratings'
permettant d'obtenir la liste des notes d'un utilisateur dans l'entité Rating
.
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.
Rating
.
Bookmark
.