Every bit a office of our QuickAdminPanel, we likewise generate APIs. Just your goal is also to provide documentation for forepart-end who would consume that API, right? OpenAPI (ex. Swagger) is a well-known standard for that. How to use it to a Laravel projection?


In this article, we will have these sections:

  1. Short Intro: What is OpenAPI and How Does information technology Work?
  2. Grooming: Initial Laravel API Lawmaking
  3. Installing Laravel Swagger Package
  4. Writing Comments and Generating Documentation

At the stop of the commodity, y'all will find a link to Github repository example.


Short Intro: What is OpenAPI and How Does it Work?

First, a few words about what OpenAPI/Swagger is.

Formerly called Swagger (quite oft chosen this even now), OpenAPI is a standard of documenting APIs. Its specification is available on Github here.

The official definition from their homepage: "The OpenAPI Specification: a broadly adopted manufacture standard for describing modern APIs."

Keep in heed that it's not a Laravel API standard. Not even PHP linguistic communication standard. Information technology'southward API schema that can be used for whatever programming linguistic communication. It's like a ready of rules that tin can be adapted to your framework.

For Laravel specifically, in that location were a few packages created, and we will utilize ane of them: DarkaOnLine/L5-Swagger

Allow's take a look at the end consequence – here'southward the documentation folio that will be generated automatically from your code comments:

Laravel OpenAPI Swagger Documentation

Inside this folio, you tin can click on items to expand them and get more than data.

And all of it because y'all have written some comments, like these:

