Generating OpenAPI docs for Laravel with Swagger-PHP
API Code-first is the art of building an API, and then popping some annotations or metadata in there to output API documentation in an API description format like OpenAPI.
The most popular API Code-first approach in Laravel uses a tool called Swagger-PHP. With Swagger-PHP you write the OpenAPI for each API endpoint as annotations or PHP attributes, keeping the API description close to the code that it’s describing.
Creating OpenAPI with Swagger-PHP #
Step 1: Install the zircote/swagger-php Composer dependency.
composer require zircote/swagger-php
Step 2: Add some annotations to your controllers.
Sprinkle metadata around your controllers explaining their paths, responses, and adding some descriptions that will help people understand how things work when the documentation is built.
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\{WidgetCollection, WidgetResource};
use App\Models\Widget;
use Illuminate\Http\{Request, Response};
use OpenApi\Attributes as OA;
class WidgetController extends Controller
{
#[OA\Get(path: '/api/widgets', description: 'Display a collection of widgets.')]
#[OA\Response(response: Response::HTTP_OK, description: 'OK')]
public function index()
{
$widgets = Widget::all();
return new WidgetCollection($widgets);
}
#[OA\Get(path: '/api/widgets/{id}', description: 'Display the specified widget.')]
#[OA\Response(response: Response::HTTP_OK, description: 'OK')]
public function show(Widget $widget)
{
return new WidgetResource($widget);
}
#[OA\Post(path: '/api/widgets', description: 'Created a new widget.')]
#[OA\Response(response: Response::HTTP_CREATED, description: 'Created')]
public function store(Request $request)
{
$widget = Widget::create([
'name' => $request->post('name'),
'description' => $request->post('description'),
]);
return response()->json(new WidgetResource($widget), Response::HTTP_CREATED);
}
#[OA\Put(path: '/api/widgets/{id}', description: 'Update the specified widget by replacing all properties.')]
#[OA\Response(response: Response::HTTP_OK, description: 'OK')]
public function update(Request $request, Widget $widget)
{
$widget->update([
'name' => $request->post('name'),
'description' => $request->post('description'),
]);
return response()->json(new WidgetResource($widget), Response::HTTP_OK);
}
#[OA\Delete(path: '/api/widgets/{id}', description: 'Delete the specified widget entirely.')]
#[OA\Response(response: Response::HTTP_NO_CONTENT, description: 'Success')]
public function destroy(Widget $widget)
{
$widget->delete();
return response()->noContent(Response::HTTP_NO_CONTENT);
}
}
This metadata uses the PHP 8 Attributes syntax, but there is a docblock based syntax if you’re stuck on an older version of PHP.
Step 3: Export the OpenAPI from your source code.
$ vendor/bin/openapi src -o openapi.yaml
This outputs a good chunk of OpenAPI for you, so lets take a look and sew what we’ve got.
# openapi.yaml
openapi: 3.0.0
info:
title: 'My Widget API'
version: 1.0.0
paths:
/api/:
get:
description: 'The home resource shows you what can be done with the API.'
operationId: 14776015d6f5a2e7b4d4eb64c7ae2f1f
responses:
'200':
description: OK
/api/widgets:
get:
description: 'Display a collection of widgets.'
operationId: 3f6fb451248ef3559606c43b021abfcd
responses:
'200':
description: OK
post:
description: 'Created a new widget.'
operationId: 87fc629a5099009546b979bfbcb47dd2
responses:
'201':
description: Created
'/api/widgets/{id}':
get:
description: 'Display the specified widget.'
operationId: 12dd67833d3a2d1036437661a4cd8f07
responses:
'200':
description: OK
put:
description: 'Update the specified widget by replacing all properties.'
operationId: 256916253053a8ae669607bc6cc61e63
responses:
'200':
description: OK
delete:
description: 'Delete the specified widget entirely.'
operationId: b1915b538ae8a9a211fe476ed8219673
responses:
'204':
description: Success
Step 4: OpenAPI being generated means we can deploy it to Bump.sh. # Deploying OpenAPI documentation to Bump.sh. Create and name your first API documentation. Then, retrieve the name and token of this documentation from the CI deployment settings page.
Step 5: Install the Bump.sh CLI with npm.
npm install -g bump-cli
Step 6: The moment you’ve been waiting for, deploying your OpenAPI to Bump.sh to generated beautiful hosted documentation!
$ bump deploy openapi.yaml \
--doc laravel-code-first \
--token my-documentation-token
* Your new documentation version will soon be ready at https://bump.sh/bump-examples/hub/code-samples/doc/laravel-code-first
Step 7: Head over to your documentation and see how it looks at this early stage.
It looks like a start, but there is so much more we can do, including explaining responses, including longer descriptions, all of which is crucial to making API documentation useful, relevant, and readable. Time to learn a bit more about Swagger-PHP Annotations and get more data into our docs.
For the next trick, let’s add parameters and multiple responses to this controller method.
# app/Http/Controllers/Api/WidgetController.php
#[OA\Get(path: '/api/widgets/{id}', description: 'Display the specified widget.')]
#[OA\Parameter(in: "path", name: "id", required: true, schema: new OA\Schema(type: 'string'))]
#[OA\Response(response: Response::HTTP_OK, description: 'OK')]
#[OA\Response(response: Response::HTTP_NOT_FOUND, description: 'Not found')]
public function show(Widget $widget)
{
return new WidgetResource($widget);
}
This will improve our generated OpenAPI:
'/api/widgets/{id}':
get:
description: 'Display the specified widget.'
operationId: 12dd67833d3a2d1036437661a4cd8f07
parameters:
-
name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: OK
'404':
description: 'Not found'
What about adding a schema in so we can see what the response body is going to look like?
# app/Http/Resources/WidgetResource.php
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use OpenApi\Attributes as OA;
#[OA\Schema()]
class WidgetResource extends JsonResource
{
#[OA\Property(
description: 'Unique auto-incrementing ID of the Widget.',
readOnly: true,
)]
protected int $id;
#[OA\Property(
description: 'The name of a widget in English.',
)]
protected string $name;
#[OA\Property(
description: 'Optional description of what the widget does, and how it works.',
)]
protected ?string $description;
#[OA\Property(
description: 'Date the widget was created, in ISO 8601 date time.',
example: '2024-01-20T09:15:28Z',
readOnly: true,
)]
protected string $created_at;
#[OA\Property(
description: 'Date the widget was last updated, in ISO 8601 date time.',,
readOnly: true,
)]
protected string $updated_at;
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'data' => [
'id' => $this->id,
'name' => $this->name,
'description' => $this->description,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
],
'links' => [
'self' => route('widgets.show', ['widget' => $this->id]),
],
];
}
}
Here the Swagger-PHP attributes are working with the reflection API to look at the name of the property and infer the types, so we don’t need to define those agai. Rerunning ./vendor/bin/openapi app -o openapi.yaml
will add the following to openapi.yaml
.
openapi: 3.0.0
info:
title: 'My Widget API'
version: 1.0.0
paths:
# snip
components:
schemas:
WidgetResource:
properties:
id:
description: 'Unique auto-incrementing ID of the Widget.'
type: integer
readOnly: true
name:
description: 'The name of a widget in English.'
type: string
description:
description: 'Optional description of what the widget does, and how it works.'
type: string
nullable: true
created_at:
description: 'Date the widget was created, in ISO 8601 date time.'
type: string
readOnly: true
example: '2024-01-20T09:15:28Z'
updated_at:
description: ''
type: string
readOnly: true
type: object
Great, but now we need to join it up with the controller and a full response body.
# app/Http/Controllers/Api/WidgetController.php
#[OA\Get(path: '/api/widgets/{id}', description: 'Display the specified widget.')]
#[OA\Parameter(in: "path", name: "id", required: true, schema: new OA\Schema(type: 'string'))]
#[OA\Response(
response: Response::HTTP_OK,
description: 'OK',
content: new OA\JsonContent(ref: "#/components/schemas/WidgetResource")
)]
#[OA\Response(response: Response::HTTP_NOT_FOUND, description: 'Not found')]
public function show(Widget $widget)
{
return new WidgetResource($widget);
}
Using ref this way feels a little funny at first because you’re using a JSON Reference to something in an OpenAPI description you don’t really control, but once you get the contention it makes sense. The "#/components/schemas/WidgetResource"
name is generated from the class we put the WidgetResource
class that contains a #[OA\Schema()]
, and together that instructs Swagger-PHP to make shared schema component, and now it’s been referenced in your schema controller the whole thing comes together like this:
'/api/widgets/{id}':
get:
description: 'Display the specified widget.'
operationId: 12dd67833d3a2d1036437661a4cd8f07
parameters:
-
name: id
in: path
required: true
schema:
type: string
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/WidgetResource'
'404':
description: 'Not found'
There is a lot more work to be done until 100% of your API is covered, but you can use these concepts to build out most things, and if you need any other help the Swagger-PHP reference documentation can help you out.
Sample Code #
The sample code for this guide is published on GitHub so you can try that if you’re having trouble adding it to your application: laravel-code-first, and the deployed documentation is over here.