Améliorez la documentation de votre API basée sur API Platform

Améliorez la documentation de votre API basée sur API Platform

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. Vous pouvez retrouver cette présentation en suivant le lien : https://github.com/acseo/my-running-planner. En complément de cette présentation, nous vous proposons aujourd’hui d’aller un peu plus loin dans la documentation de cette API grâce à des ajouts au Bundle de Nelmio : NelmioApiDocBundle (disponible en suivant le lien : https://github.com/nelmio/NelmioApiDocBundle).

Documentation des dépendances externes

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

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