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

Gestion des utilisateurs

Vous allez maintenant ajouter la gestion des utilisateurs à votre API, pour permettre aux utilisateurs de s'authentifier et d'interagir avec l'API.

Idéalement, pour décorréler l'API fournissant les données, de l'application consommant les données, l'authentification devrait reposer sur l'utilisation de JWT (JSON Web Token). Cette architecture est cependant un peu complexe à mettre en place de manière sécurisée.

Afin de ne pas perdre trop de temps avec l'authentification, dans le cadre de ce TP nous nous reposerons sur l'utilisation d'une authentification en session. L'API et l'authentification seront donc réalisées sur le même serveur.

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

  • Création d'utilisateurs dans l'API
  • Gestion de l'authentification à l'aide de Symfony
  • Création de routes personnalisées dans API Platform
  • Gestion de la normalisation/dénormalisation des données
  • Mise en place de tests fonctionnels
  • Contrôle des données

Création de la table User

Nous avons vu que l'interaction avec la base de données est réalisée à l'aide des entités de Symfony et nous utilisons le MakerBundle pour générer les entités. Cependant, l'entité représentant les utilisateurs est une entité particulière si l'on souhaite gérer l'authentification. Elle dispose donc de sa propre commande de génération :

bin/console make:user

Vous l'appellerez User et elle sera stockée dans la base de donnée. La propriété d'identification sera login et cette entité sera utilisée pour l'authentification.

Vous pourrez ensuite utiliser le générateur d'entité (bin/console make:entity User) pour lui ajouter les propriétés suivantes :

  • firstname de type string de taille 30, ne pouvant pas être null,
  • lastname de type string de taille 40, ne pouvant pas être null,
  • avatar de type blob, ne pouvant pas être null,
  • email de type string de taille 100, 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
Travail réalisé dans cette partie
  • Création de l'entité User pour gérer les utilisateurs.
  • Création de la table User dans la base de données.

Génération d'utilisateurs

Voua allez maintenant créer quelques utilisateurs. Vous vous inspirerez de la documentation de Foundry pour mettre en œuvre les consignes de génération des données factices qui suivent.

Créez une nouvelle forge UserFactory.

bin/console make:factory

Vous utiliserez la bibliothèque Jdenticon pour générer un avatar pour l'utilisateur à partir de son nom et de son prénom. Pour cela, commencez par installer le paquet Composer :

composer require jdenticon/jdenticon

Vous ajouterez ensuite, dans votre UserFactory, la méthode createAvatar avec le prototype suivant :

protected static function createAvatar(string $value)

Elle retournera un avatar de 50 pixels au format PNG, généré à partir du paramètre $value, sous forme d'un type resource (qui ne peut pas être un type de retour !), que vous obtiendrez avec le code suivant :

fopen($icon->getImageDataUri('png'),'r')

La méthode getDefaults retournera un tableau associatif comportant les clés qui correspondent aux propriétés de l'objet User à créer :

  • login : Une valeur qui doit être unique, composée de la chaîne de caractères "user" suivie d'un nombre sur 3 caractères. Par exemple "user010".
  • roles : Un tableau vide.
  • password : La chaîne de caractères "test", vous penserez à hacher le mot de passe à l'aide de la méthode initialize.
  • firstname : Un faux prénom.
  • lastname : Un faux nom.
  • avatar : Le résultat de la méthode createAvatar avec en paramètre une combinaison du nom et du prénom.
  • email : Une adresse email sous la forme prenom.nom@domain, avec le prénom et le nom de l'utilisateur en minuscule, sans caractères accentués ni espace et le domaine et un nom de domaine généré par « Faker ».

Pour la conversion du nom et du prénom, comme en S3, vous pourrez utiliser un transliterateur et une fonction de remplacement.

Si vous ne définissez que la méthode d'instance getDefaults, le mot de passe sera stocké en clair dans la base de donnée. Vous utiliserez la méthode initialize pour « hacher » le mot de passe après l'initialisation d'un utilisateur.

