Extra attributes/annotations and other bits for swagger-php.
You can use composer or simply download the release.
Composer
The preferred method is via composer. Follow the installation instructions if you do not already have composer installed.
Once composer is installed, execute the following command in your project root to install this library:
composer require radebatz/openapi-extrasUse of the included annotations/attributes requires registration of a custom swagger-php processor.
Also, in the case of annotations, the registration of custom aliases / namespaces needs to be done manually.
When using the OpenApiBuilder no additional registration code is required as the builder will always
configure the required MergeControllerDefaults processor.
<?php
use Radebatz\OpenApi\Extras\OpenApiBuilder;
$generator = (new OpenApiBuilder())->build();
// ...<?php
use OpenApi\Generator;
use OpenApi\Processors\BuildPaths;
use Radebatz\OpenApi\Extras\Processors\MergeControllerDefaults;
$generator = new Generator();
$generator->getProcessorPipeline()
->insert(new MergeControllerDefaults(), BuildPaths::class);
// ...<?php
use OpenApi\Generator;
use OpenApi\Processors\BuildPaths;
use Radebatz\OpenApi\Extras\Processors\MergeControllerDefaults;
$namespace = 'Radebatz\\OpenApi\\Extras\\Annotations';
$generator = new Generator();
$generator
->addNamespace($namespace . '\\')
->addAlias('oax', $namespace),
->getProcessorPipeline()
->insert(new MergeControllerDefaults(), BuildPaths::class);
// ...The builder aims to simplify configuring the swagger-php Generator class by implementing
explicit methods to configure all default processors.
Futhermore, it also adds a new Customizer processor which allows to pre-process all instances
of a given OpenApi annotation/attribute by registering callbacks.
<?php declare(strict_types=1);
use OpenApi\Annotations as OA;
use Psr\Log\NullLogger;
use Radebatz\OpenApi\Extras\OpenApiBuilder;
$generator = (new OpenApiBuilder())
->addCustomizer(OA\Info::class, fn (OA\Info $info) => $info->description = 'Foo')
->tagsToMatch(['admin'])
->clearUnusedComponents(enabled: true)
->operationIdHashing(enabled: false)
->pathsToMatch(['/api'])
->enumDescription()
->build(new NullLogger());The controller annotation may be used to:
- add an optional url prefix to all operations in the class
- add one or more
Tags to all operations in the class - share one or more
Responses across all operations - share one or more
Header's across all operations - share one or more
Middleware's across all operations
Example for adding the /foo prefix and a 403 response to all operations in the MyController class.
<?php declare(strict_types=1);
use OpenApi\Attributes as OAT;
use Radebatz\OpenApi\Extras\Attributes as OAX;
#[OAX\Controller(prefix: '/foo')]
#[OAT\Response(response: 403, description: 'Not allowed')]
class PrefixedController
{
#[OAT\Get(path: '/prefixed', operationId: 'prefixed')]
#[OAT\Response(response: 200, description: 'All good')]
public function prefixed(): mixed
{
return 'prefixed';
}
}Controller annotations are inherited from parent classes. This allows defining shared configuration on a base controller:
- Prefixes concatenate: parent
/api/v2+ child/users=/api/v2/users - Tags merge (deduplicated)
- Responses merge by response code (child overrides parent for same code)
- Headers merge by header name (child overrides parent for same name)
- Middlewares merge by exact name (deduplicated)
Set inherit: false on a child controller to opt out of inheritance.
<?php declare(strict_types=1);
use OpenApi\Attributes as OAT;
use Radebatz\OpenApi\Extras\Attributes as OAX;
#[OAX\Controller(prefix: '/api/v2', tags: ['api'])]
#[OAT\Response(response: 403, description: 'Not allowed')]
#[OAX\Middleware([AuthMiddleware::class])]
abstract class BaseController
{
}
#[OAX\Controller(prefix: '/users', tags: ['users'])]
#[OAT\Response(response: 404, description: 'Not found')]
class UserController extends BaseController
{
#[OAT\Get(path: '/list', operationId: 'listUsers')]
#[OAT\Response(response: 200, description: 'All good')]
public function list(): mixed
{
// effective path: /api/v2/users/list
// effective tags: ['api', 'users']
// effective responses: 200, 403 (from parent), 404 (from child)
return 'list';
}
}Middleware annotations allow to attach a list of middleware names either individually or across all operations (via the Controller annotation).
Controller-level middlewares are merged onto all operations in the class; operation-level middlewares are additive.
This is used by the openapi-router project to configure routing middleware from OpenAPI specs.
<?php declare(strict_types=1);
use OpenApi\Attributes as OAT;
use Radebatz\OpenApi\Extras\Attributes as OAX;
#[OAX\Controller(
middlewares: [new OAX\Middleware([FooMiddleware::class])]
)]
class MiddlewareController
{
#[OAT\Get(path: '/mw', operationId: 'mw')]
#[OAT\Response(response: 200, description: 'All good')]
#[OAX\Middleware([BarMiddleware::class])]
public function mw()
{
return 'mw';
}
}Middleware subclasses can implement ProvidesCustomizersInterface to automatically apply OpenAPI modifications to operations that carry them. This eliminates duplication — for example, a jwt-auth middleware can auto-inject the security scheme without every operation declaring it manually.
<?php declare(strict_types=1);
use OpenApi\Annotations as OA;
use Radebatz\OpenApi\Extras\Attributes\Middleware;
use Radebatz\OpenApi\Extras\ProvidesCustomizersInterface;
#[\Attribute(\Attribute::TARGET_ALL | \Attribute::IS_REPEATABLE)]
class SecureMiddleware extends Middleware implements ProvidesCustomizersInterface
{
public function __construct()
{
parent::__construct(names: ['jwt-auth']);
}
public static function customizers(): array
{
return [
OA\Operation::class => [
fn (OA\Operation $op) => $op->security = [['bearerAuth' => []]],
],
];
}
}Then use it on a controller — all operations inherit both the middleware name and the security effect:
<?php declare(strict_types=1);
use OpenApi\Attributes as OAT;
use Radebatz\OpenApi\Extras\Attributes as OAX;
#[OAX\Controller(middlewares: [new SecureMiddleware()])]
class UserController
{
#[OAT\Get(path: '/users', operationId: 'listUsers')]
#[OAT\Response(response: 200, description: 'All good')]
public function list(): mixed
{
// security: [['bearerAuth' => []]] is applied automatically
return 'list';
}
}The customizers() method returns the same mapping format as the Customizers processor (annotation class => callable[]), but these are scoped — they only apply to operations carrying the middleware, not globally.
A shorthand for JSON responses that reference a schema. Wraps the referenced schema in an envelope property (default "data", matching Laravel's JsonResource::$wrap). For unwrapped responses, use the regular OAT\Response annotation.
If no description is provided, it is derived from the referenced schema (fallback order: title > description > schema name > class short name).
| Parameter | Default | Effect |
|---|---|---|
wrap |
'data' |
Property name for the envelope |
<?php declare(strict_types=1);
use OpenApi\Attributes as OAT;
use Radebatz\OpenApi\Extras\Attributes as OAX;
#[OAT\Schema(schema: 'TokenPairResource', title: 'Token pair')]
class TokenPairResource
{
#[OAT\Property(property: 'access_token', type: 'string')]
public string $accessToken;
#[OAT\Property(property: 'refresh_token', type: 'string')]
public string $refreshToken;
}
class AuthController
{
#[OAT\Post(path: '/auth/login', operationId: 'login')]
// Wrapped (default): {"data": {$ref: TokenPairResource}}
#[OAX\JsonResponse(response: 200, ref: TokenPairResource::class)]
#[OAX\JsonResponse(response: 401, description: 'Invalid credentials')]
public function login(): mixed
{
return '...';
}
#[OAT\Get(path: '/auth/session', operationId: 'session')]
// Custom wrap key: {"result": {$ref: TokenPairResource}}
#[OAX\JsonResponse(response: 200, ref: TokenPairResource::class, wrap: 'result')]
public function session(): mixed
{
return '...';
}
#[OAT\Get(path: '/auth/tokens', operationId: 'tokens')]
// List: {"data": [{$ref: TokenPairResource}, ...]}
#[OAX\JsonResponse(
response: 200,
content: new OAT\JsonContent(type: 'array', items: new OAT\Items(ref: TokenPairResource::class)),
)]
public function tokens(): mixed
{
return '...';
}
}This generates:
content:
application/json:
schema:
required: [data]
properties:
data:
$ref: '#/components/schemas/TokenPairResource'A shorthand for JSON request bodies that reference a schema. Reduces nesting by wrapping the ref in a JsonContent automatically.
If no description is provided, it is derived from the referenced schema (fallback order: title > description > schema name > class short name).
<?php declare(strict_types=1);
use OpenApi\Attributes as OAT;
use Radebatz\OpenApi\Extras\Attributes as OAX;
#[OAT\Schema(schema: 'LoginRequest', title: 'Login credentials')]
class LoginRequest
{
#[OAT\Property(property: 'email', type: 'string')]
public string $email;
#[OAT\Property(property: 'password', type: 'string')]
public string $password;
}
class AuthController
{
// Method-level with explicit ref
#[OAT\Post(path: '/auth/login', operationId: 'login')]
#[OAX\JsonRequestBody(ref: LoginRequest::class, required: true)]
public function login(): mixed
{
// description auto-derived as "Login credentials" from schema title
return '...';
}
// Parameter-level — ref inferred from type-hint
#[OAT\Post(path: '/auth/register', operationId: 'register')]
public function register(#[OAX\JsonRequestBody] LoginRequest $request): mixed
{
return '...';
}
}This is equivalent to the more verbose:
#[OAT\RequestBody(
description: 'Login credentials',
required: true,
content: new OAT\JsonContent(ref: LoginRequest::class)
)]When ref points to a class with an #[OAT\RequestBody] annotation, a component $ref is generated instead of inline JsonContent:
#[OAT\RequestBody(request: 'SharedCreateBody')]
class SharedCreateBody { /* ... */ }
class ItemController
{
#[OAT\Post(path: '/items', operationId: 'createItem')]
#[OAX\JsonRequestBody(ref: SharedCreateBody::class)]
public function create(): mixed
{
// produces: $ref: '#/components/requestBodies/SharedCreateBody'
return '...';
}
}The openapi-extras project is released under the MIT license.