Generare la documentazione di una API RESTful con Swagger-PHP

Una web API per poter essere utilizzata correttamente deve essere ben documentata.

Pubblicato da ,
Ultima modifica

La libreria swagger-php consente di generare la documentazione di una web API usando le annotazioni o attributi, in questo articolo vedremo la sintassi delle annotazioni.

L'installazione si effettua tramite composer utilizzando il seguente comando da CLI

composer require zircote/swagger-php

se tutto è andato a buon fine verrà aggiunto alla chiave "require" del composer.json il nome del pacchetto appena installato

composer.json

{
  "require": {
    "zircote/swagger-php": "^4.7",
     .............................
     .............................
  }
}

la versione attuale di swagger-php è la 4.7.

L'esempio illustrato in questo articolo prevede una struttura di cartelle come la seguente

  • /path/www/myproject/
    • vendor/
    • src/
    • composer,json

"/path/www/myproject/" è la root del progetto in cui abbiamo installato swagger-php, src è la cartella dove salveremo tutte le nostre classi, se non abbiamo già impostata una chiave psr-4 nel composer.json, allora aggiungiamo alla chiave psr-4 il namespace da attribuire alla cartella src

composer.json

{
  "require": {
    "zircote/swagger-php": "^4.4"
  },
  "autoload": {
    "psr-4": {
      "App\\": "src/"
    }
  }
}

in questo esempio App corrisponde al namespace delle classi che definiremo nella cartella src.

Come abbiamo detto swagger-php utilizza annotazioni nel codice PHP, di seguito è riportato un esempio di una classe Articles

src/Articles.php

<?php

namespace App;

use OpenApi\Annotations as OA;

/**
 * @OA\Tag(
 *     name="API Example",
 *     description="API Example related operations"
 * )
 *
 * @OA\Info(
 *     version="1.0",
 *     title="API Documetation",
 *     description="API Documetation Example",
 *     @OA\Contact(name="API Team", email="info@example.it")
 * )
 *
 * @OA\Server(
 *     url="https://example.localhost",
 *     description="API server"
 * )
 *
 * @OA\Get(
 *     path="/api/articles/{id}",
 *     @OA\Parameter(
 *     name="id",
 *     in="path",
 *     required=true,
 *     @OA\Schema(
 *             type="integer"
 *         )
 * ),
 *     @OA\Response(
 *          response="200",
 *          description="Get article",
 *          @OA\MediaType(
 *                mediaType="application/json",
 *                @OA\Schema(ref="#/components/schemas/Article"),
 *   )
 *  ),
 *    @OA\Response(response="422", description="422 Unprocessable Entity"),
 *    @OA\Response(response="default", description="an ""unexpected"" error")
 * ),
 *
 *  @OA\Schema(
 *    schema="Article",
 *    type="object",
 *
 *   @OA\Property(
 *        property="id",
 *        description="ID article",
 *        type="integer",
 *        example=1
 *      ),
 *
 *    @OA\Property(
 *      property="title",
 *      type="string",
 *      example="Lorem Ipsum"
 *    ),
 *
 *    @OA\Property(
 *      property="created_at",
 *      type="string",
 *      example="2023-11-20 13:49:00"
 *     ),
 *
 *    @OA\Property(
 *        property="descrition",
 *        type="string",
 *        example="Aliquip adipisicing do aliquip eu officia non minim eu do amet laboris et consectetur est."
 *      )
 *    )
 */

class Articles
{
    //......
}

in questo esempio stiamo specificando diverse informazioni, un tag con descrizione, il titolo, la versione e la descrizione della documentazione, un'email di contatto