Créez ensuite une nouvelle classe de génération de contenu pour la table que vous appellerez UserFixtures :

bin/console make:fixtures

Pour les données, afin de simplifier la connexion, vous créerez 3 utilisateurs  "user1", "user2", "user3", puis vous ajouterez 20 utilisateurs aléatoires.

Vous pourrez enfin remplir la base de données à l'aide de la commande suivante :

composer db

Encore une fois, vous pouvez vérifier que tout fonctionne correctement soit à l'aide de phpMyAdmin, soit en réalisant une requête :

bin/console dbal:run-sql "SELECT login, firstname, lastname, email FROM user LIMIT 5"

Vous constaterez que vos utilisateurs ont des noms plutôt de type anglo-saxon. En consultant la documentation sur la configuration de Foundry, vous modifierez la localisation de Faker et obtenir des noms, prénom et emails français.

Vous devriez finalement obtenir ce type de résultat :

 --------- ----------- ---------- ----------------------------- 
  login     firstname   lastname   email                         
 --------- ----------- ---------- ----------------------------- 
  user1     Sabine      Mace       sabine.mace@aubert.fr        
  user2     Renée       Michaud    renee.michaud@guillot.com    
  user3     Benoît      Paul       benoit.paul@bourgeois.com    
  user871   Adélaïde    Gay        adelaide.gay@riou.com        
  user349   Camille     Gregoire   camille.gregoire@lefevre.fr  
 --------- ----------- ---------- ----------------------------- 

Vous constaterez à l'aide de la requête suivante que bien que les mots de passe soient identiques pour tous les utilisateurs, les chaînes de caractères stockées dans la base de données sont toutes différentes.

bin/console dbal:run-sql "SELECT id, password FROM user"
Travail réalisé dans cette partie
  • Création d'un script de génération de données : UserFixtures.
  • Création automatique de contenu pour la table User.

Mise en place de l'authentification

Maintenant que vous disposez d'utilisateurs à authentifier, vous allez pouvoir mettre en place l'authentification dans Symfony, toujours depuis la console :

bin/console make:auth

Vous souhaitez créer un authentificateur reposant sur un formulaire d'authentification (Login form authenticator), que vous appellerez LoginFormAuthenticator. Le contrôleur s'appellera SecurityController et vous souhaitez aussi une route pour que vos utilisateurs se déconnectent. Vous permettrez aussi à l'application de se souvenir des utilisateurs s'ils le souhaitent en ayant coché la case « Se souvenir de moi » à la connexion.

À l'heure de la rédaction de ce sujet, la gestion de l'option « Se souvenir de moi » du générateur de Symfony est systèmatiquement en « toujours ». Vous allez corriger ce problème en :

  • commentant l'option always_remember_me du fichier config/packages/security.yaml,
  • ajoutant une case à cocher dans le formulaire d'authentification, comme spécifié dans la documentation de Symfony.

Vous disposez maintenant d'un formulaire de connexion sur la route /login. Vous ferez un peu de mise en forme en ajoutant du CSS dans le fichier public/css/login.css et vous insérerez la feuille de style dans le twig du formulaire.

Afin que les tests fournis dans la suite du TP restent fonctionnels, le texte du bouton de soumission doit être « Authentification ».

Vous pouvez maintenant essayer de vous connecter à l'API : http://127.0.0.1:8000/login. Symfony vous informe que vous n'avez pas géré la redirection en cas de connexion. Vous remplacerez le contenu de la méthode onAuthenticationSuccess de la classe App\Security\LoginFormAuthenticator par une redirection vers la documentation de l'API (route api_doc). Si vous mettez en place une application plus tard, vous voudrez probablement rediriger vers la page d'accueil de l'application.

Travail réalisé dans cette partie
  • Création de l'authentificateur de l'API.
  • Correction de la gestion de la case à cocher « Se souvenir de moi ».
  • Mise en forme du formulaire de connexion.
  • Finalisation de l'authentification en ajoutant une redirection vers la documentation de l'API.

