In this article, we will see how to create a REST API with FOS/RestBundle, including authentication, and a Swagger-like documentation generator.

List of necessary bundles:
friendsofsymfony/rest-bundle: Provides a set of tools to help develop a RESTful API
https://github.com/FriendsOfSymfony/FOSRestBundle

jms/serializer-bundle: Allows object serialization.
https://packagist.org/packages/jms/serializer-bundle

lexik/jwt-authentication-bundle: Manages JSON web tokens.
https://github.com/lexik/LexikJWTAuthenticationBundle

nelmio/NelmioApiDocBundle: Generates Swagger-like HTML documentation
https://github.com/nelmio/NelmioApiDocBundle

Installation in composer

We will assume that we are using our skeleton as a starting point.

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"

Then we will need to register our bundles in 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],

For documentation, the route will need to be added:

# 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 }

Create the configuration for the documentation:

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

Then, we must activate the extra framework converters:

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

Creation of our first API method

First, we will create our controller

 bin/console make:controller api

In our controller, we must include our FOSRestBundle's annotation library to use the right annotations, as well as NelmioApiDoc libraries

<?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;
    }
}

To manage response codes, we will have to use the following annotations:

     * @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

Authentication of our API

For authentication, we chose to keep it simple. Not a Bearer-type header authentication, but just a key in the URL.
It's much simpler to test, especially when you are working with third parties, and you don't want to provide support everywhere and train their interns. Even though adding a key in the header is honestly not that complicated. Anyway.

So we will declare our key in the annotations of our method.

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

Combined with our API definition, it looks like this:

/**
 * @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"
 * )

And in Swagger, it will materialize like this:

image

Customization of the documentation

The documentation has some display issues, particularly with style, and it displays a bundle logo when we want to display the client's logo instead.
So, we simply need to override the template by creating a Twig file here:
/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 %}

We also want to allow only JSON and testing in HTTPS, as our domain is in HTTPS, and if the user (the intern) tests in HTTP while the documentation is in HTTPS, the browser will block the request. It's silly, but it's a real-world issue. 😊
We will also set a title, remove the Bearer authentication, and eliminate host verification.

# 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: []

And there you have it! We are ready to develop our API.
What remains is to create a CRUD for the keys and apply our logic for the data to provide for each method.