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.
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.
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:migrationsuivie de :
bin/console doctrine:migrations:migrate
User
pour gérer les utilisateurs.
User
dans la base de données.
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 defaults
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 defaults
, 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"
La génération des utilisateurs est un peu longue, car le hachage des mots de passe est un processus, qui pour être robuste doit être couteux. Dans le cadre du développement, le hachage des mots de passe n'a pas de nécessité d'être particulièrement robuste et il est plus pratique qu'il soit rapide. En vous inspirant de la dernière section du fichier « config/packages/security.yaml » concernant le mode de « test », vous ajouterez un section pour le mode de « dev » avec la même configuration que le mode de « test ».
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"
UserFixtures
.
User
.
Maintenant que vous disposez d'utilisateurs à authentifier, vous allez pouvoir mettre en place l'authentification dans Symfony, toujours depuis la console :
php bin/console make:security:form-login
Vous souhaitez créer une authentification reposant sur un formulaire d'authentification (form-login). Le contrôleur s'appellera SecurityController
et vous souhaitez aussi une route pour que vos utilisateurs se déconnectent. Vous ne pas générer les tests PHPUnit pour le moment.
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. Par défaut, vous êtes redirigé vers la page d'accueil aprés votre authentification. Vous modifierez la configuration de la sécurité pour rediriger l'utilisateur vers la documentation de l'API (api_doc
). Si vous mettez en place une application plus tard, vous voudrez probablement rediriger vers la page d'accueil de l'application.
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
).
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
.
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
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'authentification par formulaire 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 allez personnaliser la réponse d'un utilisateur non authorisé du part-feu principal de Symfony.
:
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é.
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.
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
PATCH
de l'entité User
GET
et PATCH
de l'entité User
.
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 transmises par l'utilisateur.
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 pour la normalisation, 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 la classe UserDenormalizer
. Nous allons ici parler de la dénormalisation, mais la logique reste la même dans le cadre de la normalisation, comme dans l'example de la documentation. Cette classe doit fournir trois méthodes :
getSupportedTypes()
retourne un tableau identifiant permettant de définir si la transformation de cette classe doit s'appliquer pour le type d'objet souaité et si la transformation doit être mise en cache. Vous pouvez vous réferer à la documentation pour une explication approfondie, mais en simplifiant un peu, le tableau résultat est un tableau associatif indiquant pour chaque entité, à l'aide de son nom complétement qualifié (la clé), si la transformation : n'est pas supportée (null
), est supportée et mis en cache (true
) ou supportée et pas mis en cache (false
). Elle dispose aussi du format d'origine.
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.
denormalize()
, doit réaliser la transformation avec les mêmes informations. Ici il y a deux possibilités :
DenormalizerAwareInterface
,
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 et sa présence est détectée pour identifier les itérations suivant l'initialisation.
Sur ce principe, vous allez créer la classe UserDenormalizer
:
App\Serialization\Denormalizer
,
Symfony\Component\Serializer\Normalizer\DenormalizerInterface
Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface
,
Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait
,
ALREADY_CALLED
ayant pour valeur 'USER_DENORMALIZER_ALREADY_CALLED'
,
$passwordHasher
initialisée dans le constructeur à l'aide de l'autowiring avec le service Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface
,
$security
initialisée dans le constructeur à l'aide de l'autowiring avec le service Symfony\Bundle\SecurityBundle\Security
,
getSupportedTypes
retournant un tableau contenant une clé correspondant à l'entité User
et validant la transformation pour cette entité, sans mis en cache,
supportsDenormalization
retournant vrai si la clé self::ALREADY_CALLED
n'est pas définie dans le contexte et faux sinon,
L'assistant de génération PhpStorm ne vous ajoute pas le dernier paramètre optionnel array = []
de la méthode supportsDenormalization
. Mais PHP CS Fixer vous proposerea de coriger le prototype de la méthode.
denormalize()
true
à la clé self::ALREADY_CALLED
du contexte.
hashPassword()
du passwordHasher
, dont le premier paramètre est l'utilisateur connecté récupéré à l'aide de la méthode getUser()
de Security
)
denormalizer
de l'instance courante (fournie par le trait DenormalizerAwareTrait
) 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).
UserDenormalizer
.
UserPatchPasswordCest
.
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.
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.
Malheureusement, il n'est pas encore possible de désactiver les réponses d'erreur générées pas API Platform. Pour simplifier vous pourrez désactiver la génération des réponses d'erreur à l'aide la configuration suivante à ajouté au fichier de configuration « config/packages/api_platform.yaml
» :
MeProvider
.
User
utilisant MeProvider
.
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
.
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
, notamment en positionnant l'offset
explicitement à 0.
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 observez 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 openapi
. Vous ajouterez à la définition OpenAPI de la réponse du code HTTP 200
une propriété content
décrivant une image PNG :
GetAvatarController
.
GetAvatarController
et de la ressource /users/{id}/avatar
de votre API dans l'entité User
.
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 :
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,
loginUnicityTest
réalisant le test de validation de l'unicité du login. Selon le scenario suivant :
'authenticated'
,
'login'
,
'login'
,
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
.
User
.
User
pour supporter la présence de la propriété email
.
User
: email
, login
, firstname
et lastname
.
User
.