wwwision/types-openapi
Generator for OpenAPI schema files, see https://www.openapis.org/
Maintainers
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.8
- 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
Suggests
None
Provides
None
Conflicts
None
Replaces
None
MIT 98e7eb868a9d6d5d26012935b6040a9acda7ac28
- bwaidelich <b.waidelich.woop@wwwision.de>
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
Contribution
Contributions in the form of issues or pull requests are highly appreciated
License
See LICENSE
