Sunshine PHP 2020
Daniel Abernathy
Austin, TX
"A broadly adopted industry standard for describing modern APIs."
A guide for developers on how to build or consume an API.
An agreement between all stakeholders on how an API's requests and responses should be structured.
Practical summary:
An OpenAPI document is a machine-readable description of an API's endpoints and the structure of the data in its requests and responses.
vs. API modeling tools like Postman
OpenAPI
Postman
vs. API format specifications like JSON:API
OpenAPI
JSON:API
vs. other API description specifications
API Blueprint
RAML
Documentation
Mock Server
Testing
Validation
(... and more)
{
"openapi": "3.0.0",
"info": {
"title": "Raffle API",
"version": "",
"description": "Raffle Application API"
},
"servers": [],
"paths": {},
"components": {}
}
"/widgets": {
"get": {
"description": "Get all widgets"
"responses": {
"200": {
"description": "OK"
}
}
}
}
Paths describe your individual endpoints
They primarily contain "operations", which correspond to HTTP verbs
"/widgets/{widgetId}": {
"parameters": [
{
"schema": {
"type": "integer"
},
"name": "widgetId",
"in": "path",
"required": true
}
]
}
Paths can also contain parameters
"patch": {
"description": "Update a widget",
"responses": {
"200": {
"description": "OK"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
}
}
}
The most important parts of an operation are the request body and the responses.
Data in OpenAPI describes schema with JSON Schema.... ish
OpenAPI uses an "extended subset" of an old draft of JSON Schema.
ಠ_ಠ
"schema": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string"
},
"description": {
"type": "string",
"nullable": true
}
}
}
The root level components property lets you store data that you can reuse throughout document
{
"components": {
"schema": {},
"responses": {},
"parameters": {},
"requestBodies": {}
}
}
The $ref keyword lets you point to an item in the components section from elsewhere in the document.
{
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Widget"
}
}
}
GET /contests
POST /contests
GET /contests/{contestId}
PATCH /contests/{contestId}
DELETE /contests/{contestId}
GET /contests/{contestId}/prizes
POST /contests/{contestId}/prizes
GET /contests/{contestId}/contestants
POST /contests/{contestId}/contestants
PATCH /contestants/{contestantId}
DELETE /contestants/{contestantId}
PATCH /prizes/{prizeId}
DELETE /prizes/{prizeId}
GET /contests - VS Code
POST /contests - Stoplight
public function testCanCreateContest()
{
$response = $this->actingAs(\App\User::first())
->postJson('/contests', ['name' => 'Test Contest']);
// Additional assertions
[$success, $message] = $this->validateWithOpenApi(
'/contests',
'POST',
$response
);
$this->assertTrue($success, $message);
}
trait ValidatesWithOpenApi
{
protected $validator;
protected function initValidator()
{
$spec_path = base_path('public/raffle-api-spec/reference/raffle-api.json');
$this->validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)
->fromJsonFile($spec_path)
->getResponseValidator();
}
protected function validateWithOpenApi($path, $method, TestResponse $response)
{
$this->initValidator();
$response = $this->convertResponse($response);
$operation = new OperationAddress($path, strtolower($method));
try {
$this->validator->validate($operation, $response);
$success = true;
$message = '';
} catch (\Exception $e) {
$success = false;
$message = $e->getMessage();
// Get additional validation messages from specific
// validation exceptions
}
return [$success, $message];
}
Test that requests result in validation error
public function testContestNameMustBeString()
{
$body = [
'name' => 123,
];
$response = $this->actingAs(\App\User::first())
->postJson('/contests', $body)
->assertStatus(400);
}
Setting up validation middleware in Laravel
<?php
// app/Http/Kernel.php
protected $routeMiddleware = [
// ...
'openapi-validation' => \League\OpenAPIValidation\PSR15\ValidationMiddleware::class,
];
// Routes file
Route::post('/contests', 'ContestController@create')
->name('contest.create')
->middleware('openapi-validation');
Need PSR-15 adapter for Laravel:
Handle validation exception
<?php
// app/Exceptions/Handler.php
public function render($request, Exception $exception)
{
if ($exception instanceof \League\OpenAPIValidation\PSR7\Exception\ValidationFailed) {
$message = $exception->getMessage();
// Additional logic to get more specific error message...
app()->abort(400, $message);
}
return parent::render($request, $exception);
}