Configuration des opérations API Platform

Configuration des opérations

En vous rendant sur la page de votre API, vous constatez que par défaut, la ressource User n'est pas disponible. Vous ajouterez l'attribut PHP8 « #[ApiResource] » pour y remédier.

Par défaut, API Platform vous propose une représentation CRUD de vos entités, en utilisant le nom de l'entité comme nom de ressource. Vous commencerez par choisir les opérations exposées par votre API. Dans le cadre de ce TP, nous considérerons que les utilisateurs sont créés ou supprimés par des pages d'administration qui n'utilisent pas l'API. Vous rendrez donc accessibles les actions permettant d'obtenir le détail d'un utilisateur (GET) ainsi que la modification d'un utilisateur (PATCH).

Dans Api Platform existe deux actions permettant de modifier une ressource PUT et PATCH. La première impose une modification complète de la ressource, toutes les propriétés doivent être fournies, tandis que la deuxième permet une modification partielle la ressource, un sous ensemble des propriétés peut être fourni. Comme nous ne souhaitons pas transmettre systématiquement l'avatar de l'utilisateur lors des modifications, nous utiliserons l'action PATCH pour la modification d'utilisateur.

Vous allez ensuite choisir les attributs exposés de l'utilisateur. API Platform propose une solution efficace pour sélectionner les attributs de l'entité qui doivent être exposés, en lecture ou en écriture. Cette solution repose sur l'usage des groupes de sérialisation.

Vous définirez deux groupes :

  • User_read pour la normalisation,
  • User_write pour la dénormalisation, pour les actions (PATCH).

La lecture (GET) sera publique et vous limiterez l'accès aux attributs suivant : id, login, firstname et lastname. L'avatar sera aussi accessible, mais puisqu'il s'agit d'une image, nous le traiterons plus tard.

La modification (PATCH) sera restraint à l'utilisateur lui-même, il aura accès à plus d'attributs : login, password, firstname, lastname et email.

Remarque importante

API Platform repose grandement sur l'utilisation du cache pour la gestion des groupes de serialisation. Pour être sûr que les modifications que vous effectuez sur les groupes soient effectives, vous devez nettoyer le cache à l'aide de la commande suivante :

bin/console cache:clear

Retour sur l'authentification

Si vous essayez, vous constaterez que même non authentifié, vous pouvez modifier les données d'un utilisateur. En effet, vous n'avez rien fait pour restreindre l'accès à vos actions. API Platform propose des attributs permettant de gérer la sécurité de vos actions. Vous ajouterez les attributs nécessaires afin que l'action PATCH ne soit accessible qu'à un utilisateur authentifié et ne concerne que ces données. Vous constaterez que vous obtenez un code HTTP 500 Internal Server Error. Si vous allez voir le détail de l'erreur dans le corps de la réponse, vous comprendrez qu'API Platform par défaut suppose votre API comme « stateless » et y désactive le support des sessions que vous utilisez pour authentifier vos utilisateurs. Pour résoudre ce conflit, vous modifierez la configuration d'API Platform (« config/packages/api_platform.yaml ») pour lui indiquer que vous souhaitez utiliser les sessions, en passant l'option « stateless » de true à false.

Une fois ces modifications réalisées, si vous tentez une action PATCH sur un utilisateur sans être connecté, vous devriez constater que vous n'obtenez pas un code HTTP 401 Unauthorized, mais un code HTTP 200 avec une redirection vers le formulaire de connexion. Ceci est dû à l'utilisation de l'authentificateur LoginForm qui suppose que votre utilisateur non authentifié, ne pouvant pas accéder à sa ressource, va vouloir s'authentifier, au lieu de voir un message d'erreur. Or dans le cas d'une API vous souhaitez retourner une erreur. Pour corriger ce comportement, vous pouvez surcharger la méthode start de LoginFormAuthenticator :

