Dans cet article nous allons voir comment créer une API rest avec FOS/rest-bundle, avec une authentification, et un générateur de documentation de type swagger.

Liste des bundle nécessaire :
friendsofsymfony/rest-bundle : Fournis une série d’outils d’aide au développement d’une API restfull
https://github.com/FriendsOfSymfony/FOSRestBundle

jms/serializer-bundle : Permet la sérialisation d’objets.
https://packagist.org/packages/jms/serializer-bundle

lexik/jwt-authentication-bundle : Permet la gestion de token web en json.
https://github.com/lexik/LexikJWTAuthenticationBundle

nelmio/NelmioApiDocBundle : Permet de générer une documentation HTML à la swagger
https://github.com/nelmio/NelmioApiDocBundle

Installation dans composer

On part du principe que l’on utilise notre skeleton comme base de départ.

composer require "nelmio/cors-bundle"
composer require "lexik/jwt-authentication-bundle"
composer require "jms/serializer-bundle"
composer require "friendsofsymfony/rest-bundle"
composer require "nelmio/api-doc-bundle"
composer require "annotations"

Il faudra ensuite enregistrer nos bundles dans bundles.php

Nelmio\CorsBundle\NelmioCorsBundle::class => ['all' => true],
    Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true],
    FOS\RestBundle\FOSRestBundle::class => ['all' => true],
    Nelmio\ApiDocBundle\NelmioApiDocBundle::class => ['all' => true],

Pour la documentation, il faudra ajouter la route :

# config/routes.yaml
app.swagger_ui:
    path: /api/doc
    methods: GET
    defaults: { _controller: nelmio_api_doc.controller.swagger_ui }
    
app.swagger:
    path: /api/doc.json
    methods: GET
    defaults: { _controller: nelmio_api_doc.controller.swagger }

Créer la configuration pour la documentation :

# config/packages/nelmio_api_doc.yaml
nelmio_api_doc:
    areas:
        path_patterns: # an array of regexps
            - ^/api(?!/doc$)
        host_patterns:
            - ^api\.

Il faudra ensuite activer les converters de framework extra :

#/config/packages/sensio_framework_extra.yaml
sensio_framework_extra:
    router:
        annotations: false
    request: { converters: true }

Création de notre première méthode de l’API

On va d’abords créer notre controller

 bin/console make:controller api

Dans notre controller, nous devrons inclure notre librairie d’annotation de fosRestBundle pour y utiliser les bonnes annotations ainsi que les librairies NelmioApiDoc

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use FOS\RestBundle\Controller\Annotations\Get;
use JMS\Serializer\SerializationContext;
use Symfony\Component\HttpFoundation\Response;
use Swagger\Annotations as SWG;
use Nelmio\ApiDocBundle\Annotation\Model;
use Nelmio\ApiDocBundle\Annotation\Security;

class ApiController extends AbstractController
{

    /**
     * @Get(
     *     path = "/api/test/{id}",
     *     name = "api_test_id",
     *     requirements = {"id"="\d+"}
     * )
     */
    public function index()
    {
        $detail=['test'=>'value'];

        $serializer = $this->get('serializer');
        $response = new Response(
            $serializer->serialize(['detail' => $detail], 'json'),
            Response::HTTP_OK
            );
        $response->headers->set('Content-Type', 'application/json');
        
        return $response;
    }
}

Pour gérer les code de réponses, nous devrons utiliser les annotations suivantes :

     * @SWG\Response(
     *     response=200,
     *     @SWG\Schema(type="object",
     *         example={"foo": "bar", "hello": "world"}
     *     ),
     *     description="Response ok"
     * )
     * @SWG\Response(
     *     response=401,
     *     description="Access Denied"
     * ) 
     * @SWG\Response(
     *     response=403,
     *     description="Forbidden"
     * ) 
     * @SWG\Response(
     *     response=404,
     *     description="Not Found"
     * )
Sélection_207

Authentification de notre API

Pour l’authentification, on a choisi de faire simple. Pas une authentification par l’entête de type Bearer, mais juste une clef dans l’url.
C’est beaucoup plus simple à tester, surtout lorsque vous allez travailler avec des tiers, et que vous ne souhaitez pas faire du support dans tous les sens et former leurs stagiaire. Même si ajouter une clef dans l’entête, c’est franchement pas bien compliqué. bref.

Donc on va déclarer notre clef dans les annotations de notre méthode.

     * @SWG\Parameter(
     *     name="key",
     *     in="query",
     *     type="string",
     *     description="The authorization key"
     * )

Couplé a la définition de notre api cela donne ceci :

/**
 * @Get(
 *     path = "/api/test/{id}",
 *     name = "api_test_id",
 *     requirements = {"id"="\d+"}
 * )
 * 
 * 
 * @SWG\Parameter(
 *     name="key",
 *     in="query",
 *     type="string",
 *     description="The authorization key provided by HMF"
 * )

Et dans le swagger cela se matérialise comme cela :

image

Personnalisation de la documentation

La documentation présente quelques problèmes d’affichage du style, et notamment affiche un logo du bundle, alors que l’on souhaites afficher le logo du client.
Du coup, il suffit de surcharger le template en créant un fichier twig ici :
/templates/bundles/NelmioApiDocBundle/SwaggerUi/index.html.twig

{# templates/bundles/NelmioApiDocBundle/SwaggerUi/index.html.twig #}

{#
    To avoid a "reached nested level" error an exclamation mark `!` has to be added
    See https://symfony.com/blog/new-in-symfony-3-4-improved-the-overriding-of-templates
#}
{% extends '@!NelmioApiDoc/SwaggerUi/index.html.twig' %}

{% block stylesheets %}
    {{ parent() }}
    <link rel="stylesheet" href="{{ asset('css/custom-swagger-styles.css') }}">
    <style>
    header #logo img{
    	height: unset;
    }
    header::before{
	background-color: #FFFFFF;
    }
    .swagger-ui table tbody tr td, .response-col_description__inner{
    	padding: 0;
    	vertical-align: top;
    }
    .swagger-ui .markdown p{
		margin: 10px auto;
    }
</style>
{% endblock stylesheets %}

{% block javascripts %}
    {{ parent() }}
    <script type="text/javascript" src="{{ asset('js/custom-request-signer.js') }}"></script>
{% endblock javascripts %}


{% block header %}
    <a id="logo" href="#"><img src="{{ asset('images/logo.png') }}" alt="Hyundai"></a>
{% endblock header %}

On voudras aussi ne permettre que le json et le teste en https, car notre domaine est en https, et si l’utilisateur (le stagiaire), teste en http alors que la doc est en https, le navigateur bloquera la requête. C’est con, mais c’est du vécus 😉
On va lui mettre un titre, supprimer l’authentification Bearer, et supprimer la vérification par host.

# config/packages/nelmio_api_doc.yaml
nelmio_api_doc:
    areas:
        path_patterns: # an array of regexps
            - ^/api(?!/doc$)
#        host_patterns:
#            - ^api\.
#            
    documentation:
        host: api.hyundai.test
        #schemes: [http, https]
        schemes: [https]
        info:
            title: Hyundai API
            description: hyundai backend webservice
            version: 1.0.0
        #securityDefinitions:
        #    Bearer:
        #        type: apiKey
        #        description: 'Value: Bearer {jwt}'
        #        name: Authorization
        #        in: header
        #security:
        #    - Bearer: []

Et voila ! on est prêt à développer notre API.
Il nous resteras à créer un CRUD pour les clefs, et appliquer notre logiques de données à fournir pour chaque méthodes.