Programmation PHP avec Symfony/API
Pour créer une interface de programmation (API) REST avec Symfony, il existe plusieurs bibliothèques :
- API Platform[1], tout-en-un qui utilise les attributs PHP (ou des annotations en PHP < 8) des entités pour créer les APIs (donc pas besoin de créer des contrôleurs ou autres). Par défaut il permet de sérialiser les flux en JSON (dont JSON-LD, JSON-HAL, JSON:API), XML (dont HTML), CSV, YAML, et même en GraphQL[2].
- Sinon il faut combiner plusieurs éléments : routeur, générateur de doc en ligne et sérialiseur.
API Platform
[modifier | modifier le wikicode]Installation
[modifier | modifier le wikicode]composer require api
Utilisation
[modifier | modifier le wikicode]
Le GraphQL de la version 1.1 passe par le schéma REST, et ne bénéficie donc pas du gain de performances attendu sans overfetching.
En bref, les routes d'API sont définies depuis les entités Doctrine.
Pour ajouter des fonctionnalités supplémentaires aux create/read/update/delete, il faut passer par des data providers[3] ou des data persisters[4], pour transformer les données, respectivement à l'affichage et à la sauvegarde.
Attributs
[modifier | modifier le wikicode]ApiResource
[modifier | modifier le wikicode]Définit les noms et méthodes REST (GET, POST...) des routes de l'API.
Exemple sur la V3[5] :
#[ApiResource( operations: [ new Get(), new GetCollection() ] )] class MyEntity...
Avec personnalisation de la vue OpenAPI :
#[ApiResource( operations: [ new Get(), new GetCollection(), ], openapiContext: [ 'summary' => '', 'tags' => ['Enums'], ] )]
Idem sur la V2
#[ApiResource( collectionOperations: [ 'get' => [ 'openapiContext' => [ 'summary' => '', 'tags' => ['Enums'], ], ], ], itemOperations: [ 'get' => [ 'openapiContext' => [ 'summary' => '', 'tags' => ['Enums'], ], ], ], )]
MaxDepth
[modifier | modifier le wikicode]Définit le niveau de sérialisation d'un élément lié. Par exemple, si un client a plusieurs contrats et que ses contrats ont plusieurs produits, un MaxDepth(1) sur l'attribut client->contrat fera que la liste des clients comprendra tous les contrats mais pas leurs produits.
Évènements
[modifier | modifier le wikicode]API Platform ajoute plus d'une dizaine d'évènements donc les priorités sont définies dans EventPriorities[6].
Par exemple, pour modifier un JSON POST envoyé, utiliser EventPriorities::PRE_DESERIALIZE.
L'évènement suivant POST_DESERIALIZE contient les objets instanciés à partir du JSON.
Triplet de bibliothèques
[modifier | modifier le wikicode]Installation
[modifier | modifier le wikicode]FOS REST
[modifier | modifier le wikicode]FOSRestBundle apporte des annotations pour créer des contrôleurs d'API[7]. Installation :
composer require "friendsofsymfony/rest-bundle"
Puis dans config/packages/fos_rest.yaml :
fos_rest:
view:
view_response_listener: true
format_listener:
rules:
- { path: '^/', prefer_extension: true, fallback_format: ~, priorities: [ 'html', '*/*'] }
- { path: ^/api, prefer_extension: true, fallback_format: json, priorities: [ json ] }
Documentation
[modifier | modifier le wikicode]Toute API doit exposer sa documentation avec ses routes et leurs paramètres. NelmioApiDocBundle est un de générateur de documentation automatique à partir du code[8], qui permet en plus de tester en ligne. En effet, pour éviter de tester les API en copiant-collant leurs chemins dans une commande cURL ou dans des logiciels plus complets comme Postman[9], on peut installer une interface graphique ergonomique qui allie documentation et test en ligne :
composer require "nelmio/api-doc-bundle"
Son URL se configure ensuite dans routes/nelmio_api_doc.yml :
app.swagger_ui:
path: /api/doc
methods: GET
defaults: { _controller: nelmio_api_doc.controller.swagger_ui }
À ce stade l'URL /api/doc affiche juste un lien NelmioApiDocBundle. Mais si les contrôleurs d'API sont identifiés dans annotations.yaml (avec un préfixe "api"), on peut voir une liste automatique de toutes leurs routes.
Pour documenter /api/* sauf /api/doc, il faut préciser l'exception en regex dans packages/nelmio_api_doc.yaml :
nelmio_api_doc:
areas:
path_patterns:
- ^/api/(?!/doc$)
Il faut vider le cache de Symfony à chaque modification de nelmio_api_doc.yaml.
Authentification
[modifier | modifier le wikicode]Pour tester depuis la documentation des routes nécessitant un token JWT, ajouter dans packages/nelmio_api_doc.yaml :
nelmio_api_doc:
documentation:
securityDefinitions:
Bearer:
type: apiKey
description: 'Value: Bearer {jwt}'
name: Authorization
in: header
security:
- Bearer: []
Il devient alors possible de renseigner le token avant de tester.
Exemple
[modifier | modifier le wikicode]Dans un contrôleur, au-dessus de l'attribut #[Route]
d'un CRUD :
use OpenApi\Attributes as OA; ... #[OA\Post( requestBody: new OA\RequestBody( required: true, content: [ new OA\JsonContent( examples: [ new OA\Examples('1', summary: 'By ID', value: '{ "myEntity": { "id": 1 }}'), new OA\Examples('2', summary: 'By name', value: '{ "myEntity": { "name": "TEST" }}'), ], type: 'object', ), ], ), tags: ['MyEntities'], responses: [ new OA\Response( response: Response::HTTP_OK, description: 'Returns myEntity information.', content: new OA\JsonContent( properties: [ new OA\Property( property: "id", type: "integer", example: 1, nullable: true ), new OA\Property( property: "name", type: "string", example: 'TEST', nullable: true ), ] ) ), new OA\Response( response: Response::HTTP_NOT_FOUND, description: 'Returns no myEntity.', content: new OA\JsonContent( properties: [ new OA\Property( property: "id", type: "integer", example: null, nullable: true ), ] ) ) ] )]
Sérialiseur
[modifier | modifier le wikicode]Enfin pour la sérialisation, on distingue plusieurs solutions :
- symfony/serializer, qui donne des contrôleurs
extends AbstractFOSRestController
et des méthodes aux annotations@Rest\Post()
[10]. - jms/serializer-bundle, avec des contrôleurs
extends RestController
et des méthodes aux annotations@ApiDoc()
. - Le service
fos_rest.service.serializer
.
symfony/serializer
[modifier | modifier le wikicode]composer require "symfony/serializer"
jms/serializer-bundle
[modifier | modifier le wikicode]composer require "jms/serializer-bundle"
Utilisation
[modifier | modifier le wikicode]Maintenant /api/doc affiche les méthodes des différents contrôleurs API. Voici un exemple :
<?php
namespace App\Controller;
use FOS\RestBundle\Controller\AbstractFOSRestController;
use FOS\RestBundle\View\View;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class APIController extends AbstractFOSRestController
{
#[Route('/api/test', methods: ['GET'])]
public function testAction(Request $request): View
{
return View::create('ok');
}
}
Dans PHP < 8
...
/**
* @Route("/api/test", methods={"GET"})
*/
public function testAction(Request $request): View
...
}
Maintenant dans /api/doc, cliquer sur /api/test, puis "Ty it out" pour exécuter la méthode de test.
Sécurité
[modifier | modifier le wikicode]Une API étant stateless, l'authentification est assurée à chaque requête, par l'envoi par le client d'un token JSON Web Token (JWT) dans l'en-tête HTTP (clé Authorization). Côté serveur, on transforme ensuite le JWT reçu en objet utilisateur pour accéder à l'identité du client au sein du code. Ceci est fait en configurant security.yaml, pour qu'un évènement firewall appelle automatiquement un guard authenticator[11] :
composer require "symfony/security-bundle"
security:
firewalls:
main:
guard:
authenticators:
- App\Security\TokenAuthenticator
Manipuler les JWT
[modifier | modifier le wikicode]Pour décrypter le JWT :
use Lexik\Bundle\JWTAuthenticationBundle\Encoder\JWTEncoderInterface; ... public function __construct( private readonly JWTEncoderInterface $jwtEncoder, ) { } public function decodeJwt(string $jwt): array { return $this->jwtEncoder->decode($jwt): }
Résultat minimum :
array:6 [ "iat" => 1724677672 "exp" => 1724764072 "roles" => array:1 [ 0 => "ROLE_INACTIF" ] "username" => "test" ]
Si on n'a pas besoin de vérifier le JWT (validité, expiration et signature modifiée par un pirate), on peut se passer de Lexik ainsi :
private function decodeJwt(string $jwt): array|bool|null { $tokenParts = explode('.', $jwt); if (empty($tokenParts[1])) { return []; } $tokenPayload = base64_decode($tokenParts[1]); return json_decode($tokenPayload, true); }
Test
[modifier | modifier le wikicode]Pour tester en shell :
TOKEN=123 curl -H 'Accept: application/json' -H "Authorization: Bearer ${TOKEN}" http://localhost
Références
[modifier | modifier le wikicode]- ↑ https://api-platform.com/
- ↑ https://api-platform.com/docs/core/content-negotiation/
- ↑ https://api-platform.com/docs/core/data-providers/
- ↑ https://api-platform.com/docs/core/data-persisters/
- ↑ https://api-platform.com/docs/core/operations/
- ↑ https://api-platform.com/docs/core/events/
- ↑ https://github.com/FriendsOfSymfony/FOSRestBundle
- ↑ https://github.com/nelmio/NelmioApiDocBundle
- ↑ https://www.postman.com/
- ↑ https://www.thinktocode.com/2018/03/26/symfony-4-rest-api-part-1-fosrestbundle/
- ↑ https://symfony.com/doc/current/security/guard_authentication.html