Cookbook
x-tagGroups
OpenApi has the concept of grouping endpoints using tags. On top of that, some tools (redocly, for example) support further grouping via the vendor extension x-tagGroups
.
/**
* @OA\OpenApi(
* x={
* "tagGroups"=
* {{"name"="User Management", "tags"={"Users", "API keys", "Admin"}}
* }
* }
* )
*/
Adding examples to @OA\Response
/*
* @OA\Response(
* response=200,
* description="OK",
* @OA\JsonContent(
* oneOf={
* @OA\Schema(ref="#/components/schemas/Result"),
* @OA\Schema(type="boolean")
* },
* @OA\Examples(example="result", value={"success": true}, summary="An result object."),
* @OA\Examples(example="bool", value=false, summary="A boolean value."),
* )
* )
*/
External documentation
OpenApi allows a single reference to external documentation. This is a part of the top level @OA\OpenApi
.
/**
* @OA\OpenApi(
* @OA\ExternalDocumentation(
* description="More documentation here...",
* url="https://example.com/externaldoc1/"
* )
* )
*/
TIP
If no @OA\OpenApi
is configured, swagger-php
will create one automatically.
That means the above example would also work with just the OA\ExternalDocumentation
annotation.
/**
* @OA\ExternalDocumentation(
* description="More documentation here...",
* url="https://example.com/externaldoc1/"
* )
*/
Properties with union types
Sometimes properties or even lists (arrays) may contain data of different types. This can be expressed using oneOf
.
/**
* @OA\Schema(
* schema="StringList",
* @OA\Property(property="value", type="array", @OA\Items(anyOf={@OA\Schema(type="string")}))
* )
* @OA\Schema(
* schema="String",
* @OA\Property(property="value", type="string")
* )
* @OA\Schema(
* schema="Object",
* @OA\Property(property="value", type="object")
* )
* @OA\Schema(
* schema="mixedList",
* @OA\Property(property="fields", type="array", @OA\Items(oneOf={
* @OA\Schema(ref="#/components/schemas/StringList"),
* @OA\Schema(ref="#/components/schemas/String"),
* @OA\Schema(ref="#/components/schemas/Object")
* }))
* )
*/
This will resolve into this YAML
openapi: 3.0.0
components:
schemas:
StringList:
properties:
value:
type: array
items:
anyOf:
-
type: string
type: object
String:
properties:
value:
type: string
type: object
Object:
properties:
value:
type: object
type: object
mixedList:
properties:
fields:
type: array
items:
oneOf:
-
$ref: '#/components/schemas/StringList'
-
$ref: '#/components/schemas/String'
-
$ref: '#/components/schemas/Object'
type: object
Referencing a security scheme
An API might have zero or more security schemes. These are defined at the top level and vary from simple to complex:
/**
* @OA\SecurityScheme(
* type="apiKey",
* name="api_key",
* in="header",
* securityScheme="api_key"
* )
*
* @OA\SecurityScheme(
* type="oauth2",
* securityScheme="petstore_auth",
* @OA\Flow(
* authorizationUrl="http://petstore.swagger.io/oauth/dialog",
* flow="implicit",
* scopes={
* "read:pets": "read your pets",
* "write:pets": "modify pets in your account"
* }
* )
* )
*/
To declare an endpoint as secure and define what security schemes are available to authenticate a client it needs to be added to the operation, for example:
/**
* @OA\Get(
* path="/api/secure/",
* summary="Requires authentication"
* ),
* security={ {"api_key": {}} }
* )
*/
Endpoints can support multiple security schemes and have custom options too:
/**
* @OA\Get(
* path="/api/secure/",
* summary="Requires authentication"
* ),
* security={
* { "api_key": {} },
* { "petstore_auth": {"write:pets", "read:pets"} }
* }
* )
*/
File upload with headers
/**
* @OA\Post(
* path="/v1/media/upload",
* summary="Upload document",
* description="",
* tags={"Media"},
* @OA\RequestBody(
* required=true,
* @OA\MediaType(
* mediaType="application/octet-stream",
* @OA\Schema(
* required={"content"},
* @OA\Property(
* description="Binary content of file",
* property="content",
* type="string",
* format="binary"
* )
* )
* )
* ),
* @OA\Response(
* response=200, description="Success",
* @OA\Schema(type="string")
* ),
* @OA\Response(
* response=400, description="Bad Request"
* )
* )
*/
Set the XML root name
The OA\Xml
annotation may be used to set the XML root element for a given @OA\XmlContent
response body
/**
* @OA\Schema(
* schema="Error",
* @OA\Property(property="message"),
* @OA\Xml(name="details")
* )
*/
/**
* @OA\Post(
* path="/foobar",
* @OA\Response(
* response=400,
* description="Request error",
* @OA\XmlContent(ref="#/components/schemas/Error",
* @OA\Xml(name="error")
* )
* )
* )
*/
upload multipart/form-data
Form posts are @OA\Post
requests with a multipart/form-data
@OA\RequestBody
. The relevant bit looks something like this
/**
* @OA\Post(
* path="/v1/user/update",
* summary="Form post",
* @OA\RequestBody(
* @OA\MediaType(
* mediaType="multipart/form-data",
* @OA\Schema(
* @OA\Property(property="name"),
* @OA\Property(
* description="file to upload",
* property="avatar",
* type="string",
* format="binary",
* ),
* )
* )
* ),
* @OA\Response(response=200, description="Success")
* )
*/
Default security scheme for all endpoints
Unless specified each endpoint needs to declare what security schemes it supports. However, there is a way to also configure security schemes globally for the whole API.
This is done on the @OA\OpenApi
annotation:
Nested objects
Complex, nested data structures are defined by nesting @OA\Property
annotations inside others (with type="object"
).
/**
* @OA\Schema(
* schema="Profile",
* type="object",
*
* @OA\Property(
* property="Status",
* type="string",
* example="0"
* ),
*
* @OA\Property(
* property="Group",
* type="object",
*
* @OA\Property(
* property="ID",
* description="ID de grupo",
* type="number",
* example=-1
* ),
*
* @OA\Property(
* property="Name",
* description="Nombre de grupo",
* type="string",
* example="Superadmin"
* )
* )
* )
*/
Documenting union type response data using oneOf
A response with either a single or a list of QualificationHolder
's.
/**
* @OA\Response(
* response=200,
* @OA\JsonContent(
* oneOf={
* @OA\Schema(ref="#/components/schemas/QualificationHolder"),
* @OA\Schema(
* type="array",
* @OA\Items(ref="#/components/schemas/QualificationHolder")
* )
* }
* )
* )
*/
Reusing responses
Global responses are found under /components/responses
and can be referenced/shared just like schema definitions (models)
/**
* @OA\Response(
* response="product",
* description="All information about a product",
* @OA\JsonContent(ref="#/components/schemas/Product")
* )
*/
class ProductResponse {}
// ...
class ProductController
{
/**
* @OA\Get(
* tags={"Products"},
* path="/products/{product_id}",
* @OA\Response(
* response="default",
* ref="#/components/responses/product"
* )
* )
*/
public function getProduct($id)
{
}
}
`response` parameter is always required
Even if referencing a shared response definition, the response
parameter is still required.
mediaType="/"
Using */*
as mediaType
is not possible using annotations.
Example:
/**
* @OA\MediaType(
* mediaType="*/*",
* @OA\Schema(type="string",format="binary")
* )
*/
The doctrine annotations library used for parsing annotations does not handle this and will interpret the */
bit as the end of the comment.
Using just *
or application/octet-stream
might be usable workarounds.
Warning about Multiple response with same response="200"
There are two scenarios where this can happen
- A single endpoint contains two responses with the same
response
value. - There are multiple global response declared, again more than one with the same
response
value.
Callbacks
The API does include basic support for callbacks. However, this needs to be set up mostly manually.
Example
/**
* ...
*
* callbacks={
* "onChange"={
* "{$request.query.callbackUrl}"={
* "post": {
* "requestBody": @OA\RequestBody(
* description="subscription payload",
* @OA\MediaType(mediaType="application/json", @OA\Schema(
* @OA\Property(property="timestamp", type="string", format="date-time", description="time of change")
* ))
* )
* },
* "responses": {
* "202": {
* "description": "Your server implementation should return this HTTP status code if the data was received successfully"
* }
* }
* }
* }
* }
*
* ...
*
*/
(Mostly) virtual models
Typically, a model is annotated by adding a @OA\Schema
annotation to the class and then individual @OA\Property
annotations to the individually declared class properties.
It is possible, however, to nest O@\Property
annotations inside a schema even without properties. In fact, all that is needed is a code anchor - e.g. an empty class.
use OpenApi\Attributes as OA;
#[OA\Schema(
properties: [
'name' => new OA\Property(property: 'name', type: 'string'),
'email' => new OA\Property(property: 'email', type: 'string'),
]
)]
class User {}
Using class name as type instead of references
Typically, when referencing schemas this is done using $ref
's
#[OAT\Schema(schema: 'user')]
class User
{
}
#[OAT\Schema()]
class Book
{
/**
* @var User
*/
#[OAT\Property(ref: '#/components/schemas/user')]
public $author;
}
This works, but is not very convenient.
First, when using custom schema names (schema: 'user'
), this needs to be taken into account everywhere. Secondly, having to write ref: '#/components/schemas/user'
is tedious and error-prone.
Using attributes all this changes as we can take advantage of PHP itself by referring to a schema by its (fully qualified) class name.
With the same User
schema as before, the Book::author
property could be written in a few different ways
#[OAT\Property()]
public User author;
or
/**
* @var User
*/
#[OAT\Property()]
public author;
or
#[OAT\Property(type: User::class)]
public author;
Enums
As of PHP 8.1 there is native support for enum
's.
swagger-php
supports enums in much the same way as class names can be used to reference schemas.
Example
#[Schema()]
enum State
{
case OPEN;
case MERGED;
case DECLINED;
}
#[Schema()]
class PullRequest
#[OAT\Property()]
public State $state
}
However, in this case the schema generated for State
will be an enum:
components:
schemas:
PullRequest:
properties:
state:
$ref: '#/components/schemas/State'
type: object
State:
type: string
enum:
- OPEN
- MERGED
- DECLINED
Multi value query parameter: &q[]=1&q[]=1
PHP allows to have query parameters multiple times in the url and will combine the values to an array if the parameter name uses trailing []
. In fact, it is possible to create nested arrays too by using more than one pair of []
.
In terms of OpenAPI, the parameters can be considered a single parameter with a list of values.
/**
* @OA\Get(
* path="/api/endpoint",
* description="The endpoint",
* operationId="endpoint",
* tags={"endpoints"},
* @OA\Parameter(
* name="things[]",
* in="query",
* description="A list of things.",
* required=false,
* @OA\Schema(
* type="array",
* @OA\Items(type="integer")
* )
* ),
* @OA\Response(response="200", description="All good")
* )
*/
The corresponding bit of the spec will look like this:
parameters:
-
name: 'things[]'
in: query
description: 'A list of things.'
required: false
schema:
type: array
items:
type: integer
swagger-ui
will show a form that allows to add/remove items (integer
values in this case) to/from a list and post those values as something like ?things[]=1&things[]=2&things[]=0
Custom response classes
Even with using refs there is a bit of overhead in sharing responses. One way around that is to write your own response classes. The beauty is that in your custom __construct()
method you can prefill as much as you need.
Best of all, this works for both annotations and attributes.
Example:
use OpenApi\Attributes as OA;
/**
* @Annotation
*/
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
class BadRequest extends OA\Response
{
public function __construct()
{
parent::__construct(response: 400, description: 'Bad request');
}
}
class Controller
{
#[OA\Get(path: '/foo', responses: [new BadRequest()])]
public function get()
{
}
#[OA\Post(path: '/foo')]
#[BadRequest]
public function post()
{
}
/**
* @OA\Delete(
* path="/foo",
* @BadRequest()
* )
*/
public function delete()
{
}
}
Annotations only?
If you are only interested in annotations you canleave out the attribute setup line (#[\Attribute...
) for BadRequest
.
Furthermore, your custom annotations should extend from the OpenApi\Annotations
namespace.
Annotating class constants
use OpenApi\Attributes as OA;
#[OA\Schema()]
class Airport
{
#[OA\Property(property='kind')]
public const KIND = 'Airport';
}
The const
property is supported in OpenApi 3.1.0.
components:
schemas:
Airport:
properties:
kind:
type: string
const: Airport
For 3.0.0 this is serialized into a single value enum
.
components:
schemas:
Airport:
properties:
kind:
type: string
enum:
- Airport