grade ProjectsApiController extends Controller {     /**      * @OA\Go(      *      path="/projects",      *      operationId="getProjectsList",      *      tags={"Projects"},      *      summary="Get list of projects",      *      description="Returns list of projects",      *      @OA\Response(      *          response=200,      *          description="Successful functioning",      *          @OA\JsonContent(ref="#/components/schemas/ProjectResource")      *       ),      *      @OA\Response(      *          response=401,      *          description="Unauthenticated",      *      ),      *      @OA\Response(      *          response=403,      *          clarification="Forbidden"      *      )      *     )      */     public function index()     {         abort_if(Gate::denies('project_access'), Response::HTTP_FORBIDDEN, '403 Forbidden');          render new ProjectResource(Project::with(['writer'])->get());     }        

And then, this is a short overview, now let's get deeper and bear witness you how to generate the documentation step-by-step.


Preparation: Initial Laravel API Code

First, I will show the base code of API structure, it may be useful to learn even if you're not planning to generate documentation.

Imagine y'all accept a model Project and all API activeness for information technology: index, shop, update, show, destroy.

So, hither's routes/api.php:

Route::group([   'prefix' => 'v1',    'as' => 'api.',    'namespace' => 'Api\V1\Admin',    'middleware' => ['auth:api'] ], function () {     Road::apiResource('projects', 'ProjectsApiController'); });        

In this instance, we put the ProjectsApiController within of V1/Admin subfolder.

So here's the code of our app/Http/Controllers/V1/Admin/ProjectsApiController.php:

namespace App\Http\Controllers\Api\V1\Admin;  use App\Http\Controllers\Controller; employ App\Http\Requests\StoreProjectRequest; use App\Http\Requests\UpdateProjectRequest; use App\Http\Resources\Admin\ProjectResource; utilise App\Project; use Gate; use Illuminate\Http\Asking; use Symfony\Component\HttpFoundation\Response;  class ProjectsApiController extends Controller {     public part index()     {         abort_if(Gate::denies('project_access'), Response::HTTP_FORBIDDEN, '403 Forbidden');          return new ProjectResource(Project::with(['author'])->get());     }      public office store(StoreProjectRequest $request)     {         $projection = Project::create($request->all());          return (new ProjectResource($project))             ->response()             ->setStatusCode(Response::HTTP_CREATED);     }      public function show(Project $projection)     {         abort_if(Gate::denies('project_show'), Response::HTTP_FORBIDDEN, '403 Forbidden');          return new ProjectResource($project->load(['author']));     }      public role update(UpdateProjectRequest $request, Project $project)     {         $project->update($request->all());          return (new ProjectResource($projection))             ->response()             ->setStatusCode(Response::HTTP_ACCEPTED);     }      public function destroy(Projection $project)     {         abort_if(Gate::denies('project_delete'), Response::HTTP_FORBIDDEN, '403 Forbidden');          $project->delete();          return response(null, Response::HTTP_NO_CONTENT);     } }        

Hither are a few additional things we demand to mention:

  • We use Laravel Gates to restrict the admission;
  • We employ API Resources to return the response data;
  • Nosotros utilise Form Request classes to validate the input data.

Here'due south ane of those files – app/Http/Resource/Admin/ProjectResource.php:

namespace App\Http\Resources\Admin;  use Illuminate\Http\Resources\Json\JsonResource;  class ProjectResource extends JsonResource {     public role toArray($request)     {         return parent::toArray($request);     } }        

Also, for validation – app/Http/Requests/StoreProjectRequest.php:

namespace App\Http\Requests;  use Gate; use Illuminate\Foundation\Http\FormRequest; utilize Symfony\Component\HttpFoundation\Response;  course StoreProjectRequest extends FormRequest {     public office authorize()     {         abort_if(Gate::denies('project_create'), Response::HTTP_FORBIDDEN, '403 Forbidden');         render truthful;     }      public function rules()     {         render [             'proper name' => [                 'required',             ],         ];     } }        

Then, here's our beginning. Now, permit's kickoff generating the documentation with OpenAPI.


Installing Laravel Swagger Package

1 of the nearly popular packages to generate OpenAPI documentation in Laravel is DarkaOnLine/L5-Swagger.

Don't be dislocated by the proper name – it didn't modify the name role of Swagger to OpenAPI, just it actually supports both standards. Also "L5" in the name is non of import either – current Laravel 6 is supported well.

So, we install the bundle:

composer require darkaonline/l5-swagger        

Next, following installation instructions in Readme, we publish config/views from Service Provider:

php artisan vendor:publish --provider "L5Swagger\L5SwaggerServiceProvider"        

Finally, we have config/l5-swagger.php file with huge amount of options:

           [         /*         |--------------------------------------------------------------------------         | Edit to set the api's championship         |--------------------------------------------------------------------------         */          'title' => 'L5 Swagger UI',     ],      'routes' => [         /*         |--------------------------------------------------------------------------         | Road for accessing api documentation interface         |--------------------------------------------------------------------------         */          'api' => 'api/documentation',          // ...      ],      // ... many more than options      /*     |--------------------------------------------------------------------------     | Uncomment to add constants which tin can be used in annotations     |--------------------------------------------------------------------------      */     'constants' => [         'L5_SWAGGER_CONST_HOST' => env('L5_SWAGGER_CONST_HOST', 'http://my-default-host.com'),     ], ];        

For this example, we will edit the title from 'L5 Swagger UI' to 'Projects API', and add this into .env file:

L5_SWAGGER_CONST_HOST=http://project.test/api/v1        

And so we should launch this magic command:

php artisan l5-swagger:generate        

It should generate a JSON file, which and then will exist transformed into HTML page.

Just, for at present, it won't generate anything, cause we haven't added whatsoever comments anywhere. Shall nosotros?


Writing Comments and Generating Documentation

This is probably the main office of this article – rules on how to write those comments and where exactly.
The package will scan all your files and await for the patterns of OpenAPI-related comments.

Then, what types of comments nosotros need to add together?

  • Global: Project comments
  • Local: Controller/Method comments
  • Virtual: Models, Validation and Response comments

I volition just listing the comments here, for more data on their logic please refer to the short examples inside of the Laravel package, or to the detailed OpenAPI official specification.


Comment Type 1: Global

We demand to provide the data about the whole project, and for that, we create a carve up file – an empty Controller which wouldn't even be used anywhere – app/Http/Controllers/Api/Controller.php:

class Controller {     /**      * @OA\Info(      *      version="ane.0.0",      *      title="Laravel OpenApi Demo Documentation",      *      description="L5 Swagger OpenApi description",      *      @OA\Contact(      *          email="admin@admin.com"      *      ),      *      @OA\License(      *          proper noun="Apache ii.0",      *          url="http://www.apache.org/licenses/LICENSE-2.0.html"      *      )      * )      *      * @OA\Server(      *      url=L5_SWAGGER_CONST_HOST,      *      description="Demo API Server"      * )       *      * @OA\Tag(      *     proper noun="Projects",      *     description="API Endpoints of Projects"      * )      */ }

These variables will help generate the principal information in documentation folio header:


Annotate Type 2: Controllers Methods

To describe every API endpoint, we need to add together comments annotations on top of every method in API Controllers.

So here's a full example of our app/Http/Controllers/Api/V1/Admin/ProjectsApiController.php:

class ProjectsApiController extends Controller {     /**      * @OA\Become(      *      path="/projects",      *      operationId="getProjectsList",      *      tags={"Projects"},      *      summary="Become listing of projects",      *      description="Returns list of projects",      *      @OA\Response(      *          response=200,      *          description="Successful operation",      *          @OA\JsonContent(ref="#/components/schemas/ProjectResource")      *       ),      *      @OA\Response(      *          response=401,      *          description="Unauthenticated",      *      ),      *      @OA\Response(      *          response=403,      *          description="Forbidden"      *      )      *     )      */     public office alphabetize()     {         abort_if(Gate::denies('project_access'), Response::HTTP_FORBIDDEN, '403 Forbidden');          return new ProjectResource(Project::with(['writer'])->get());     }      /**      * @OA\Post(      *      path="/projects",      *      operationId="storeProject",      *      tags={"Projects"},      *      summary="Store new projection",      *      description="Returns project data",      *      @OA\RequestBody(      *          required=true,      *          @OA\JsonContent(ref="#/components/schemas/StoreProjectRequest")      *      ),      *      @OA\Response(      *          response=201,      *          description="Successful operation",      *          @OA\JsonContent(ref="#/components/schemas/Projection")      *       ),      *      @OA\Response(      *          response=400,      *          description="Bad Asking"      *      ),      *      @OA\Response(      *          response=401,      *          clarification="Unauthenticated",      *      ),      *      @OA\Response(      *          response=403,      *          description="Forbidden"      *      )      * )      */     public function store(StoreProjectRequest $request)     {         $project = Project::create($request->all());          return (new ProjectResource($projection))             ->response()             ->setStatusCode(Response::HTTP_CREATED);     }      /**      * @OA\Become(      *      path="/projects/{id}",      *      operationId="getProjectById",      *      tags={"Projects"},      *      summary="Get project information",      *      clarification="Returns project data",      *      @OA\Parameter(      *          name="id",      *          clarification="Projection id",      *          required=true,      *          in="path",      *          @OA\Schema(      *              blazon="integer"      *          )      *      ),      *      @OA\Response(      *          response=200,      *          clarification="Successful operation",      *          @OA\JsonContent(ref="#/components/schemas/Project")      *       ),      *      @OA\Response(      *          response=400,      *          description="Bad Request"      *      ),      *      @OA\Response(      *          response=401,      *          description="Unauthenticated",      *      ),      *      @OA\Response(      *          response=403,      *          description="Forbidden"      *      )      * )      */     public role testify(Project $projection)     {         abort_if(Gate::denies('project_show'), Response::HTTP_FORBIDDEN, '403 Forbidden');          return new ProjectResource($project->load(['author']));     }      /**      * @OA\Put(      *      path="/projects/{id}",      *      operationId="updateProject",      *      tags={"Projects"},      *      summary="Update existing project",      *      description="Returns updated project information",      *      @OA\Parameter(      *          name="id",      *          description="Project id",      *          required=true,      *          in="path",      *          @OA\Schema(      *              blazon="integer"      *          )      *      ),      *      @OA\RequestBody(      *          required=true,      *          @OA\JsonContent(ref="#/components/schemas/UpdateProjectRequest")      *      ),      *      @OA\Response(      *          response=202,      *          description="Successful operation",      *          @OA\JsonContent(ref="#/components/schemas/Project")      *       ),      *      @OA\Response(      *          response=400,      *          description="Bad Request"      *      ),      *      @OA\Response(      *          response=401,      *          description="Unauthenticated",      *      ),      *      @OA\Response(      *          response=403,      *          clarification="Forbidden"      *      ),      *      @OA\Response(      *          response=404,      *          clarification="Resource Not Institute"      *      )      * )      */     public role update(UpdateProjectRequest $request, Project $project)     {         $project->update($request->all());          render (new ProjectResource($project))             ->response()             ->setStatusCode(Response::HTTP_ACCEPTED);     }      /**      * @OA\Delete(      *      path="/projects/{id}",      *      operationId="deleteProject",      *      tags={"Projects"},      *      summary="Delete existing project",      *      description="Deletes a tape and returns no content",      *      @OA\Parameter(      *          name="id",      *          description="Projection id",      *          required=true,      *          in="path",      *          @OA\Schema(      *              type="integer"      *          )      *      ),      *      @OA\Response(      *          response=204,      *          clarification="Successful operation",      *          @OA\JsonContent()      *       ),      *      @OA\Response(      *          response=401,      *          clarification="Unauthenticated",      *      ),      *      @OA\Response(      *          response=403,      *          clarification="Forbidden"      *      ),      *      @OA\Response(      *          response=404,      *          description="Resources Not Found"      *      )      * )      */     public function destroy(Project $projection)     {         abort_if(Gate::denies('project_delete'), Response::HTTP_FORBIDDEN, '403 Forbidden');          $project->delete();          render response(null, Response::HTTP_NO_CONTENT);     } }        

Wow, it feels similar A LOT of comments, right?
But that's the right fashion of preparing the documentation – y'all need to describe all the methods, all the cases, all the parameters, all the exceptions.


Comment Blazon 3: Model, Validation and Response

You may have noticed some references to external files in the comments in the Controller above. So what is StoreProjectRequest there? We define all of those rules in our folder chosen app/Virtual, see the list of files:

Allow'due south take a expect within of app/Virtual/Models/Projection.php:

/**  * @OA\Schema(  *     championship="Project",  *     description="Project model",  *     @OA\Xml(  *         name="Project"  *     )  * )  */ class Projection {      /**      * @OA\Property(      *     championship="ID",      *     description="ID",      *     format="int64",      *     example=i      * )      *      * @var integer      */     private $id;      /**      * @OA\Property(      *      championship="Name",      *      clarification="Name of the new project",      *      example="A dainty project"      * )      *      * @var string      */     public $name;      /**      * @OA\Belongings(      *      title="Description",      *      clarification="Description of the new project",      *      example="This is new project's description"      * )      *      * @var string      */     public $description;      /**      * @OA\Belongings(      *     title="Created at",      *     description="Created at",      *     example="2020-01-27 17:50:45",      *     format="datetime",      *     type="string"      * )      *      * @var \DateTime      */     private $created_at;      /**      * @OA\Property(      *     title="Updated at",      *     clarification="Updated at",      *     example="2020-01-27 17:50:45",      *     format="datetime",      *     type="string"      * )      *      * @var \DateTime      */     private $updated_at;      /**      * @OA\Holding(      *     title="Deleted at",      *     description="Deleted at",      *     example="2020-01-27 17:50:45",      *     format="datetime",      *     blazon="string"      * )      *      * @var \DateTime      */     private $deleted_at;      /**      * @OA\Holding(      *      championship="Writer ID",      *      description="Author's id of the new project",      *      format="int64",      *      example=one      * )      *      * @var integer      */     public $author_id;       /**      * @OA\Property(      *     title="Author",      *     description="Projection author'due south user model"      * )      *      * @var \App\Virtual\Models\User      */     individual $author; }        

See, nosotros need to define every property of that Projection model, including relationship to the author.

At present, what near form validation requests? Run across app/Virtual/StoreProjectRequest.php:

/**  * @OA\Schema(  *      title="Store Project request",  *      description="Store Project request torso data",  *      type="object",  *      required={"name"}  * )  */  class StoreProjectRequest {     /**      * @OA\Property(      *      title="name",      *      description="Name of the new project",      *      case="A nice project"      * )      *      * @var cord      */     public $proper name;      /**      * @OA\Property(      *      title="description",      *      description="Clarification of the new projection",      *      example="This is new project'southward clarification"      * )      *      * @var cord      */     public $description;      /**      * @OA\Property(      *      championship="author_id",      *      description="Author's id of the new project",      *      format="int64",      *      example=one      * )      *      * @var integer      */     public $author_id; }        

Almost a copy-paste, right?

Finally, nosotros demand to define API Resources, which would exist "data" in our case – in app/Virtual/Resources/ProjectResource.php:

/**  * @OA\Schema(  *     championship="ProjectResource",  *     description="Project resource",  *     @OA\Xml(  *         name="ProjectResource"  *     )  * )  */ class ProjectResource {     /**      * @OA\Property(      *     title="Information",      *     description="Information wrapper"      * )      *      * @var \App\Virtual\Models\Project[]      */     private $information; }        

And, that's finally it! Nosotros can run this artisan command once again:

php artisan l5-swagger:generate        

Ta-daaa!

Laravel OpenAPI Swagger Documentation

And if you click on whatsoever endpoint, it expands with all the parameters you provided, and even with example response – that'southward the biggest dazzler:

Finally, a nice small matter on this documentation page is that you can click "Endeavour information technology out" (see elevation-right of the screenshot above) and it would attempt to actually run that API call. But keep in mind that you lot accept to be authenticated exactly as your API requests.


Conclusion and "Quicker" Culling to OpenAPI

If you lot are anything like me equally I was reading nearly OpenAPI the first time, it's a huge amount of information to take in. And it seems like a hell of a lot of work just to generate the documentation, correct?

But if you're dealing with a larger project, yous would inevitably accept to spend a lot of fourth dimension on documentation, and that OpenAPI is a standard for near anybody these days, so it's useful to invest in information technology.

Information technology wouldn't be so applicative to smaller projects, may even sound an overkill. So for those, I would recommend some other Laravel parcel, which requires less amount of comments to generate roughly similar documentation HTML: mpociot/laravel-apidoc-generator

Equally promised, Github repository for this article is here: LaravelDaily/Laravel-OpenAPI-Swagger-Documentation-Example