[![Latest Stable Version](https://poser.pugx.org/league/openapi-psr7-validator/v/stable)](https://packagist.org/packages/league/openapi-psr7-validator) [![Build Status](https://travis-ci.org/thephpleague/openapi-psr7-validator.svg?branch=master)](https://travis-ci.org/thephpleague/openapi-psr7-validator) [![License](https://poser.pugx.org/league/openapi-psr7-validator/license)](https://packagist.org/packages/lezhnev74/openapi-psr7-validator) ![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg) # OpenAPI PSR-7 Message (HTTP Request/Response) Validator This package can validate PSR-7 messages against OpenAPI (3.0.x) specifications expressed in YAML or JSON. ![](image.jpg) ## Installation ``` composer require league/openapi-psr7-validator ``` ## OpenAPI (OAS) Terms 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 file - `data` - actual thing that we validate against a specification, including body and metadata - `schema` - the part of the specification that describes the body of the request / response - `keyword` - properties that are used to describe the instance are called key words, or schema keywords - `path` - a relative path to an individual endpoint - `operation` - a method that we apply on the path (like `get /password`) - `response` - described response (includes status code, content types etc) ## How To Validate ### ServerRequest Message You can validate `\Psr\Http\Message\ServerRequestInterface` instance like this: ```php $yamlFile = "api.yaml"; $jsonFile = "api.json"; $validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromYamlFile($yamlFile)->getServerRequestValidator(); #or $validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromYaml(file_get_contents($yamlFile))->getServerRequestValidator(); #or $validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromJson(file_get_contents($jsonFile))->getServerRequestValidator(); #or $validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromJsonFile($jsonFile)->getServerRequestValidator(); #or $schema = new \cebe\openapi\spec\OpenApi(); // generate schema object by hand $validator = (new \League\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` ```php $address = new \League\OpenAPIValidation\PSR7\OperationAddress('/some/operation', 'post'); $validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromSchema($schema)->getRoutedRequestValidator(); $validator->validate($address, $request); ``` This would simplify validation a lot and give you more performance. ### Request Message You can validate `\Psr\Http\Message\RequestInterface` instance like this: ```php $yamlFile = "api.yaml"; $jsonFile = "api.json"; $validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromYamlFile($yamlFile)->getRequestValidator(); #or $validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromYaml(file_get_contents($yamlFile))->getRequestValidator(); #or $validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromJson(file_get_contents($jsonFile))->getRequestValidator(); #or $validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromJsonFile($jsonFile)->getRequestValidator(); #or $schema = new \cebe\openapi\spec\OpenApi(); // generate schema object by hand $validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromSchema($schema)->getRequestValidator(); $match = $validator->validate($request); ``` ### Response Message 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: ```php $yamlFile = "api.yaml"; $jsonFile = "api.json"; $validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromYamlFile($yamlFile)->getResponseValidator(); #or $validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromYaml(file_get_contents($yamlFile))->getResponseValidator(); #or $validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromJson(file_get_contents($jsonFile))->getResponseValidator(); #or $validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromJsonFile($jsonFile)->getResponseValidator(); #or $schema = new \cebe\openapi\spec\OpenApi(); // generate schema object by hand $validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromSchema($schema)->getResponseValidator(); $operation = new \League\OpenAPIValidation\PSR7\OperationAddress('/password/gen', 'get') ; $validator->validate($operation, $response); ``` ### Reuse Schema After Validation `\League\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: ```php $validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromYamlFile($yamlFile)->getServerRequestValidator(); # or $validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromYamlFile($yamlFile)->getResponseValidator(); /** @var \cebe\openapi\spec\OpenApi */ $openApi = $validator->getSchema(); ``` ### PSR-15 Middleware PSR-15 middleware can be used like this: ```php $yamlFile = 'api.yaml'; $jsonFile = 'api.json'; $psr15Middleware = (new \League\OpenAPIValidation\PSR15\ValidationMiddlewareBuilder)->fromYamlFile($yamlFile)->getValidationMiddleware(); #or $psr15Middleware = (new \League\OpenAPIValidation\PSR15\ValidationMiddlewareBuilder)->fromYaml(file_get_contents($yamlFile))->getValidationMiddleware(); #or $psr15Middleware = (new \League\OpenAPIValidation\PSR15\ValidationMiddlewareBuilder)->fromJsonFile($jsonFile)->getValidationMiddleware(); #or $psr15Middleware = (new \League\OpenAPIValidation\PSR15\ValidationMiddlewareBuilder)->fromJson(file_get_contents($jsonFile))->getValidationMiddleware(); #or $schema = new \cebe\openapi\spec\OpenApi(); // generate schema object by hand $validator = (new \League\OpenAPIValidation\PSR7\ValidationMiddlewareBuilder)->fromSchema($schema)->getValidationMiddleware(); ``` ### SlimFramework Middleware Slim framework uses slightly different middleware interface, so here is an adapter which you can use like this: ```php $yamlFile = 'api.yaml'; $jsonFile = 'api.json'; $psr15Middleware = (new \League\OpenAPIValidation\PSR15\ValidationMiddlewareBuilder)->fromYamlFile($yamlFile)->getValidationMiddleware(); #or $psr15Middleware = (new \League\OpenAPIValidation\PSR15\ValidationMiddlewareBuilder)->fromYaml(file_get_contents($yamlFile))->getValidationMiddleware(); #or $psr15Middleware = (new \League\OpenAPIValidation\PSR15\ValidationMiddlewareBuilder)->fromJsonFile($jsonFile)->getValidationMiddleware(); #or $psr15Middleware = (new \League\OpenAPIValidation\PSR15\ValidationMiddlewareBuilder)->fromJson(file_get_contents($jsonFile))->getValidationMiddleware(); #or $schema = new \cebe\openapi\spec\OpenApi(); // generate schema object by hand $validator = (new \League\OpenAPIValidation\PSR7\ValidationMiddlewareBuilder)->fromSchema($schema)->getValidationMiddleware(); $slimMiddleware = new \League\OpenAPIValidation\PSR15\SlimAdapter($psr15Middleware); /** @var \Slim\App $app */ $app->add($slimMiddleware); ``` ### Caching Layer / PSR-6 Support PSR-7 Validator has a built-in caching layer (based on [PSR-6](https://www.php-fig.org/psr/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: ```php // Configure a PSR-6 Cache Pool $cachePool = new ArrayCachePool(); // Pass it as a 2nd argument $validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder) ->fromYamlFile($yamlFile) ->setCache($cachePool) ->getResponseValidator(); # or \League\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`)](https://www.php-fig.org/psr/psr-6/#definitions) 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. ### Standalone OpenAPI Validator The package contains a standalone validator which can validate any data against an OpenAPI schema like this: ```php $spec = <<resolveReferences(new ReferenceContext($spec, "/")); $schema = new cebe\openapi\spec\Schema($spec->schema); try { (new \League\OpenAPIValidation\Schema\SchemaValidator())->validate($data, $schema); } catch(\League\OpenAPIValidation\Schema\Exception\KeywordMismatch $e) { // you can evaluate failure details // $e->keyword() == "enum" // $e->data() == "c" // $e->dataBreadCrumb()->buildChain() -- only for nested data } ``` ## Custom Type Formats As you know, OpenAPI allows you to add formats to types: ```yaml schema: type: string format: binary ``` This package contains a bunch of built-in format validators: - `string` type: - `byte` - `date` - `date-time` - `email` - `hostname` - `ipv4` - `ipv6` - `uri` - `uuid` (uuid4) - `number` type - `float` - `double` You can also add your own formats. Like this: ```php # 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 \League\OpenAPIValidation\Schema\TypeFormats\FormatsContainer::registerFormat('string', 'custom', $customFormat); ``` ## Exceptions The package throws a list of various exceptions which you can catch and handle. There are some of them: - Schema related: - `\League\OpenAPIValidation\Schema\Exception\KeywordMismatch` - Indicates that data was not matched against a schema's keyword - `\League\OpenAPIValidation\Schema\Exception\TypeMismatch` - Validation for `type` keyword failed against a given data. For example `type:string` and value is `12` - `\League\OpenAPIValidation\Schema\Exception\FormatMismatch` - data mismatched a given type format. For example `type: string, format: email` won't match `not-email`. - PSR7 Messages related: - `\League\OpenAPIValidation\PSR7\Exception\NoContentType` - HTTP message(request/response) contains no Content-Type header. General HTTP errors. - `\League\OpenAPIValidation\PSR7\Exception\NoPath` - path is not found in the spec - `\League\OpenAPIValidation\PSR7\Exception\NoOperation` - operation os not found in the path - `\League\OpenAPIValidation\PSR7\Exception\NoResponseCode` - response code not found under the operation in the spec - Validation exceptions (check parent exception for possible root causes): - `\League\OpenAPIValidation\PSR7\Exception\ValidationFailed` - generic exception for failed PSR-7 message - `\League\OpenAPIValidation\PSR7\Exception\Validation\InvalidBody` - body does not match schema - `\League\OpenAPIValidation\PSR7\Exception\Validation\InvalidCookies` - cookies does not match schema or missing required cookie - `\League\OpenAPIValidation\PSR7\Exception\Validation\InvalidHeaders` - header does not match schema or missing required header - `\League\OpenAPIValidation\PSR7\Exception\Validation\InvalidPath` - path does not match pattern or pattern values does not match schema - `\League\OpenAPIValidation\PSR7\Exception\Validation\InvalidQueryArgs` - query args does not match schema or missing required argument - `\League\OpenAPIValidation\PSR7\Exception\Validation\InvalidSecurity` - request does not match security schema or invalid security headers - Request related: - `\League\OpenAPIValidation\PSR7\Exception\MultipleOperationsMismatchForRequest` - request matched multiple operations in the spec, but validation failed for all of them. ## Testing You can run the tests with: ``` vendor/bin/phpunit ``` ## Contribution Guide Feel free to open an Issue or add a Pull request. There is a certain code style that this package follows: [doctrine/coding-standard](https://www.doctrine-project.org/projects/doctrine-coding-standard/en/latest/reference/index.html#introduction). To conform to this style please use a git hook, shipped with this package at `.githooks/pre-commit`. How to use it: 1. Clone the package locally and navigate to the folder 2. Create a symlink to the hook like this: `ln -s -f ../../.githooks/pre-commit .git/hooks/pre-commit` 3. Add execution rights: `chmod +x .git/hooks/pre-commit` 4. Now commit any new changes and the code will be checked and formatted accordingly. 5. If there are any issues with your code, check the log here: `.phpcs-report.txt` ## Credits People: - [Dmitry Lezhnev](https://github.com/lezhnev74) - [Carsten Brandt](https://github.com/cebe) - [Samuel Nela](https://github.com/samnela) - [Pavel Batanov](https://github.com/scaytrase) - [Christopher L Bray](https://github.com/brayniverse) - [David Pauli](https://github.com/dpauli) - [Jason Judge](https://github.com/judgej) - [Yannick Chenot](https://github.com/osteel) - [TarasBK](https://github.com/TarasBK) - [Jason B. Standing](https://github.com/jasonbstanding) - [Dmytro Demchyna](https://github.com/dmytro-demchyna) - [Will Chambers](https://github.com/willchambers99) - [Ignacio](https://github.com/imefisto) - A big thank you to [Henrik Karlström](https://github.com/hkarlstrom) who kind of inspired me to work on this package. Resources: - Icons made by Freepik, licensed by CC 3.0 BY - [cebe/php-openapi](https://github.com/cebe/php-openapi) package for Reading OpenAPI files - [slim3-psr15](https://github.com/bnf/slim3-psr15) package for Slim middleware adapter ## License The MIT License (MIT). Please see `License.md` file for more information. ## TODO - [ ] Support Discriminator Object (note: apparently, this is not so straightforward, as discriminator can point to any external scheme)