<?php

 /**
 * @OA\Tag(
 *     name="API Example",
 *     description="API Example related operations"
 * )
 *
 * @OA\Info(
 *     version="1.0",
 *     title="API Documetation",
 *     description="API Documetation Example",
 *     @OA\Contact(name="API Team", email="info@example.it")
 * )
 

un url al quale vengono inviate le richieste con relativa descrizione

<?php

/**
 * ...........
 * ...........
 *
 *
 * @OA\Server(
 *     url="https://example.localhost",
 *     description="API server"
 * )
 

una richiesta di tipo GET con un parametro id di tipo numerico che recupera un singolo articolo con un response 200, 422 e default

<?php

 /**
  * ...........
  * ...........
  *
  *
  * @OA\Get(
  *     path="/api/articles/{id}",
  *     @OA\Parameter(
  *     name="id",
  *     in="path",
  *     required=true,
  *     @OA\Schema(
  *             type="integer"
  *         )
  * ),
  *     @OA\Response(
  *          response="200",
  *          description="Get article",
  *          @OA\MediaType(
  *                mediaType="application/json",
  *                @OA\Schema(ref="#/components/schemas/Article"),
  *   )
  *  ),
  *    @OA\Response(response="422", description="422 Unprocessable Entity"),
  *    @OA\Response(response="default", description="an ""unexpected"" error")
  * )
  

in particolare per il tipo di response 200 è stato specificato un tipo ("application/json") e uno schema al quale è stato assegnato il nome Article

<?php

/**
 * ...........
 * ...........
 *
 *
 * @OA\MediaType(
 *    mediaType="application/json",
 *    @OA\Schema(ref="#/components/schemas/Article"),
 * )
 

nello schema sono definite le proprietà dell'oggetto json della risposta

<?php

/**
 * ...........
 * ...........
 *
 *
 *  @OA\Schema(
 *    schema="Article",
 *    type="object",
 *
 *   @OA\Property(
 *        property="id",
 *        description="ID article",
 *        type="integer",
 *        example=1
 *      ),
 *
 *    @OA\Property(
 *      property="title",
 *      type="string",
 *      example="Lorem Ipsum"
 *    ),
 *
 *    @OA\Property(
 *      property="created_at",
 *      type="string",
 *      example="2023-11-20 13:49:00"
 *     ),
 *
 *    @OA\Property(
 *        property="descrition",
 *        type="string",
 *        example="Aliquip adipisicing do aliquip eu officia non minim eu do amet laboris et consectetur est."
 *      )
 *    )
 

se necessario aggiorniamo l'autoload di composer tramite il comando

$ composer dump-autoload

Per generare il file openapi.yaml contenente la documentazione si eseguono i seguenti comandi da CLI

$ cd vendor/zircote/swagger-php/bin

e poi per generare il file .yaml

$ php openapi /path/www/myproject/src -o /path/www/myproject/openapi.yaml

specificando il percorso dove sono salvati i file sorgente, che in questo caso é la cartella src/ ("/path/www/myproject/src"), il percorso dove salvare il file openapi.yaml, che è la root del progetto ("/path/www/myproject/"), il file .yaml generato sarà simile a questo

openapi.yaml

openapi: 3.0.0
info:
  title: 'API Documetation'
  description: 'API Documetation Example'
  contact:
    name: 'API Team'
    email: info@example.it
  version: '1.0'
servers:
  -
    url: 'https://example.localhost'
    description: 'API server'
paths:
  '/api/articles/{id}':
    get:
      parameters:
        -
          name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: 'Get article'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Article'
        '422':
          description: '422 Unprocessable Entity'
        default:
          description: 'an "unexpected" error'
components:
  schemas:
    Article:
      properties:
        id:
          description: 'ID article'
          type: integer
          example: 1
        title:
          type: string
          example: 'Lorem Ipsum'
        created_at:
          type: string
          example: '2023-11-20 13:49:00'
        descrition:
          type: string
          example: 'Aliquip adipisicing do aliquip eu officia non minim eu do amet laboris et consectetur est.'
      type: object
tags:
  -
    name: 'API Example'
    description: 'API Example related operations'

è anche possibile generare un file in formato .json invece del formato .yaml

$ php openapi /path/www/myproject/src -o /path/www/myproject/openapi.json

il risultato sarà un file .json simile a questo

openapi.json

{
    "openapi": "3.0.0",
    "info": {
        "title": "API Documetation",
        "description": "API Documetation Example",
        "contact": {
            "name": "API Team",
            "email": "info@example.it"
        },
        "version": "1.0"
    },
    "servers": [
        {
            "url": "https://example.localhost",
            "description": "API server"
        }
    ],
    "paths": {
        "/api/articles/{id}": {
            "get": {
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "integer"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "Get article",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Article"
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "422 Unprocessable Entity"
                    },
                    "default": {
                        "description": "an \"unexpected\" error"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "Article": {
                "properties": {
                    "id": {
                        "description": "ID article",
                        "type": "integer",
                        "example": 1
                    },
                    "title": {
                        "type": "string",
                        "example": "Lorem Ipsum"
                    },
                    "created_at": {
                        "type": "string",
                        "example": "2023-11-20 13:49:00"
                    },
                    "descrition": {
                        "type": "string",
                        "example": "Aliquip adipisicing do aliquip eu officia non minim eu do amet laboris et consectetur est."
                    }
                },
                "type": "object"
            }
        }
    },
    "tags": [
        {
            "name": "API Example",
            "description": "API Example related operations"
        }
    ]
}

Fin qui abbiamo visto come utilizzare swagger-php per definire una richiesta di tipo GET, vediamo adesso come gestire una richiesta POST per aggiungere un nuovo articolo con parametri nella richiesta stessa.

Aggiungiamo alla classe Articles la seguente annotazione

<?php

 /**
  * ...........
  * ...........
  *
  *
  * @OA\Post(
  *      path="/api/articles/add",
  *      description="Add new article",
  *      @OA\RequestBody(
  *         @OA\MediaType(
  *             mediaType="application/json",
  *             @OA\Schema(ref="#/components/schemas/AddNewArticle"),
  *   ) 
  *  ),
  *  @OA\Response(response="200", description="OK"),
  *  @OA\Response(response="default", description="an ""unexpected"" error")
  * ),
  

con questa annotazione abbiamo specificato il tipo di richiesta POST, il path ("/api/articles/add"), il tipo del corpo della richiesta stessa ("application/json") e uno schema al quale abbiamo assegnato il nome AddNewArticle, inoltre abbiamo specificato un response 200 e default, l'annotazione per lo schema AddNewArticle è la seguente

<?php

 /**
  * ...........
  * ...........
  *
  *
  *  @OA\Schema(
  *    schema="AddNewArticle",
  *    type="object",
  *
  *    @OA\Property(
  *      property="title",
  *      type="string",
  *      example="Lorem Ipsum"
  *    ),
  *
  *    @OA\Property(
  *        property="descrition",
  *        type="string",
  *        example="Aliquip adipisicing do aliquip eu officia non minim eu do amet laboris et consectetur est."
  *  )
  * )
  

eseguendo il comando per aggionare il file .yaml avremo il seguente risultato

openapi: 3.0.0
info:
  title: 'API Documetation'
  description: 'API Documetation Example'
  contact:
    name: 'API Team'
    email: info@example.it
  version: '1.0'
servers:
  -
    url: 'https://example.localhost'
    description: 'API server'
paths:
  '/api/articles/{id}':
    get:
      parameters:
        -
          name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: 'Get article'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Article'
        '422':
          description: '422 Unprocessable Entity'
        default:
          description: 'an "unexpected" error'
  /api/articles/add:
    post:
      description: 'Add new article'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AddNewArticle'
      responses:
        '200':
          description: OK
        default:
          description: 'an "unexpected" error'
components:
  schemas:
    Article:
      properties:
        id:
          description: 'ID article'
          type: integer
          example: 1
        title:
          type: string
          example: 'Lorem Ipsum'
        created_at:
          type: string
          example: '2023-11-20 13:49:00'
        descrition:
          type: string
          example: 'Aliquip adipisicing do aliquip eu officia non minim eu do amet laboris et consectetur est.'
      type: object
    AddNewArticle:
      properties:
        title:
          type: string
          example: 'Lorem Ipsum'
        descrition:
          type: string
          example: 'Aliquip adipisicing do aliquip eu officia non minim eu do amet laboris et consectetur est.'
      type: object
tags:
  -
    name: 'API Example'
    description: 'API Example related operations'

Infine vediamo come aggiungere un token JWT nella richesta POST appena creata usando le annotazioni.

Aggiungiamo alla classe Articles la seguente annotazione per definire un SecurityScheme al quale assegnamo il nome bearerAuth

<?php

 /**
  * ...........
  * ...........
  *
  *
  * @OA\SecurityScheme(
  *    securityScheme="bearerAuth",
  *    type="http",
  *    scheme="bearer",
  *    bearerFormat="JWT",
  * )

e modifichiamo la richiesta POST aggiungendo la chiave security in modo che nell'intestazione ci sia un token JWT

<?php

/**
 * ...........
 * ...........
 *
 * 
 * @OA\Post(
 *      path="/api/articles/add",
 *      description="Add new article",
 *      security={
 *           {"bearerAuth": {}}
 *       },
 *      @OA\RequestBody(
 *         @OA\MediaType(
 *             mediaType="application/json",
 *             @OA\Schema(ref="#/components/schemas/AddNewArticle"),
 *   ) 
 *  ),
 *  @OA\Response(response="200", description="OK"),
 *  @OA\Response(response="default", description="an ""unexpected"" error")
 * ),


aggiornando il file .yaml sempre con lo stesso comando da CLI avremo il seguente risultato

openapi: 3.0.0
info:
  title: 'API Documetation'
  description: 'API Documetation Example'
  contact:
    name: 'API Team'
    email: info@example.it
  version: '1.0'
servers:
  -
    url: 'https://example.localhost'
    description: 'API server'
paths:
  '/api/articles/{id}':
    get:
      parameters:
        -
          name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: 'Get article'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Article'
        '422':
          description: '422 Unprocessable Entity'
        default:
          description: 'an "unexpected" error'
  /api/articles/add:
    post:
      description: 'Add new article'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AddNewArticle'
      responses:
        '200':
          description: OK
        default:
          description: 'an "unexpected" error'
      security:
        -
          bearerAuth: []
components:
  schemas:
    Article:
      properties:
        id:
          description: 'ID article'
          type: integer
          example: 1
        title:
          type: string
          example: 'Lorem Ipsum'
        created_at:
          type: string
          example: '2023-11-20 13:49:00'
        descrition:
          type: string
          example: 'Aliquip adipisicing do aliquip eu officia non minim eu do amet laboris et consectetur est.'
      type: object
    AddNewArticle:
      properties:
        title:
          type: string
          example: 'Lorem Ipsum'
        descrition:
          type: string
          example: 'Aliquip adipisicing do aliquip eu officia non minim eu do amet laboris et consectetur est.'
      type: object
  securitySchemes:
    bearerAuth:
      type: http
      bearerFormat: JWT
      scheme: bearer
tags:
  -
    name: 'API Example'
    description: 'API Example related operations'

quando avremo generato l'intera documentazione è possibile fare dei test su Swagger Editor inserendo il contenuto del file openapi.yaml.