Stars: 167
Forks: 10
Pull Requests: 34
Issues: 37
Watchers: 13
Last Updated: 2021-10-06 10:50:42
It validates PSR-7 messages (HTTP request/response) against OpenAPI specifications
License: MIT License
Languages: PHP, Shell
https://lessthan12ms.com/openapi-with-php-documenting-and-testing-api-automatically/
Go to https://github.com/thephpleague/openapi-psr7-validator
This package is here for existing users only.
This package can validate PSR-7 messages against OpenAPI (3.0.x) specifications expressed in YAML or JSON.
composer require lezhnev74/openapi-psr7-validator
There are some specific terms that are used in the package. These terms come from OpenAPI:
specification - an OpenAPI document describing an API, expressed in JSON or YAML filedata - actual thing that we validate against a specification, including body and metadataschema - the part of the specification that describes the body of the request / responsekeyword - properties that are used to describe the instance are called key
words, or schema keywordspath - a relative path to an individual endpointoperation - a method that we apply on the path (like get /password)response - described response (includes status code, content types etc)You can validate \Psr\Http\Message\ServerRequestInterface instance like this:
$yamlFile = "api.yaml";
$jsonFile = "api.json";
$validator = (new \OpenAPIValidation\PSR7\ValidatorBuilder)->fromYamlFile($yamlFile)->getServerRequestValidator();
#or
$validator = (new \OpenAPIValidation\PSR7\ValidatorBuilder)->fromYaml(file_get_contents($yamlFile))->getServerRequestValidator();
#or
$validator = (new \OpenAPIValidation\PSR7\ValidatorBuilder)->fromJson(file_get_contents($jsonFile))->getServerRequestValidator();
#or
$validator = (new \OpenAPIValidation\PSR7\ValidatorBuilder)->fromJsonFile($jsonFile)->getServerRequestValidator();
#or
$schema = new \cebe\openapi\spec\OpenApi(); // generate schema object by hand
$validator = (new \OpenAPIValidation\PSR7\ValidatorBuilder)->fromSchema($schema)->getServerRequestValidator();
$match = $validator->validate($request);As a result you would get and OperationAddress $match which has matched the given request. If you already know
the operation which should match your request (i.e you have routing in your project), you can use
RouterRequestValidator
$address = new \OpenAPIValidation\PSR7\OperationAddress('/some/operation', 'post');
$validator = (new \OpenAPIValidation\PSR7\ValidatorBuilder)->fromSchema($schema)->getRoutedRequestValidator();
$validator->validate($address, $request);This would simplify validation a lot and give you more performance.
You can validate \Psr\Http\Message\RequestInterface instance like this:
$yamlFile = "api.yaml";
$jsonFile = "api.json";
$validator = (new \OpenAPIValidation\PSR7\ValidatorBuilder)->fromYamlFile($yamlFile)->getRequestValidator();
#or
$validator = (new \OpenAPIValidation\PSR7\ValidatorBuilder)->fromYaml(file_get_contents($yamlFile))->getRequestValidator();
#or
$validator = (new \OpenAPIValidation\PSR7\ValidatorBuilder)->fromJson(file_get_contents($jsonFile))->getRequestValidator();
#or
$validator = (new \OpenAPIValidation\PSR7\ValidatorBuilder)->fromJsonFile($jsonFile)->getRequestValidator();
#or
$schema = new \cebe\openapi\spec\OpenApi(); // generate schema object by hand
$validator = (new \OpenAPIValidation\PSR7\ValidatorBuilder)->fromSchema($schema)->getRequestValidator();
$match = $validator->validate($request);Validation of \Psr\Http\Message\ResponseInterface is a bit more complicated
. Because you need not only YAML file and Response itself, but also you need
to know which operation this response belongs to (in terms of OpenAPI).
Example:
$yamlFile = "api.yaml";
$jsonFile = "api.json";
$validator = (new \OpenAPIValidation\PSR7\ValidatorBuilder)->fromYamlFile($yamlFile)->getResponseValidator();
#or
$validator = (new \OpenAPIValidation\PSR7\ValidatorBuilder)->fromYaml(file_get_contents($yamlFile))->getResponseValidator();
#or
$validator = (new \OpenAPIValidation\PSR7\ValidatorBuilder)->fromJson(file_get_contents($jsonFile))->getResponseValidator();
#or
$validator = (new \OpenAPIValidation\PSR7\ValidatorBuilder)->fromJsonFile($jsonFile)->getResponseValidator();
#or
$schema = new \cebe\openapi\spec\OpenApi(); // generate schema object by hand
$validator = (new \OpenAPIValidation\PSR7\ValidatorBuilder)->fromSchema($schema)->getResponseValidator();
$operation = new \OpenAPIValidation\PSR7\OperationAddress('/password/gen', 'get') ;
$validator->validate($operation, $request);\OpenAPIValidation\PSR7\ValidatorBuilder reads and compiles schema in memory as instance of \cebe\openapi\spec\OpenApi. Validators use this instance to perform validation logic. You can reuse this instance after the validation like this:
$validator = (new \OpenAPIValidation\PSR7\ValidatorBuilder)->fromYamlFile($yamlFile)->getServerRequestValidator();
# or
$validator = (new \OpenAPIValidation\PSR7\ValidatorBuilder)->fromYamlFile($yamlFile)->getResponseValidator();
/** @var \cebe\openapi\spec\OpenApi */
$openApi = $validator->getSchema();\Psr\Http\Message\RequestInterface validation is not implemented.
PSR-15 middleware can be used like this:
$yamlFile = 'api.yaml';
$jsonFile = 'api.json';
$psr15Middleware = (new \OpenAPIValidation\PSR15\ValidationMiddlewareBuilder)->fromYamlFile($yamlFile)->getValidationMiddleware();
#or
$psr15Middleware = (new \OpenAPIValidation\PSR15\ValidationMiddlewareBuilder)->fromYaml(file_get_contents($yamlFile))->getValidationMiddleware();
#or
$psr15Middleware = (new \OpenAPIValidation\PSR15\ValidationMiddlewareBuilder)->fromJsonFile($jsonFile)->getValidationMiddleware();
#or
$psr15Middleware = (new \OpenAPIValidation\PSR15\ValidationMiddlewareBuilder)->fromJson(file_get_contents($jsonFile))->getValidationMiddleware();
#or
$schema = new \cebe\openapi\spec\OpenApi(); // generate schema object by hand
$validator = (new \OpenAPIValidation\PSR7\ValidationMiddlewareBuilder)->fromSchema($schema)->getValidationMiddleware();Slim framework uses slightly different middleware interface, so here is an adapter which you can use like this:
$yamlFile = 'api.yaml';
$jsonFile = 'api.json';
$psr15Middleware = (new \OpenAPIValidation\PSR15\ValidationMiddlewareBuilder)->fromYamlFile($yamlFile)->getValidationMiddleware();
#or
$psr15Middleware = (new \OpenAPIValidation\PSR15\ValidationMiddlewareBuilder)->fromYaml(file_get_contents($yamlFile))->getValidationMiddleware();
#or
$psr15Middleware = (new \OpenAPIValidation\PSR15\ValidationMiddlewareBuilder)->fromJsonFile($jsonFile)->getValidationMiddleware();
#or
$psr15Middleware = (new \OpenAPIValidation\PSR15\ValidationMiddlewareBuilder)->fromJson(file_get_contents($jsonFile))->getValidationMiddleware();
#or
$schema = new \cebe\openapi\spec\OpenApi(); // generate schema object by hand
$validator = (new \OpenAPIValidation\PSR7\ValidationMiddlewareBuilder)->fromSchema($schema)->getValidationMiddleware();
$slimMiddleware = new \OpenAPIValidation\PSR15\SlimAdapter($psr15Middleware);
/** @var \Slim\App $app */
$app->add($slimMiddleware);PSR-7 Validator has a built-in caching layer (based on PSR-6 interfaces) which saves time on parsing OpenAPI specs. It is optional. You enable caching if you pass a configured Cache Pool Object to the static constructor like this:
// Configure a PSR-6 Cache Pool
$cachePool = new ArrayCachePool();
// Pass it as a 2nd argument
$validator = (new \OpenAPIValidation\PSR7\ValidatorBuilder)
->fromYamlFile($yamlFile)
->setCache($cachePool)
->getResponseValidator();
# or
\OpenAPIValidation\PSR15\ValidationMiddleware::fromYamlFile($yamlFile, $cachePool);You can use ->setCache($pool, $ttl) call for both PSR-7 and PSR-15 builder in order to set
proper expiration ttl in seconds (or explicit null)
If you want take control over the cache key for schema item, or your cache does not support cache key generation by itself
you can ->overrideCacheKey('my_custom_key') to ensure cache uses key you want.
The package contains a standalone validator which can validate any data against an OpenAPI schema like this:
$spec = <<<SPEC
schema:
type: string
enum:
- a
- b
SPEC;
$data = "c";
$spec = cebe\openapi\Reader::readFromYaml($spec);
# (optional) reference resolving
$spec->resolveReferences(new ReferenceContext($spec, "/"));
$schema = new cebe\openapi\spec\Schema($spec->schema);
try {
(new \OpenAPIValidation\Schema\SchemaValidator())->validate($data, $schema);
} catch(\OpenAPIValidation\Schema\Exception\KeywordMismatch $e) {
// you can evaluate failure details
// $e->keyword() == "enum"
// $e->data() == "c"
// $e->dataBreadCrumb()->buildChain() -- only for nested data
}As you know, OpenAPI allows you to add formats to types:
schema:
type: string
format: binaryThis package contains a bunch of built-in format validators:
string type:
bytedatedate-timeemailhostnameipv4ipv6uriuuid (uuid4)number type
floatdoubleYou can also add your own formats. Like this:
# A format validator must be a callable
# It must return bool value (true if format matched the data, false otherwise)
# A callable class:
$customFormat = new class()
{
function __invoke($value): bool
{
return $value === "good value";
}
};
# Or just a closure:
$customFormat = function ($value): bool {
return $value === "good value";
};
# Register your callable like this before validating your data
\OpenAPIValidation\Schema\TypeFormats\FormatsContainer::registerFormat('string', 'custom', $customFormat);The package throws a list of various exceptions which you can catch and handle. There are some of them:
\OpenAPIValidation\Schema\Exception\KeywordMismatch - Indicates that data was not matched against a schema's keyword
\OpenAPIValidation\Schema\Exception\TypeMismatch - Validation for type keyword failed against a given data. For example type:string and value is 12\OpenAPIValidation\Schema\Exception\FormatMismatch - data mismatched a given type format. For example type: string, format: email won't match not-email.\OpenAPIValidation\PSR7\Exception\NoContentType - HTTP message(request/response) contains no Content-Type header. General HTTP errors.\OpenAPIValidation\PSR7\Exception\NoPath - path is not found in the spec\OpenAPIValidation\PSR7\Exception\NoOperation - operation os not found in the path\OpenAPIValidation\PSR7\Exception\NoResponseCode - response code not found under the operation in the spec\OpenAPIValidation\PSR7\Exception\ValidationFailed - generic exception for failed PSR-7 message\OpenAPIValidation\PSR7\Exception\Validation\InvalidBody - body does not match schema\OpenAPIValidation\PSR7\Exception\Validation\InvalidCookies - cookies does not match schema or missing required cookie\OpenAPIValidation\PSR7\Exception\Validation\InvalidHeaders - header does not match schema or missing required header\OpenAPIValidation\PSR7\Exception\Validation\InvalidPath - path does not match pattern or pattern values does not match schema\OpenAPIValidation\PSR7\Exception\Validation\InvalidQueryArgs - query args does not match schema or missing required argument\OpenAPIValidation\PSR7\Exception\Validation\InvalidSecurity - request does not match security schema or invalid security headers\OpenAPIValidation\PSR7\Exception\MultipleOperationsMismatchForRequest - request matched multiple operations in the spec,
but validation failed for all of them.You can run the tests with:
vendor/bin/phpunit
Feel free to open an Issue or add a Pull request. There is a certain code style that this package follows: doctrine/coding-standard.
To conform to this style please use a git hook, shipped with this package at .githooks/pre-commit.
How to use it:
ln -s -f ../../.githooks/pre-commit .git/hooks/pre-commitchmod +x .git/hooks/pre-commit.phpcs-report.txtPeople:
Resources:
The MIT License (MIT). Please see License.md file for more information.