API Plateform : Améliorer la doc de votre API
Lors du précédent Symfony Pot du 25 février, nous vous avons présenté le framework API Platform dédié à la création d’API REST.

Lorsqu’on travaille en équipe ou pour faciliter la maintenance de son API, il est nécessaire de documenter les routes disponibles d’une API. La difficulté est que certaines de ces routes sont gérées par des dépendances externes pour lesquelles il n’est pas possible d’ajouter facilement des annotations ApiDoc. Par exemple pour une authentification à l’aide des bundles FOSUserBundle et LexikJWTAuthenticationBundle, la route login_check accessible n’est pas documentée et il n’est pas possible de facilement ajouter une annotation ApiDoc comme on le ferait sur nos propres controller. Comme nous avions pu le voir durant la présentation (https://github.com/acseo/my-running-planner/tree/5-document-external-dependencies), il est possible de créer son propre Provider d’annotations afin de fournir des annotations construites en dehors de ses controlleurs. Ce service peut être retrouvé ici : https://github.com/acseo/my-running-planner/blob/5-document-external-dependencies/src/ACSEO/Bundle/MyRunningPlannerBundle/Service/NelmioApiYmlProvider.php ou ci-dessous :
namespace ACSEOBundleMyRunningPlannerBundleService;
use NelmioApiDocBundleAnnotationApiDoc;
use NelmioApiDocBundleExtractorAnnotationsProviderInterface;
use SymfonyComponentFinderFinder;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentRoutingRoute;
use SymfonyComponentYamlYaml;
/**
* Generate annotations for vendor routes to be displayed in Nelmio ApiDoc.
*/
class NelmioApiYmlProvider implements AnnotationsProviderInterface
{
private $rootDir;
public function __construct($rootDir)
{
$this->rootDir = $rootDir;
}
/**
* {@inheritdoc}
*/
public function getAnnotations()
{
$annotations = [];
$configDirectories = array($this->rootDir.'/config/apidoc');
$finder = new Finder();
$finder->files()->in($configDirectories);
if (count($finder) == 0) {
throw new NotFoundHttpException('No file found for this protocol');
}
foreach ($finder as $file_) {
$data = Yaml::parse(file_get_contents($file_));
$vendors = array_keys($data);
foreach ($vendors as $vendor) {
$apiDoc = new ApiDoc($data[$vendor]);
$route = new Route(
$data[$vendor]['route']['path'],
$data[$vendor]['route']['defaults'],
$data[$vendor]['route']['requirements'],
$data[$vendor]['route']['options'],
$data[$vendor]['route']['host'],
$data[$vendor]['route']['schemes'],
$data[$vendor]['route']['methods'],
$data[$vendor]['route']['condition']
);
$apiDoc->setRoute($route);
$apiDoc->setResponse($data[$vendor]['response']);
$annotations[] = $apiDoc;
}
}
return $annotations;
}
}
et il est déclaré avec le tag nelmio_api_doc.extractor.annotations_provider qui lui permet d’être appelé par le Bundle ApiDoc.
<service id="nelmio_api_doc.yml_provider.acseo_yml_provider" class="%nelmio_api_doc.yml_provider.acseo_yml_provider.class%">
<argument>%kernel.root_dir%
<tag name="nelmio_api_doc.extractor.annotations_provider" />
</service>
On peut alors documenter une nouvelle route (par exemple le login_check) de la façon suivante :
// app/config/apidoc/lexik_jwt_authentication.yml
login_check:
requirements: []
views: []
filters: []
parameters:
_password:
dataType: string
required: true
name: _password
description: Password
readonly: false
_username:
dataType: string
required: true
name: _username
description: Username
readonly: false
input: null
output: null
link: null
description: "Get JWT token for logged user"
section: "Session"
documentation: null
resource: null
method: "POST"
host: ""
uri: "/login_check"
response:
token:
dataType: string
required: true
description: JWT token
readonly: true
route:
path: /login_check
defaults:
_controller: FOSUserBundleControllerSecurityController::checkAction
requirements: []
options:
compiler_class: SymfonyComponentRoutingRouteCompiler
host: ''
schemes: []
methods: [ 'POST' ]
condition: ''
https: false
authentication: false
authenticationRoles: []
cache: null
deprecated: false
statusCodes: []
resourceDescription: null
responseMap: []
parsedResponseMap: []
tags: []
Vous avez maintenant votre route login_check documentée et utilisable depuis la sandbox.
Utilisation du context hydra pour décrire ses routes API
L’url d’une route ne donne pas toujours autant d’informations qu’on le souhaiterait sur sa fonction. C’est pourquoi il est possible d’ajouter une description à la route via l’annotation “description” du Bundle de l’ApiDoc. Cette description est gérée nativement par le Bundle DunglasApiBundle qui utilise le titre hydra (hydra:title) pour description de la route. Notre problème ici est que seule la première action correspondant à une certaine méthode (par exemple GET) est utilisée pour lire le hydra:title (voir méthode getOperationHydraDoc ligne 170 à 177 : https://github.com/nelmio/NelmioApiDocBundle/blob/master/Extractor/AnnotationsProvider/DunglasApiProvider.php) Pour changer cela nous avons modifié cette méthode dans notre adaptation du Bundle : https://github.com/acseo/NelmioApiDocBundle/blob/master/Extractor/AnnotationsProvider/DunglasApiProvider.php
private function getOperationHydraDoc($method, array $hydraDoc, $context = null)
{
$contextTitle = isset($context['hydra:title']) ? $context['hydra:title'] : null;
foreach ($hydraDoc['hydra:supportedOperation'] as $supportedOperation) {
$operationTitle = isset($supportedOperation['hydra:title']) ? $supportedOperation['hydra:title'] : null;
if ($supportedOperation['hydra:method'] === $method and ($contextTitle == null or $contextTitle === $operationTitle)) {
return $supportedOperation;
}
}
}
et son appel ligne 108. Ainsi chaque route a sa propre description.
Utilisation du context hydra pour documenter les codes de retour
L’un des aspects d’une documentation est de fournir les codes de retours des appels afin de pouvoir comprendre les retours de l’API. Ceci est possible grâce à l’ApiDoc de nelmio avec l’annotation “statusCodes”, mais comme pour le paragraphe précédent, on ne peut pas utiliser ces annotations dans nos controller. On va donc utiliser le context hydra de la manière suivante :
resource.user.collection_operation.cpost:
class: "DunglasApiBundleApiOperationOperation"
public: false
factory: [ "@api.operation_factory", "createCollectionOperation" ]
arguments:
- "@resource.user"
- [ "POST" ]
- "/users"
- "AcseoCoreBundle:User:cpost"
- "api_users_post"
- # Context (will be present in Hydra documentation)
"@type": "hydra:Operation"
"hydra:title": "Creates a User Resource."
"returns": "xmls:string"
"statusCodes":
201: [ 'Resource created' ]
400: [ 'Data is invalid' ]
Et nous modifions notre adaptation du bundle de la façon suivante : https://github.com/acseo/NelmioApiDocBundle/blob/master/Extractor/AnnotationsProvider/DunglasApiProvider.php lignes 112 à 120.
private function getApiDoc(
...
)
...
$statusCodes = isset($operation->getContext()['statusCodes']) ? $operation->getContext()['statusCodes'] : [];
$data = [
'resource' => $route->getPath(),
'description' => $operationHydraDoc['hydra:title'],
'resourceDescription' => $resourceHydraDoc['hydra:title'],
'section' => $resourceHydraDoc['hydra:title'],
'statusCodes' => $statusCodes,
];
...
L’ApiDoc nous permet à présent de documenter les codes de retours de nos routes grâce au contexte hydra de nos web-services.
Conclusion
Au travers de cet article nous vous avons expliqué comment vous pouvez continuer d’utiliser ce que vous connaissiez de l’ApiDoc de Nelmio avec les codes de retours et la description des routes et plus encore avec la documentation des dépendances externes tout en profitant de la puissance d’API Platform. Retrouvez notre Bundle https://github.com/acseo/NelmioApiDocBundle