Skip to content

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.

php
#[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

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: 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:

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:

See OA\Security\Requirement.

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:

php
/**
 * @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

php
#[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:

yaml
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 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:

yaml
      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

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:

php
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 ​

php
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.

yaml
components:
  schemas:
    Airport:
        properties:
          kind:
            type: string
            const: Airport

For 3.0.0 this is serialized into a single value enum.

yaml
components:
  schemas:
    Airport:
        properties:
          kind:
            type: string
            enum:
              - Airport

Add 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:

yaml
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
<?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
<?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:

yaml
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: Received

Where 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
<?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 with setMeta() under a key the integration owns, so its own augmenters can leave it alone.
  • is new for every part. A Builder builds 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
<?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;
}
php
buildPart(new OA\Parameter\Path(name: 'id', schema: new OA\Schema(type: 'integer'), component: 'id'), 'parameters');

give:

yaml
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: integer

A $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.

See Contributing to the Specification.