Maintenant que vous obtenez un code HTTP 401 Unauthorized en essayant de modifier à une ressource protégée sans être authentifié, vous allez pouvoir tenter la même opération en étant authentifié.

Validation par les tests

Avant de mettre en place les tests, vous constaterez qu'un utilisateur peut modifier son email, mais qu'il ne peut pas vérifier la modification, car il n'a pas accès à celui-ci. En effet, l'email n'est pas exposé en lecture. Vous ajouterez donc l'attribut email à un nouveau groupe de sérialisation User_me et vous ajouterez ce groupe au context de normalization de l'opération PATCH.

Vous allez maintenant pouvoir ajouter les tests fonctionnels pour vos actions GET et PATCH.

Vous utiliserez les classes« UserGetCest.php » (télécharger) et « UserPatchCest.php » (télécharger) dans le répertoire tests/Api/User et vous ferez en sortes que votre code valide tous les tests.

Remarque importante

Vous avez sans doute remarqué des messages d'erreur lors de l'exécution de vos tests, même si ces derniers passent :

App\Tests.Api Tests (14) ---------------------------------------------------------------------------------------------------------
 UserGetCest: Anonymous user get simple user element (0.05s)
 UserGetCest: Authenticated user get simple user element for others (0.02s)
- UserPatchCest: Anonymous user forbidden to patch user[error] Uncaught PHP Exception
 Symfony\Component\HttpKernel\Exception\HttpException: "" at api-bookmarks/src/Security/LoginFormAuthenticator.php line 70

 UserPatchCest: Anonymous user forbidden to patch user (0.01s)
- UserPatchCest: Authenticated user forbidden to patch other user[error] Uncaught PHP Exception
Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException: "Access Denied." at api-bookmarks/vendor/symfony/security-http/Firewall/ExceptionListener.php line 138

Ces « erreurs » sont « normales » : ces tests demandent la création d'un client Web qui interroge le serveur Web de votre application Symfony, laquelle produit des traces d'exécution en cas de problèmes de sécurité, ce qui est justement l'objet de certains des tests exécutés.

Afin de masquer ces traces d'exécution qui n'ont pas leur place dans la console de tests, vous allez créer un environnement de test dans lequel les traces d'exécution seront désactivées. Pour cela, vous ajouterez une configuration spécifique à l'environnement de test dans le fichier config/services.yaml permettant de désactiver les traces d'exécution :

when@test:
    services:
        # Disable logger to avoid showing errors during tests
        Psr\Log\NullLogger: ~
        logger: '@Psr\Log\NullLogger'

Afin de prendre en compte cette nouvelle configuration, vous devez effacer le cache pour l'environnement de test :

APP_ENV=test bin/console cache:clear
Travail réalisé dans cette partie
  • Configuration des opérations supportées.
  • Configuration de la sérialisation des ressources.
  • Surcharge de la méthode start de LoginFormAuthenticator.
  • Activation du support des sessions dans API Platform.
  • Ajout et validation des tests pour les actions GET et PATCH de l'entité User.

Modification des données lors de la dénormalisation

Vos actions de modification de l'utilisateur (PATCH) comportent un sérieux défaut. Si l'utilisateur modifie son mot de passe, le mot de passe n'est pas haché dans la base de données et l'utilisateur ne peut plus s'authentifier.

Le script Composer « db » mis en place dans le TP précédent permet de réinitialiser complètement la base de données et ainsi faire en sorte que les id des utilisateurs user1, user2 et user3 soient toujours 1, 2 et 3. Ceci va simplifier le travail de développement et de test.

Vous allez devoir transformer les données transmises à l'API avant qu'elles ne soient sauvegardées dans la base de données. De nombreuses stratégies sont envisageables, nous verrons plus tard comment utiliser les événements pour intéragir avec Symfony et Doctrine. Vous allez ici vous insérer dans la chaîne de transformation des données en instance de User.

