Cookbook
Return JSON from an endpoint
For application/json content, OA\MediaType\Json takes the schema directly, without a nested OA\Schema. OA\MediaType\Xml is the same for XML.
See OA\MediaType.
Document a list of objects
A property holding a list takes OA\Schema\Items, with the schema of one item as its arguments.
See OA\Schema subclasses.
Describe an object without a PHP class
Where a controller returns an array rather than an object of a class, properties can be keyed by property name. Classic has no short form for this.
A named schema works the same way, with an empty class as the anchor. An OA\Schema as the value nests an object:
See Keyed properties.
Add constraints to a class property
A class property needs only an OA\Schema with the constraints. The name and the type come from the declaration. Spec also lists a typed property without a default as required.
An explicit OA\Property is needed only where the document uses a different name, such as full_name for $name, or on a getter. See OA\Property.
Document path, query and header parameters
OA\Parameter\Path, Query, Header and Cookie set in, and a path parameter is always required.
See OA\Parameter.
Document several operations on one path
Each controller method gets the attribute for its HTTP method, OA\Operation\Get, OA\Operation\Post and so on.
See OA\Operation\*.
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.
Adding examples to OA\Response
Adding examples to OA\Schema
A schema's examples is the JSON Schema keyword rather than a map of Example Objects, so it takes a list of values. OA\Schema\Items and OA\Schema\AdditionalProperties are schemas too and take the same list. A property takes it on the OA\Schema beside its OA\Property.
A media type, parameter or header takes Example Objects instead.
Mode difference
In classic, OA\Property, OA\Items and OA\AdditionalProperties are schemas themselves and take the list directly. A nested OA\Examples is accepted there and contributes its value. The rest of it (the key, summary, description, externalValue) is dropped. In spec, an Example Object in the list is reported with a warning.
The keyword arrived in OpenAPI 3.1. Generating 3.0 from the same source drops the list in classic and carries the first value across as example in spec. Both warn.
External documentation
OpenApi allows a single reference to external documentation. This is a part of the top level OA\OpenApi.
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.
#[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.
This will resolve into this YAML
openapi: 3.1.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: objectReferencing 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:
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:
Endpoints can support multiple security schemes and have custom options too:
See OA\Security\Requirement and OA\Security\Scheme\*.
File upload with headers
Set the XML root name
OA\Xml may be used to set the XML root element for an XML response body
upload multipart/form-data
Form posts are POST operations with a multipart/form-data request body. The relevant bit looks something like this
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 OA\OpenApi:
Documenting union type response data using oneOf
A response with either a single or a list of QualificationHolder's.
Reusing responses
Global responses are found under /components/responses and can be referenced/shared just like schema definitions (models)
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 with docblock annotations. Attributes do not have this problem.
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.
Callbacks
The API does include basic support for callbacks. However, this needs to be set up mostly manually.
Example
Using class name as a type instead of references
Typically, when referencing schemas this is done using $ref's
#[OA\Schema(component: 'user')]
class User
{
}
#[OA\Schema]
class Book
{
/**
* @var User
*/
#[OA\Schema(ref: '#/components/schemas/user')]
public $author;
}This works but is not very convenient.
First, when using custom schema names (component: '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
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
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
- DECLINEDMulti value query parameter: &q[]=1&q[]=1
PHP allows having 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.
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: integerswagger-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
See OA\Parameter\* and OA\Schema subclasses.
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.
Example:
use OpenApi\Spec as OA;
#[\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\Operation\Get(path: '/foo', responses: [new BadRequest()])]
public function get()
{
}
#[OA\Operation\Post(path: '/foo')]
#[BadRequest]
public function post()
{
}
}Classic attributes and docblock annotations
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()
{
}
}Docblock annotations only?
If you are only interested in docblock annotations, you can leave out the attribute setup line (#[\Attribute...) for BadRequest.
Furthermore, your custom docblock annotations should extend from the OpenApi\Annotations namespace.
Annotating class constants
The const property is supported in OpenApi 3.1.0.
components:
schemas:
Airport:
properties:
kind:
type: string
const: AirportFor 3.0.0 this is serialized into a single value enum.
components:
schemas:
Airport:
properties:
kind:
type: string
enum:
- AirportAdd a hand-written document
Some parts of an API have no PHP code to put attributes on, webhooks for example. Write them as an OpenAPI 3.1 document and pass it to the Builder with withDocument(). A $ref in it can point at a schema built from a class:
openapi: 3.1.0
info:
title: Shop
version: 1.0.0
webhooks:
orderPlaced:
post:
summary: An order was placed
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
responses:
'200':
description: Received<?php declare(strict_types=1);
namespace OpenApi\Snippets\Guide\Cookbook\Documents;
use OpenApi\Spec as OA;
#[OA\Schema(description: 'An order')]
class Order
{
#[OA\Property]
public int $id;
#[OA\Property]
public string $status;
}
class OrderController
{
#[OA\Operation\Get(path: '/orders/latest', operationId: 'latestOrder', responses: [
new OA\Response(response: 200, description: 'The latest order', content: [new OA\MediaType\Json(schema: new OA\Schema(ref: Order::class))]),
])]
public function latest(): void
{
}
}<?php declare(strict_types=1);
namespace OpenApi\Snippets\Guide\Cookbook\Documents;
use OpenApi\Builder;
function build(): Builder
{
return (new Builder())
->addSource(__DIR__ . '/models.php')
->withDocument(__DIR__ . '/webhooks.yaml');
}gives:
openapi: 3.1.0
info:
title: Shop
version: 1.0.0
paths:
/orders/latest:
get:
operationId: latestOrder
responses:
'200':
description: 'The latest order'
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
components:
schemas:
Order:
type: object
description: 'An order'
properties:
id:
type: integer
status:
type: string
required:
- id
- status
webhooks:
orderPlaced:
post:
summary: 'An order was placed'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
responses:
'200':
description: ReceivedWhere a document and the attributes describe the same thing, the attributes win. The details, and what a document cannot hold, are in Documents.
Build parts of a document for another one
An integration that keeps a document of its own can still take parts of it from spec attributes: a model's schema, or a parameter. Build each part with a Builder, so it goes through the whole pipeline: the resolver, the augmenters and any augmenter the integration registers. Compiling the attributes directly skips all of that.
<?php declare(strict_types=1);
namespace OpenApi\Snippets\Guide\Cookbook\Fragments;
use OpenApi\Augmenter\Cleanup;
use OpenApi\Builder;
use OpenApi\Builder\Mode;
use OpenApi\Contracts\AttributeInterface;
use OpenApi\Spec as OA;
use OpenApi\Specification;
/**
* A new builder for each part; the host's own augmenters are registered here too.
*/
function partBuilder(): Builder
{
return (new Builder())
->setMode(Mode::SPEC)
->setVersion('3.1.0')
->withSpecification(fn (Specification $specification) => $specification->add(
(new OA\Info(title: 'Part', version: '1.0.0'))->setMeta('host.filler', true)
));
}
/**
* The schema of a class with `#[OA\Schema]`.
*
* @param class-string $class
*
* @return array<string, mixed>
*/
function buildModel(string $class): array
{
$reflector = new \ReflectionClass($class);
$document = partBuilder()
->addSource($reflector)
->withSpecification(function (Specification $specification): void {
foreach ($specification->schemas as $schema) {
$schema->setMeta(Cleanup::class, false);
}
})
->build()
->toArray();
return $document['components']['schemas'][$reflector->getShortName()];
}
/**
* A part keyed with `component:`, from the given `components` bucket.
*
* @return array<string, mixed>
*/
function buildPart(AttributeInterface $part, string $bucket): array
{
$document = partBuilder()
->withSpecification(fn (Specification $specification) => $specification->add($part->setMeta(Cleanup::class, false)))
->build()
->toArray();
return $document['components'][$bucket][$part->component];
}Nothing references a part, so the Cleanup augmenter would remove it as unused. Each part is marked with setMeta(Cleanup::class, false), which Cleanup leaves alone, together with what the part references.
The builder:
- adds an
OA\Info, since a document without one reports an error. It is filler, marked withsetMeta()under a key the integration owns, so its own augmenters can leave it alone. - is new for every part. A
Builderbuilds one document, and building a few classes is cheap.
The integration then reads each part by its key and ignores the rest of the document. For the classes below, buildModel(User::class) and a path parameter passed to buildPart():
<?php declare(strict_types=1);
namespace OpenApi\Snippets\Guide\Cookbook\Fragments;
use OpenApi\Spec as OA;
#[OA\Schema(description: 'A registered user')]
class User
{
/**
* @example 7
*/
#[OA\Property(schema: new OA\Schema(readOnly: true))]
public int $id;
#[OA\Property(property: 'display_name')]
public ?string $name = null;
#[OA\Property]
public Address $address;
}
#[OA\Schema]
class Address
{
#[OA\Property]
public string $street;
}buildPart(new OA\Parameter\Path(name: 'id', schema: new OA\Schema(type: 'integer'), component: 'id'), 'parameters');give:
User:
type: object
description: 'A registered user'
properties:
id:
type: integer
readOnly: true
example: 7
display_name:
type:
- string
- 'null'
address:
$ref: '#/components/schemas/Address'
required:
- id
- address
id:
name: id
in: path
required: true
schema:
type: integerA $ref in a part points into the components of the build that made it, as #/components/schemas/Address does here. The integration maps it to the name it uses for that class in its own document.
At version 3.0.0 the build also reports an error that paths is required. An OA\Operation added as filler next to the OA\Info removes the error.