wwwision / types-openapi
Generator for OpenAPI schema files, see https://www.openapis.org/
Fund package maintenance!
bwaidelich
Paypal
Requires
- php: >=8.3
- psr/http-factory: ^1
- psr/http-server-handler: ^1
- psr/http-server-middleware: ^1
- webmozart/assert: ^1.11
- wwwision/types: ^1.4
- wwwision/types-jsonschema: ^2
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3
- guzzlehttp/psr7: ^2.7
- phpstan/phpstan: ^2
- phpunit/phpunit: ^10 || ^11 || ^12
- roave/security-advisories: dev-latest
This package is auto-updated.
Last update: 2025-04-15 12:26:42 UTC
README
...possibly... ;)
Integration for the wwwision/types package that allows for generation of OpenAPI schemas and APIs from PHP code
Usage
This package can be installed via composer:
composer require wwwision/types-openapi
Simple Example
This is all that is required to generate an OpenAPI schema for a simple HTTP endpoint:
final class SomeApi { #[Operation(path: '/', method: 'GET')] public function someEndpoint(): string { return '{"success":true}'; } } $openApiObject = (new OpenApiGenerator())->generate(SomeApi::class); assert($openApiObject instanceof OpenApiObject); $expectedSchema = <<<JSON {"openapi":"3.0.3","info":{"title":"","version":"0.0.0"},"paths":{"\/":{"get":{"operationId":"someEndpoint","responses":{"200":{"description":"Default","content":{"application\/json":{"schema":{"type":"string"}}}}}}}}} JSON; assert(json_encode($openApiObject) === $expectedSchema);
Serve HTTP Requests
This package comes with a RequestHandler
that allows for serving HTTP requests using the generated OpenAPI schema.
The RequestHandler
is PSR-7 compatible such that it can easily be integrated with a corresponding psr/http-factory
/psr/http-message
provider, e.g. guzzlehttp/psr7
:
// ... $api = new SomeApi(); $httpFactory = new HttpFactory(); $requestHandler = new RequestHandler($api, $httpFactory, $httpFactory); $request = ServerRequest::fromGlobals(); try { $response = $requestHandler($request); } catch (RequestException $e) { $response = $httpFactory->createResponse($e::getStatusCode(), $e::getReasonPhrase()); $response->getBody()->write($e->getMessage()); } http_response_code($response->getStatusCode()); foreach ($response->getHeaders() as $k => $values) { foreach ($values as $v) { header(sprintf('%s: %s', $k, $v), false); } } echo $response->getBody();
Parameters
Arguments of the endpoint methods are automatically mapped to OpenAPI parameters.
All OpenAPI parameter types are supported (query
, path
, header
, cookie
).
Query parameters
By default, the parameter type is query
:
final class SomeApi { #[Operation(path: '/', method: 'GET')] public function someEndpoint(string $someParam, string|null $someOptionalParam = null): string { return $someParam; } }
will accept requests like
GET /?someParam=foo HTTP/1.1
and
GET /?someParam=foo&someOptionalParam=bar HTTP/1.1
and will map the values to the corresponding method arguments.
Path parameters
Operations can also make use of Path Templating in order to map method arguments from the query path:
final class SomeApi { #[Operation(path: '/static/{param1}/{param2}', method: 'GET')] public function someEndpoint(string $param1, string $param2): string { // ... } }
Path params cannot be optional and must be defined in the path template.
Header parameters
To define a header parameter, the #[Parameter]
attribute can be used:
final class SomeApi { #[Operation(path: '/', method: 'GET')] public function someEndpoint(#[Parameter(in: ParameterLocation::header, name: "X-HeaderName")] string $paramFromHeader): string { // ... } }
Cookie parameters
Likewise, to define a cookie parameter, the #[Parameter]
attribute can be used:
final class SomeApi { #[Operation(path: '/', method: 'GET')] public function someEndpoint(#[Parameter(in: ParameterLocation::cookie, name: "CookieName")] string $paramFromCookie): string { // ... } }
Complex types
Complex parameter types are supported as well as long as they follow the wwwision/types best practices:
#[StringBased(minLength: 3)] final readonly class Username { private function __construct( public string $value, ) {} } final class SomeApi { #[Operation(path: '/', method: 'GET')] public function someEndpoint(Username $username): string { return $username->value; } }
This will validate and map the parameter and fail if it does not satisfy the constraints:
{ "type": "https://www.rfc-editor.org/rfc/rfc9110#name-400-bad-request", "title": "Bad Request", "issues": [ { "code": "too_small", "message": "String must contain at least 3 character(s)", "path": [ "query.username" ], "type": "string", "minimum": 3, "inclusive": true, "exact": false } ] }
Example
The following example makes use of all parameter types:
final class SomeApi { #[Operation(path: '/{paramFromPath}', method: 'GET')] public function someEndpoint( string $paramFromPath, #[Parameter(in: ParameterLocation::header, name: "X-Foo")] string $paramFromHeader, #[Parameter(in: ParameterLocation::cookie, name: "SomeCookie")] string $paramFromCookie, string $paramFromQuery, ): string { return json_encode(func_get_args()); } }
This will lead to an OpenAPI definition like this:
{ // ... "paths": { "/{paramFromPath}": { "get": { "operationId": "someEndpoint", "parameters": [ { "name": "paramFromPath", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "X-Foo", "in": "header", "required": true, "schema": { "type": "string" } }, { "name": "SomeCookie", "in": "cookie", "required": true, "schema": { "type": "string" } }, { "name": "paramFromQuery", "in": "query", "required": true, "schema": { "type": "string" } } ], // ... } } } }
And an HTTP request like:
GET /valueFromPath?paramFromQuery=valueFromQuery HTTP/1.1 Host: localhost:8000 X-Foo: valueFromHeader Cookie: SomeCookie=valueFromCookie
...will result in the following response:
["valueFromPath","valueFromHeader","valueFromCookie","valueFromQuery"]
Security
To implement custom authentication schemes, you can implement the AuthenticationContextProvider
interface and return an instance of your custom AuthenticationContext
class:
final class CustomAuthContext implements AuthenticationContext { public function __construct( public readonly string|null $authenticatedUserId, ) { } } final class AuthContextProvider implements AuthenticationContextProvider { public function getAuthenticationContext(ServerRequestInterface $request, SecurityRequirementObject $securityRequirement): CustomAuthContext|null { // TODO: evaluate the request and security requirement return new CustomAuthContext(authenticatedUserId: 'john.doe'); } }
The AuthenticationContext
is passed to the endpoint method as an additional argument, if the security
option of the Operation
attribute is set.
The RequestHandler
will automatically call the getAuthenticationContext()
method of your provider and pass the result to the endpoint method.
Security schemes can be defined using the #[OpenApi]
attribute:
// ... #[OpenApi( // ... securitySchemes: [ 'someSchema' => [ 'type' => 'http', 'scheme' => 'bearer', ], ], )] final class SomeApi { #[Operation(path: '/', method: 'POST', security: 'someSchema')] public function securedEndpoint(CustomAuthContext $authContext): CreatedResponse { if ($authContext->authenticatedUserId !== 'john.doe') { return new UnauthorizedResponse(); } // do something return new CreatedResponse(); } }
More Examples
More complex example
final class CustomAuthContext implements AuthenticationContext { public function __construct( public readonly string|null $authenticatedUserId, ) { } } #[Description('Unique handle for a user in the API')] #[StringBased(minLength: 1, maxLength: 200)] final class Username { private function __construct( public readonly string $value, ) { } public static function fromString(string $value): self { return instantiate(self::class, $value); } } #[Description('Email address of a user')] #[StringBased(format: StringTypeFormat::email)] final class EmailAddress { private function __construct( public readonly string $value, ) { } public static function fromString(string $value): self { return instantiate(self::class, $value); } } final class User { public function __construct( public readonly Username $username, public readonly EmailAddress $emailAddress, ) { } } /** * @implements IteratorAggregate<User> */ #[Description('A set of users')] #[ListBased(itemClassName: User::class)] final class Users implements IteratorAggregate { /** * @param array<User> $users */ private function __construct(private readonly array $users) { } public static function fromArray(array $users): self { return instantiate(self::class, $users); } public function getIterator(): Traversable { yield from $this->users; } } final class AddUser { public function __construct( public readonly Username $username, public readonly EmailAddress $emailAddress, ) { } } interface UserRepository { public function findAll(): Users; public function findByUsername(Username $username): User|null; public function add(User $user): void; } #[OpenApi(apiTitle: 'Some API', apiVersion: '1.2.3', openApiVersion: '3.0.3', contact: ['name' => 'Contact Name', 'url' => 'https://contact-url.example.com', 'email' => 'contact@example.com'], license: ['name' => 'License name', 'id' => 'licenseId', 'url' => 'https://license.example.com'], securitySchemes: ['basicAuth' => ['type' => 'http', 'scheme' => 'basic', 'description' => 'Basic authentication']])] #[Description('Some API description')] final class SomeApi { public function __construct( private readonly UserRepository $userRepository, ) { } #[Operation(path: '/users', method: 'GET', summary: 'Get Users')] #[Description('Retrieves all users from the repository')] public function users(): Users { return $this->userRepository->findAll(); } #[Operation(path: '/users/{username}', method: 'GET', summary: 'Get a single user by its username')] #[Description('Retrieves a single user or returns a 404 response if not found')] public function userByUsername(Username $username): User|NotFoundResponse { return $this->userRepository->findByUsername($username) ?: new NotFoundResponse(); } #[Operation(path: '/users', method: 'POST', summary: 'Add a new user', security: 'basicAuth')] #[Description('Saves a new user to the repository')] public function addUser(AddUser $command, CustomAuthContext $authContext): CreatedResponse|UnauthorizedResponse { if ($authContext->authenticatedUserId !== 'john.doe') { return new UnauthorizedResponse(); } $this->userRepository->add(new User($command->username, $command->emailAddress)); return new CreatedResponse(); } } $generator = new OpenApiGenerator(); $openApiObject = $generator->generate(SomeApi::class, OpenApiGeneratorOptions::create()); assert($openApiObject instanceof OpenApiObject); $expectedSchema = <<<'JSON' {"openapi":"3.0.3","info":{"title":"Some API","version":"1.2.3","description":"Some API description","contact":{"name":"Contact Name","url":"https:\/\/contact-url.example.com","email":"contact@example.com"},"license":{"name":"License name","url":"https:\/\/license.example.com"}},"paths":{"\/users":{"get":{"summary":"Get Users","description":"Retrieves all users from the repository","operationId":"users","responses":{"200":{"description":"Default","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/Users"}}}}}},"post":{"summary":"Add a new user","description":"Saves a new user to the repository","operationId":"addUser","requestBody":{"content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/AddUser"}}},"required":true},"responses":{"201":{"description":"Created"},"400":{"description":"Bad Request"},"401":{"description":"Unauthorized"}},"security":[{"basicAuth":[]}]}},"\/users\/{username}":{"get":{"summary":"Get a single user by its username","description":"Retrieves a single user or returns a 404 response if not found","operationId":"userByUsername","parameters":[{"name":"username","in":"path","required":true,"schema":{"$ref":"#\/components\/schemas\/Username"}}],"responses":{"200":{"description":"Default","content":{"application\/json":{"schema":{"$ref":"#\/components\/schemas\/User"}}}},"400":{"description":"Bad Request"},"404":{"description":"Not Found"}}}}},"components":{"schemas":{"Username":{"type":"string","description":"Unique handle for a user in the API","minLength":1,"maxLength":200},"EmailAddress":{"type":"string","description":"Email address of a user","format":"email"},"User":{"type":"object","properties":{"username":{"$ref":"#\/components\/schemas\/Username"},"emailAddress":{"$ref":"#\/components\/schemas\/EmailAddress"}},"additionalProperties":false,"required":["username","emailAddress"]},"Users":{"type":"array","description":"A set of users","items":{"$ref":"#\/components\/schemas\/User"}},"AddUser":{"type":"object","properties":{"username":{"$ref":"#\/components\/schemas\/Username"},"emailAddress":{"$ref":"#\/components\/schemas\/EmailAddress"}},"additionalProperties":false,"required":["username","emailAddress"]}},"securitySchemes":{"basicAuth":{"type":"http","description":"Basic authentication","scheme":"basic"}}}} JSON; assert(json_encode($openApiObject) === $expectedSchema);
// ... final class FakeUserRepository implements UserRepository { /** * @var array<string, User> */ private array $usersByUsername; public function __construct() { $this->usersByUsername = [ 'john.doe' => new User(Username::fromString('john.doe'), EmailAddress::fromString('john.doe@example.com')), 'jane.doe' => new User(Username::fromString('jane.doe'), EmailAddress::fromString('jane.doe@example.com')), ]; } public function findAll(): Users { return Users::fromArray(array_values($this->usersByUsername)); } public function findByUsername(Username $username): User|null { return $this->usersByUsername[$username->value] ?? null; } public function add(User $user): void { $this->usersByUsername[$user->username->value] = $user; } } final class AuthContextProvider implements AuthenticationContextProvider { public function getAuthenticationContext(ServerRequestInterface $request, SecurityRequirementObject $securityRequirement): CustomAuthContext|null { // TODO: evaluate the request and security requirement return new CustomAuthContext(authenticatedUserId: 'john.doe'); } } $api = new SomeApi(new FakeUserRepository()); $httpFactory = new HttpFactory(); $requestHandler = new RequestHandler($api, $httpFactory, $httpFactory, authenticationContextProvider: new AuthContextProvider()); $request = ServerRequest::fromGlobals(); try { $response = $requestHandler($request); } catch (RequestException $e) { $response = $httpFactory->createResponse($e::getStatusCode(), $e::getReasonPhrase()); $response->getBody()->write($e->getMessage()); } http_response_code($response->getStatusCode()); foreach ($response->getHeaders() as $k => $values) { foreach ($values as $v) { header(sprintf('%s: %s', $k, $v), false); } } echo $response->getBody();
Contribution
Contributions in the form of issues or pull requests are highly appreciated
License
See LICENSE