Comme vous l'avez vu avec l'usage des groupes de sérialisation, le processus de transformation des données (JSON, XML ou autres...) en un objet (une instance d'entité pour vous) est la désérialisation (la sérialisation étant la transformation inverse). Celle-ci se décompose en deux étapes, le passage du format de données en un tableau, puis de ce tableau vers l'objet. Nous allons intervenir lors de cette deuxième étape : la dénormalisation.

De manière similaire à l'exemple fourni dans la documentation, vous allez créer, une fois que vous aurez lu l'ensemble des consignes de cette partie, une classe UserDenormalizer qui va pouvoir modifier les données avant qu'elles ne soient transformées en instance de User.

Mais avant de commencer le code, étudions le fonctionnement de cette classe. Nous allons ici parler de la dénormalisation, mais la logique reste la même dans le cadre de la normalisation. Cette classe doit fournir deux méthodes :

  • la première supportsDenormalization() retourne un booléen indiquant si la classe doit transformer les données. Pour cela, elle dispose des données, du type de ressource cible, du format d'origine et du contexte.
  • la seconde méthode, denormalize(), doit réaliser la transformation avec les mêmes informations. Ici il y a deux possibilités :
    • soit vous prenez en charge la création ou récupération de l'objet à partir des données et vous les retournez et vous n'avez pas besoin d'implémenter l'interface DenormalizerAwareInterface,
    • soit vous vous contentez de modifier les données puis de transmettre les données modifiées au dénormaliseur d'API Platform.

Cette dernière solution est plus élégante car elle permet de chaîner plusieurs transformations. Cependant API Platform ne conserve pas de trace des transformations qui ont déjà été effectuées. C'est pourquoi dans l'exemple une constante comportant le nom de la classe est ajoutée au contexte afin d'en garder la trace.

Sur ce principe, vous allez créer la classe UserDenormalizer :

  • à l'aide de l'assistant de PhpStorm,
  • dans l'espace de nom App\Serialization\Denormalizer,
  • implémentant les interfaces
    • Symfony\Component\Serializer\Normalizer\DenormalizerInterface
    • et Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface,
  • utilisant le trait Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait,
  • définissant
    • une constante ALREADY_CALLED ayant pour valeur 'USER_DENORMALIZER_ALREADY_CALLED',
    • une propriété $passwordHasher initialisée dans le constructeur à l'aide de l'autowiring avec le service Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface,
    • une propriété $security initialisée dans le constructeur à l'aide de l'autowiring avec le service Symfony\Bundle\SecurityBundle\Security,
    • une méthode supportsDenormalization retournant vrai si la clé self::ALREADY_CALLED n'est pas définie dans le contexte et que le type ciblé est la classe User, et faux sinon,
      Remarque importante

      L'assistant de génération PhpStorm ne vous ajoute pas le dernier paramètre optionnel array = [] de la méthode supportsDenormalization. Vous veillerez donc à l'ajouter manuellement, conformément à l'exemple de la documentation.

      De plus, l'assistant peut vous ajouter une méthode __call() qui n'est pas nécessaire. Vous veillerez donc à la supprimer.

    • Et pour finir, la méthode denormalize()
      • ajoutant la valeur true à la clé self::ALREADY_CALLED du contexte.
      • puis, si l'utilisateur a transmis un mot de passe dans les données, utilisant les services pour hacher le mot de passe (méthode hashPassword() du passwordHasher, dont le premier paramètre est l'utilisateur connecté récupéré à l'aide de la méthode getUser() de Security)
      • avant d'invoquer à nouveau la dénormalisation depuis la propriété denormalizer de l'instance courante et de retourner le résultat de l'invocation.

Un utilisateur devrait maintenant pouvoir changer son mot de passe.

Vous validerez votre code à l'aide des tests contenus dans le fichier « UserPatchPasswordCest.php » (télécharger).

Travail réalisé dans cette partie
  • Création du dénormalisateur : UserDenormalizer.
  • Validation du code par les tests de la classe UserPatchPasswordCest.

Ajout d'une route personnalisée vers l'utilisateur connecté

Afin de pouvoir obtenir les informations de l'utilisateur connecté depuis une application Web en AJAX, vous allez ajouter une ressource /me à votre API retournant les informations de l'utilisateur connecté.

Cette route ne correspond pas au schéma classique des données dans API Platform : ce n'est pas une collection que l'on souhaite et la route ne fournit pas de paramètre pour identifier une ressource particulière. De plus, pour obtenir les données de l'utilisateur connecté, il faudrait utiliser un service dans l'entité, ce qui est contraire aux bonnes pratiques. Vous allez donc vous-même définir la source des données pour une nouvelle action personnalisée.

Vous allez utiliser le MakerBundle de Symfony pour générer une nouvelle source de données appelée MeProvider :

bin/console make:state-provider MeProvider

Pour obtenir l'utilisateur courant depuis MeProvider, vous requerrez l'injection d'une instance de Symfony\Bundle\SecurityBundle\Security lors de sa construction, que vous utiliserez pour initialiser une propriété vous permettant d'utiliser la méthode getUser() de Security pour obtenir l'utilisateur courant dans la méthode provide de MeProvider.

Vous pourrez ensuite associer cette source de données à une nouvelle action personnalisée de l'entité User. En utilisant la définition de la route /me suivante :

Vous noterez l'utilisation du paramètre uriTemplate pour spécifier la route à utiliser pour cette action personnalisée, ainsi que l'utilisation du paramètre provider pour indiquer la source des données.

Vous prendrez soin de comprendre l'utilisation du paramètre openapiContext permettant de surcharger certains éléments de la documentation de l'opération de l'API.

Pour finaliser cette action, vous ferez en sorte qu'un utilisateur ne puisse accéder à cette ressource que s'il est authentifié en utilisant le paramètre security de l'opération.

Vous utiliserez ensuite le groupe de sérialisation User_me pour identifier les attributs accessibles à l'utilisateur connecté, email dans notre cas. La ressource /me devrez permettre d'obtenir l'identifiant, le login, le nom, le prénom et l'email de l'utilisateur connecté.

Pour valider votre code, vous utiliserez la classe de test UserGetMeCest (télécharger).

Vous constaterez que l'un de vos tests ne passe pas, en effet, lorsque l'utilisateur n'est pas connecté le code de retour est un code HTTP 404 au lieu d'un code HTTP 401. Ceci est dû au fait qu'API Platform gère l'accès à la ressource et le cloisonnement des données à l'aide de la même propriété : security. La gestion de l'accès à la ressource est donc gérée après l'obtention de la ressource, nécessaire au cloisonnement. Une exception de ressource non trouvée est donc jetée avant que l'accès à la ressource puisse être géré.

Ce comportement n'est pas idéal d'un point de vue de la sécurité, vous allez donc modifier votre approche. Vous allez gérer l'accès aux ressources à l'aide de la propriété access_control de la configuration globale de la sécurité.

Vous adopterez une approche stricte, sous la forme d'une liste blanche, en commençant par restreindre l'accès à l'ensemble de l'API à un utilisateur authentifié :

Vous pourrez alors ajouter des règles pour les ressources que vous souhaitez rendre publiques :

Vos tests devraient maintenant tous être valides.

Remarque importante

Comme d'habitude avec le routage Symfony, l'ordre est important. La première règle qui correspond à une route est utilisée. Vous devez donc placer les règles les plus spécifiques en premier, dans notre cas, la règle imposant l'authentification sur toutes l'API doit être placée à la fin.

L'accès aux ressources étant gérée dans le fichier « security.yaml », vous n'utiliserez le paramètre security des opérations d'API Platform que pour le cloisonnement des données. Vous pouvez donc simplifier ceux-que vous avez déjà.

Pour finir, vous constaterez que la documentation des types de réponses n'est pas correcte, la réponse HTTP 404 n'est pas possible puisque l'utilisateur est nécessairement authentifié pour accéder à la ressource. Vous allez donc surcharger la documentation OpenAPI des réponses d'une opération pour ne retourner qu'une réponse HTTP 200.

Vous êtes obligé de remplacer l'ensemble des réponses, pour n'y placer qu'une réponse HTTP 200, similaires à celle générée par API Platform. Vous pouvez consulter la documentation OpenAPI générée par API Platform sous la forme d'un JSON injecté dans le code source de la page de la documentation de votre API.

Fondamentalement, vous allez redéfinir le contenu de la réponse HTTP 200 en faisant référence au schéma généré à l'aide des groupes de sérialisation.

Remarque importante

Le chemin vers le schéma comporte une partie décrivant les groupes de sérialisation : User_me_User_read. Cette partie dépend de l'ordre de déclaration des groupes de sérialisation. Si vous avez déclaré le groupe User_me avant le groupe User_read, vous devez utiliser User_me_User_read, sinon vous devez utiliser User_read_User_me.

Travail réalisé dans cette partie
  • Création de la source de données MeProvider.
  • Ajout de l'opération me sur l'entité User utilisant MeProvider.
  • Choix des attributs exposés.
  • Ajout et validation des tests.
  • Gestion des droits d'accès à la route.
  • Surcharge de la documentation OpenAPI.

Ajout d'une route personnalisée vers l'avatar d'un utilisateur

L'avatar d'un utilisateur est une image stockée dans la base de données qui n'est pas directement accessible pour l'afficher dans une page web. Vous allez créer une route permettant d'obtenir l'avatar d'un utilisateur.

Cette route ne correspond pas non plus au schéma classique des données dans API Platform, cependant elle ne correspond pas non plus au cas d'utilisation des sources de données personnalisées que nous venons de voir, car le type de données à retourner n'est pas sérialisable. Dans ce cas, API Platform propose la création d'une opération personnalisée reposant sur les controller de Symfony.

Vous commencerez par ajoutez la classe de test d'accès à l'avatar : « tests/Api/User/UserGetAvatarCest.php » (télécharger).

Vous allez ensuite utiliser le MakerBundle de Symfony pour générer un nouveau contrôleur appelé GetAvatarController, sans template twig associé :

bin/console make:controller --no-template GetAvatarController

Conformément à la documentation d'API Platform, vous remplacerez le contenu de la définition de la classe du contrôleur par une unique méthode d'instance publique __invoke retournant une instance de Symfony\Component\HttpFoundation\Response. Cette méthode n'étant pas une action, elle ne comportera pas d'attribut #[Route()]. Dans la méthode, vous modifierez l'en-tête Content-Type de cette réponse pour qu'il soit égal à image/png et vous définirez le contenu de la réponse avec l'avatar de l'utilisateur reçu en paramètre.

L'accesseur getAvatar vous retourne une ressource PHP, pour obtenir le contenu de l'image, vous utiliserez la fonction stream_get_contents.

Remarque importante

La lecture d'un flux implique la mise à jour du curseur de lecture interne dudit flux. Une fois que le curseur a atteint la fin du flux, plus aucune lecture n'est possible si le curseur n'est pas replacé en début de flux. Afin d'assurer une lecture complète des données de l'avatar dans toutes les conditions, veillez à bien préciser tous les paramètres de stream_get_contents.

Le contrôleur est maintenant fonctionnel, il ne restera plus qu'à l'associer à la ressource /users/{id}/avatar dans votre API en réalisant les étapes qui suivent.

En vous inspirant de la documentation, vous ajouterez une opération Get dans le tableau operations de l'attribut #[ApiResource] de l'entité User. Cette nouvelle opération sera associée à la ressource /users/{id}/avatar et au contrôleur GetAvatarController :

Les tests devraient vous révéler un problème d'accès à l'avatar d'un utilisateur. En effet, vous constaterez que la route n'est pas accessible pour les utilisateurs anonymes. Vous allez donc ajouter une règle d'accès à la ressource /users/{id}/avatar dans le fichier « security.yaml afin de rendre l'ensemble des tests valides.

Si vous testez observer cette opération en utilisant l'interface Web de l'API : http://localhost:8000/api/, vous constaterez que la documentation du type de retour n'est pas correcte. Vous allez devoir surcharger la documentation OpenAPI de l'opération en définissant le paramètre openapiContext. Vous ajouterez à la définition OpenAPI de la réponse du code HTTP 200 une propriété content décrivant une image PNG :

Travail réalisé dans cette partie
  • Création de contrôleur : GetAvatarController.
  • Retourne l'avatar de l'utilisateur passé en paramètre lors de l'invocation du contrôleur.
  • Association du contrôleur GetAvatarController et de la ressource /users/{id}/avatar de votre API dans l'entité User.
  • Surcharge de la documentation OpenAPI.

Validation des données

Vous avez probablement noté qu'un utilisateur authentifié peut modifier son email, mais qu'il n'apparait pas dans la synthèse du résultat de la modification, ce qui est contre intuitif en terme d'ergonomie.

Maintenant que vous avez ajouté un nouveau groupe de sérialisation User_me contenant les informations supplémentaires de l'utilisateur connecté, vous allez pouvoir ajouter ces informations au résultat de la normalisation de l'opération PATCH.

Vos tests de l'opération PATCH ne devraient plus être valides, vous devez corriger les tests en ajoutant la propriété email de type string:email au tableau résultat de la méthode de classe expectedProperties des classes de tests.

Un dernier point important lorsque vous interagissez avec les données fournies par l'utilisateur est de contrôler la validité de ces données. API Platform offre des solutions pour valider automatiquement ces données.

Vous allez ajouter votre premier jeu de tests permettant de valider le contrôle des données en créant la classe de tests User\UserPatchDataValidationCest :

php vendor/bin/codecept generate:cest Api User\\UserPatchDataValidationCest

En vous inspirant des autres tests, vous remplacerez le contenu dez la classe par ces 2 méthodes :

  • la méthode de classe expectedProperties, retournant un tableau associatif avec comme clés les propriétés supportées par l'opération PATCH et leur type comme valeur associée,
  • la méthode de test loginUnicityTest réalisant le test de validation de l'unicité du login. Selon le scenario suivant :
    • création d'un utilisateur avec le login 'authenticated',
    • authentification de cet utilisateur,
    • création d'un nouvel utilisateur avec le login 'login',
    • modification du login de l'utilisateur authentifié avec le login 'login',
    • constater que la réponse est un code HTTP 422 Unprocessable Entity.

Vous pouvez maintenant ajouter une contrainte d'unicité sur le login des instances de User à l'aide de la contraintes UniqueEntity afin que tous vos tests soient valides.

Vous allez ensuite ajouter des contrôles sur l'email et les autres chaînes de caractères. Vous commencerez par ajouter dans la classe UserPatchDataValidationCest la methode de test suivante avec son fournisseur de données associé :

Vous commencerez par ajouter un contrôle sur la propriété email.

Ensuite, comme solution rapide pour se protéger de l'injection JavaScript, vous interdirez, à l'aide d'une expression rationnelle, les caractères « < », « > », « & » et « " » pour les propriétés login, firstname et lastname afin qu'ils ne puissent pas contenir de code malicieux.

Une fois que votre code valide l'ensemble des tests, vous constaterez dans la documentation que les exemples générés automatiquement pour les propriétés login, firstname et lastname retournées par les opérations de User ne sont pas très explicites. Vous allez donc surcharger ces exemples en utilisant le paramètre example de l'attribut PHP #[ApiProperty] des propriétés login, firstname et lastname dans l'entité User.

Travail réalisé dans cette partie
  • Modification de la normalisation des actions PATCH de l'entité User.
  • Modification des tests des actions PATCH de l'entité User pour supporter la présence de la propriété email.
  • Contrôle de l'unicité du login.
  • Ajout de la validation des données de User : email, login, firstname et lastname.
  • Ajout de tests de validation du contrôle des données de User.
A. Jonquet DUT-INFO/REIMS