league/openapi-psr7-validator
Install the package:
composer require league/openapi-psr7-validator
Load your OpenAPI spec (YAML/JSON):
use League\OpenAPIValidation\PSR7\ValidatorBuilder;
$validator = (new ValidatorBuilder())
->fromYamlFile(__DIR__.'/api.yaml')
->getServerRequestValidator();
Validate a PSR-7 request (e.g., in a middleware or controller):
$match = $validator->validate($request);
Validate incoming requests against your OpenAPI spec before routing:
$validator = (new ValidatorBuilder())
->fromYamlFile(config('openapi.spec'))
->getServerRequestValidator();
try {
$match = $validator->validate($request);
// Proceed with matched operation
} catch (\League\OpenAPIValidation\PSR7\Exception\ValidationFailed $e) {
abort(400, 'Invalid request: ' . $e->getMessage());
}
Workflow:
Create middleware in app/Http/Middleware/ValidateOpenApi.php:
namespace App\Http\Middleware;
use League\OpenAPIValidation\PSR15\ValidationMiddlewareBuilder;
use Closure;
class ValidateOpenApi
{
public function __construct()
{
$this->middleware = (new ValidationMiddlewareBuilder())
->fromYamlFile(config('openapi.spec'))
->getValidationMiddleware();
}
public function handle($request, Closure $next)
{
return $this->middleware->process($request, $next);
}
}
Register in app/Http/Kernel.php:
protected $middleware = [
\App\Http\Middleware\ValidateOpenApi::class,
];
For known routes, use RoutedRequestValidator:
use League\OpenAPIValidation\PSR7\OperationAddress;
public function store(Request $request)
{
$validator = (new ValidatorBuilder())
->fromYamlFile(config('openapi.spec'))
->getRoutedRequestValidator();
$address = new OperationAddress('/users', 'post');
$validator->validate($address, $request);
// Proceed with business logic
}
Validate API responses before sending:
use League\OpenAPIValidation\PSR7\OperationAddress;
public function show(User $user)
{
$response = response()->json($user);
$validator = (new ValidatorBuilder())
->fromYamlFile(config('openapi.spec'))
->getResponseValidator();
$address = new OperationAddress('/users/{id}', 'get');
$validator->validate($address, $response);
return $response;
}
Extend built-in formats (e.g., for Laravel's Carbon instances):
use League\OpenAPIValidation\Schema\TypeFormats\FormatsContainer;
FormatsContainer::registerFormat('string', 'date-time', function ($value) {
return Carbon::parse($value) instanceof Carbon;
});
Cache the OpenAPI schema to avoid re-parsing:
use League\OpenAPIValidation\PSR7\ValidatorBuilder;
use Symfony\Contracts\Cache\CacheInterface;
public function __construct(CacheInterface $cache)
{
$this->validator = (new ValidatorBuilder())
->fromYamlFile(config('openapi.spec'))
->setCache($cache, 3600) // 1-hour TTL
->getServerRequestValidator();
}
Case-Sensitive Paths OpenAPI paths are case-sensitive. Ensure your Laravel routes match exactly:
# api.yaml
paths:
/users:
get: ...
// Laravel route (correct)
Route::get('/users', ...);
// Laravel route (will fail validation)
Route::get('/Users', ...);
Content-Type Headers
Missing or incorrect Content-Type headers cause NoContentType exceptions. Always set them:
$request->getBody()->rewind();
$request = $request->withHeader('Content-Type', 'application/json');
Query Parameter Validation
OpenAPI validates query params strictly. Use explode for multi-value params:
# api.yaml
parameters:
- in: query
name: tags
schema:
type: array
items:
type: string
// Laravel request (correct)
$request->query('tags', ['tag1', 'tag2']);
// Laravel request (will fail)
$request->query('tags', 'tag1,tag2');
Response Status Codes Validate responses against the exact status code defined in OpenAPI:
# api.yaml
responses:
200:
description: OK
// Laravel response (correct)
return response()->json([], 200);
// Laravel response (will fail)
return response()->json([], 201); // Not in OpenAPI spec
Inspect Validation Errors
Catch ValidationFailed and inspect nested exceptions:
try {
$validator->validate($request);
} catch (\League\OpenAPIValidation\PSR7\Exception\ValidationFailed $e) {
foreach ($e->getErrors() as $error) {
dump($error->getMessage()); // e.g., "Invalid query parameter 'limit': must be integer"
}
}
Validate Standalone Schemas Test JSON schemas independently:
use League\OpenAPIValidation\Schema\SchemaValidator;
$validator = new SchemaValidator();
$schema = cebe\openapi\Reader::readFromYaml(file_get_contents('api.yaml'))->schema;
try {
$validator->validate($data, $schema);
} catch (\League\OpenAPIValidation\Schema\Exception\KeywordMismatch $e) {
dump($e->keyword(), $e->data()); // e.g., "type", "string"
}
Log Matched Operations Debug route matching:
$match = $validator->validate($request);
dump($match->getPath(), $match->getMethod()); // e.g., "/users", "GET"
Custom Error Responses Override default error handling in middleware:
public function handle($request, Closure $next)
{
try {
return $next($request);
} catch (\League\OpenAPIValidation\PSR7\Exception\ValidationFailed $e) {
return response()->json([
'errors' => collect($e->getErrors())
->map(fn($error) => $error->getMessage())
->all()
], 400);
}
}
Dynamic Schema Loading Load OpenAPI specs from a database or external API:
$spec = Cache::remember('openapi-spec', 3600, function () {
return file_get_contents(config('openapi.url'));
});
$validator = (new ValidatorBuilder())
->fromJson($spec)
->getServerRequestValidator();
Integration with Laravel Packages
$validator->validate(
new OperationAddress('/users/{id}', 'get'),
response()->json($this->toArray($user))
);
# api.yaml
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
security:
- bearerAuth: []
Testing Validations Use in PHPUnit tests:
public function test_valid_request()
{
$validator = (new ValidatorBuilder())
->fromYamlFile(__DIR__.'/api.yaml')
->getServerRequestValidator();
$request = new ServerRequest(
'GET',
'/users',
['Content-Type' => 'application/json']
);
$this->assertInstanceOf(
OperationAddress::class,
$validator->validate($request)
);
}
Schema Caching
$validator->setCache(new ArrayCachePool(), 86400); // 24h TTL
$validator->overrideCacheKey('shared-openapi-spec');
Middleware Overhead
How can I help you explore Laravel packages today?