Pour finir, vous allez configurer les opérations sur les bookmarks.
Vous commencerez par ajouter une moyenne des notes d'un bookmark, que vous maintiendrez à jour lors des intéraction avec les notes du signet. Vous en profiterez pour ajouter une opération permettant d'obtenir la liste des notes d'un bookmark.
Et enfin, vous ajouterez un propriétaire aux bookmarks et vous en tiendrez compte lors de leur affichage.
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.
Vous allez ajouter les groupes de sérialisation permettant de sélectionner les propriétés accessibles par les différentes opérations de l'entité Bookmark.
Vous allez gérer trois groupes aux propriétés :
Bookmark_read pour l'accès aux propriétés générales id, name, public et url,
Bookmark_detail pour l'accès aux propriétés description, creationDate et ratings, pour le détail d'un signet (GET, POST et PATCH)
Bookmark_write pour l'écriture des propriétés name, description, public et url.
Vous affecterez ces groupes aux propriétés de l'entité Bookmark en conséquence et vous corrigerez les tests de l'entité Bookmark pour qu'ils soient à nouveau valides, notamment l'accès à la collection qui ne retourne plus les propriétés description et creationDate.
Vous en profiterez pour spécifier les opérations supportées : GET pour une entité ou la collection d'entité, POST, PATCH et DELETE.
Seules les opérations GET d'une entité, POST et PATCH auront accès au détail d'un signet.
Les bonnes pratiques déconseillent l'usage des triggers de base de données, car ils vont à l'encontre de l'usage de Doctrine, qui permet de décorréler le projet du moteur de base de données, ainsi qu'offrir un déploiement/redéploiement simplifié puisque la configuration de la base de données est réalisée par Symfony. Une approche classique pourrait être d'employer le bundle DoctrineExtensions, mais par souci de simplicité et de pédagogie, vous allez réaliser cette opération à la main à l'aide des fonctionnalités de Symfony. Vous allez donc utiliser le cycle de vie des entités de Doctrine pour automatiser l'initialisation de l'attribut creationDate de l'entité Bookmark.
Comme précisé dans la documentation, lorsque l'intervention est simple, comme définir la date de création ou de modification par exemple, il s'agit juste de définir un callback dans l'entité.
Vous ajouterez donc une nouvelle méthode setCreatedDateValue initialisation la date de création du signet. Vous l'enregistrerez comme callback pour l'événement prePersist de l'entité Bookmark par la présence de l'attribut #[ORM\PrePersist] et vous préciserez à Symfony que l'entité comporte des fonctions de rappel pour les événements du cycle de vie de Doctrine à l'aide de l'attribut #[ORM\HasLifecycleCallbacks].
Contrairement à la documentation, afin que la date de création puisse être initialisée, notamment dans les tests, vous prendrez soin dans la méthode setCreatedDateValue de ne pas initialiser la date de création si elle est déjà initialisée.
setCreatedDateValue à l'entité Bookmark.
Pour ne pas avoir à recalculer la moyenne des notes d'un bookmark lors de l'accès à celui-ci, vous allez lui ajouter une nouvelle propriété rateAverage, qui contiendra la moyenne des notes du signet.
Comme précédemment, vous utiliserez le maker de Symfony, pour ajouter une propriété rateAverage à l'entité Bookmark :
bin/console make:entity Bookmark
Il s'agira d'une propriété float ne pouvant pas être null et vous prendrez soin de lui affecter une valeur par défaut de 0. Même si ce n'est pas nécessaire, afin d'avoir un schéma de base de données valide, vous ajouterez aussi la valeur pas défaut pour la colonne.
Vous penserez à mettre à jour votre base de données au travers l'utilisation d'une migration :
bin/console make:migration
bin/console doctrine:migrations:migrate
Vous relancerez vos jeux de tests pour constater que les tests de l'entité Bookmark ne sont plus valides, en effet, la propriété rateAverage doit être ajoutée à la liste des propriétés attendues. Vous corrigerez le problème pour que l'ensemble des tests soit valide.
La validation des types des propriétés JSON, dans les tests, est réalisée à l'aide de la méthode seeResponseMatchesJsonType du module REST, vous vous référerez à la documentation pour associer un type entier ou réel à la propriété rateAverage.
rateAverage.
Pour mettre à jour la moyenne des notes, vous allez vous integrer au cycle de vie des entités de Doctrine. Cependant, avant de vous intéresser aux événements, vous allez ajouter à la classe BookmarkRepository la méthode suivante :
public function updateRateAverage(Bookmark $bookmark): void
Cette méthode utilisera le QueryBuilder de Doctrine pour mettre à jour l'attribut rateAverage du Bookmark passé en paramètre. Vous veillerez à considérer le cas particulier où la moyenne ne peut pas être calculée parce qu'il n'y a pas d'évaluations pour le Bookmark.
Vous allez maintenant pouvoir définir un écouteur externe à l'entité Rating permettant de réagir à la suite de la création(postPersist) de chaque note afin mettre à jour la moyenne des notes du bookmark associé. Vous ajouterez le squelette de la
classe BookmarkRateAverageUpdateListener (télécharger) à votre projet dans le répertoire src/EntityListener.
La méthode postPersist permettra de mettre à jour le signet associé à l'instance de Rating passé en paramètre à l'aide de la méthode updateRateAverage que vous venez d'écrire. Votre écouteur est enregistré comme un service attaché à l'événement postPersist de l'entité Rating par la présence de l'attribut #[AsEntityListener] et de son paramètre.
Vous ajouterez une classe de test BookmarkRateAverageCest contenant les tests suivants :
0.
BookmarkRepository::updateRateAverage.
BookmarkRateAverageUpdateListener::postPersist.
#[AsEntityListener].
De manière similaire, vous ajouterez la méthode postUpdate à la classe BookmarkRateAverageUpdateListener que vous enregistrerez comme écouteur d'événement pour l'événement postUpdate de l'entité Rating.
Vous validerez son bon fonctionnement à l'aide d'un test de la classe BookmarkRateAverageCest réalisant la modification d'une note d'un bookmark, qui provoquera la mise à jour de la moyenne des notes du bookmark.
Puis, vous ferez de même pour la suppression d'une note. Vous ajouterez deux tests :
0 de la moyenne des notes du bookmark.
Dans la methode postRemove de la classe BookmarkRateAverageCest, vous penserez à supprimer la note de la collection de notes du signet avant de mettre à jour la moyenne des notes.
C'est ici que vous devriez avoir une erreur 500 du serveur Symfony si vous avez oublié de configurer la propriété cascade: ['persist'] dans les relations avec la note, dans le TP précédent.
Pour en finir avec l'ajout de propriété, vous ajouterez une nouvelle propriété owner à l'entité Bookmark, qui sera une relation ManyToOne, ne pouvant être null, vers l'entité User, et inversable par la propriété bookmarks de l'entité User. Vous utiliserez le maker de Symfony pour ajouter cette propriété :
bin/console make:entity Bookmark
Vous prendrez soin de mettre à jour votre base de données à l'aide d'une migration :
bin/console make:migration
Mais vous constaterez que la migration n'est pas applicable sur votre base de donnée. En effet, la propriété n'ayant pas de valeur par défaut et votre base de données contenant déjà des bookmarks, il n'est pas possible de leur affecter un propriétaire automatiquement.
Si vous deviez gérer une base de donnée en production, vous devriez ici éditer votre migration pour rendre la propriété nullable, affecter une valeur intelligente à tous les enregistrements déjà présent dans la base de données, puis rendre la propriété de nouveau non nullable. Mais comme nous sommes encore en phase de développement, vous allez simplement supprimer la base de données et la recréer à l'aide de la commande :
composer db
Mais avant cela, vous allez ajouter la propriété owner à la forge BookmarkFactory, ainsi que faire en sorte qu'un utilisateur soit tiré aléatoirement comme propriétaire de chaque bookmark lors de la génération des données factices.
La génération des signets suppose que les utilisateurs soient dans la base de données, vous devez donc vous assurer que les fixtures pour les utilisateurs soient exécutées avant les signets. Vous préciserez donc des dépendances entre ces deux fixtures.
Vous devriez maintenant pouvoir régénérer votre base de données, vous pourrez controller le bon déroulement en réalisant une requête :
bin/console dbal:run-sql "\ SELECT owner_id, COUNT(*) AS bookmarks \ FROM bookmark \ GROUP BY owner_id \ ORDER BY owner_id \ "
Vous devriez obtenir un résultat similaire à :
--------- --------- owner_id bookmarks ---------- ----------- 1 4 2 1 3 4 4 2 5 7 6 2 7 4 8 1 9 4 10 2 11 2 12 2 13 2 14 3 15 4 16 4 17 3 18 1 19 3 20 3 21 4 22 1 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.
Vous penserez aussi à ajouter la propriété owner aux groupes de sérialisation pour rendre cette propriété accéssible aux utilisateurs de l'API.
Comme d'habitude, vous constaterez que certains de vos tests ne fonctionnent plus, vous les corrigerez pour que l'ensemble des tests soit valide.
owner.
Bookmark : BookmarkFactory.
BookmarkFixtures.
Vous avez dû constater qu'un utilisateur à accès à tous les signets, même les signets privés des autres utilisateurs.
Vous allez donc modifier les requêtes d'accès aux ressources pour que les signets privés ne soient accessibles que par leur propriétaire.
Dans cette optique, Doctrine propose des extensions permettant de modifier les requêtes d'accès aux ressources. Vous allez donc ajouter une extension à Doctrine pour modifier les requêtes d'accès aux ressources de l'entité Bookmark.
Vous ajouterez une classe BookmarkIsPublicOrOwnedExtension dans le répertoire src/Doctrine de votre projet, qui étendra la classe QueryCollectionExtensionInterface. Cette classe enregistrera le service Security dans son constructeur.
Enfin, dans la méthode applyToCollection, si la ressource est une instance de Bookmark, vous ajouterez à la requête passée en paramètre ($queryBuilder) les conditions suivantes :
Normalement, si vous relancez les tests, les tests produits par la méthode getFilteredBookmarkCollection de la classe BookmarkGetCollectionCest devraient échoués. En effet, certain des bookmarks générés ne sont pas public et aucun utilisateur n'est authentifié. Pour résoudre ce problème, vous allez créer un utilisateur que vous authentifierez et vous l'ajouterez comme propriétaire des 4 signets générés.
Une fois que tous les tests sont à nouveau valides, vous ajouterez deux tests à la classe BookmarkGetCollectionCest :
anonymousUserAccessOnlyPublicBookmarks qui vérifiera qu'un utilisateur anonyme n'a accès qu'aux signets publics.
authenticatedUserAccessPublicAndOwnBookmarks qui vérifiera qu'un utilisateur authentifié accède à tous les signets publics, ainsi qu'à ses signets privés.
BookmarkIsPublicOrOwnedExtension.
BookmarkGetCollectionCest.
Vous n'avez défini l'extension Doctrine que pour les collections, il reste donc de nombreuses failles de sécurités.
Vous ferez en sorte d'appliquer les restrictions suivantes aux opérations de l'entité Bookmark :
GET d'un signet doit permettre
POST n'est accessible que par un utilisateur authentifié.
PATCH n'est accessible que par un utilisateur authentifié et il ne peut modifier que ses signets.
DELETE n'est accessible que par un utilisateur authentifié et il ne peut supprimer que ses signets.
Vous en profiterez pour ajouter des contrôles sur la validité des données soumise par l'utilisateur :
name et description ne peuvent pas contenir les caractères « < », « > », « & » et « " » afin de vous protéger contre l'injection de code JavaScript,
url doit être une URL valide.
Vous ajouterez des tests pour valider le fonctionnement votre API.
Bookmark.
Il est toujours un peu fastidieux de devoir transmettre les données lors des requêtes utilisant votre API. Vous allez donc réduire le nombre de propriétés nécessaire à la création d'un signet :
public de l'entité Bookmark.
BookmarkDenormalizer qui injectera l'utilisateur connecté comme owner si la propriété n'est pas présente.
Vous pourrez ajouter un test à la classe BookmarkCreateCest pour valider le fonctionnement de vos ajouts.
public de l'entité Bookmark.
BookmarkDenormalizer.
Vous allez ajouter deux nouvelle opérations permettant d'obtenir des sous-ressources :
/bookmarks/{id}/ratings »),
/users/{id}/bookmarks »).
Comme précédemment, vous ferrez en sorte que la liste des notes d'un signet contienne des informations nichées de l'utilisateur concerné (id, firstname et lastname).
Ces deux routes ne doivent être accessibles que par un utilisateur authentifié.
Selon les choix réalisés lors de la sécurisation des sous-routes « /bookmarks » en GET, il est possible que vous ayez été permissif puisqu'elles étaient toutes accessible par les utilisateurs anonymes. Vous devrez donc peut-être être plus strict.
Vous pouvez constater qu'une faille persiste, un utilisateur à accès à toutes les notes, même les notes des signets privés. Vous corrigerez cette faille à l'aide d'une extension Doctrine : RatingOfPublicOrOwnedBookmarkExtension. Cette extension restreindra les collections de notes aux notes de l'utilisateur connecté et aux notes des signets publics ou de l'utilisateur connecté.
De manière similaire, vous restreindrez l'accès au détail d'une note, aux notes de l'utilisateur connecté et aux notes des signets publics ou de l'utilisateur connecté, à l'aide de la propriété security.
Vous corrigerez les éventuels tests invalides et vous ajouterez des tests pour valider le fonctionnement votre API.
/bookmarks/{id}/ratings ».
/users/{id}/bookmarks ».
RatingOfPublicOrOwnedBookmarkExtension.
security pour restreindre l'accès au détail d'une note.