league/openapi-psr7-validator

验证 PSR-7 消息是否符合 OpenAPI (3.0.2) 规范,规范用 YAML 或 JSON 表达

0.22 2024-01-10 19:11 UTC

README

Latest Stable Version Build Status License contributions welcome

OpenAPI PSR-7 消息(HTTP 请求/响应)验证器

此包可以验证 PSR-7 消息是否符合用 YAML 或 JSON 表达的 OpenAPI (3.0.x) 规范。

安装

composer require league/openapi-psr7-validator

OpenAPI (OAS) 术语

包中使用了某些特定术语。这些术语来自 OpenAPI

  • specification - 一个 OpenAPI 文档,描述一个 API,以 JSON 或 YAML 文件表达
  • data - 实际上与规范进行验证的东西,包括正文和元数据
  • schema - 规范中描述请求/响应正文的那个部分
  • keyword - 用于描述实例的属性被称为关键字,或 schema 关键字
  • path - 到单个端点的相对路径
  • operation - 我们在路径上应用的方法(例如 get /password
  • response - 描述的响应(包括状态码、内容类型等)

如何验证

ServerRequest 消息

您可以像这样验证 \Psr\Http\Message\ServerRequestInterface 实例

$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);

结果您将获得一个 OperationAddress $match,它与给定的请求匹配。如果您已经知道应该匹配您的请求的操作(即您在项目中具有路由),则可以使用 RouterRequestValidator

$address = new \League\OpenAPIValidation\PSR7\OperationAddress('/some/operation', 'post');

$validator = (new \League\OpenAPIValidation\PSR7\ValidatorBuilder)->fromSchema($schema)->getRoutedRequestValidator();

$validator->validate($address, $request);

这将大大简化验证并提高性能。

请求消息

您可以像这样验证 \Psr\Http\Message\RequestInterface 实例

$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);

响应消息

\Psr\Http\Message\ResponseInterface 的验证稍微复杂一些。因为您需要不仅需要 YAML 文件和响应本身,还需要知道这个响应属于哪个操作(从 OpenAPI 的角度来说)。

示例

$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);

验证后重用模式

\League\OpenAPIValidation\PSR7\ValidatorBuilder 读取并编译内存中的模式,作为 \cebe\openapi\spec\OpenApi 的实例。验证器使用此实例来执行验证逻辑。您可以在验证后重用此实例,如下所示

$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 中间件

PSR-15 中间件可以像这样使用

$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 中间件

Slim 框架使用略微不同的中间件接口,因此这里有一个适配器,您可以使用它,如下所示

$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);

缓存层/PSR-6 支持

PSR-7 验证器有一个内置的缓存层(基于 PSR-6 接口),可以节省解析 OpenAPI 规范的时间。这是可选的。您可以通过像这样传递配置的缓存池对象来启用缓存

// 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
$psr15Middleware = (new \OpenAPIValidation\PSR15\ValidationMiddlewareBuilder)
    ->fromYamlFile($yamlFile)
    ->setCache($cachePool)
    ->getValidationMiddleware();

您可以使用 ->setCache($pool, $ttl) 调用为 PSR-7 和 PSR-15 构建器设置 正确的过期 ttl(以秒为单位)或显式 null

如果您想控制模式项的缓存键,或者您的缓存不支持自动生成缓存键,您可以使用 ->overrideCacheKey('my_custom_key') 来确保缓存使用您想要的键。

独立 OpenAPI 验证器

该包包含一个独立验证器,可以像这样验证任何数据是否与 OpenAPI 模式匹配

$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 \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
}

自定义类型格式

如您所知,OpenAPI 允许您向类型添加格式

schema:
  type: string
  format: binary

此包包含一组内置格式验证器

  • string 类型
    • 字节
    • 日期
    • 日期时间
    • 电子邮件
    • 主机名
    • IPv4
    • IPv6
    • URI
    • uuid(uuid4)
  • number 类型
    • 浮点数
    • double

您还可以添加自己的格式。例如这样

# 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);

异常

该包抛出一系列异常,您可以选择捕获和处理。其中一些如下

  • 模式相关
    • \League\OpenAPIValidation\Schema\Exception\KeywordMismatch - 表示数据未与模式的关键字匹配
      • \League\OpenAPIValidation\Schema\Exception\TypeMismatch - 对 type 关键字的验证失败,例如 type:string 和值是 12
      • \League\OpenAPIValidation\Schema\Exception\FormatMismatch - 数据与给定的类型格式不匹配。例如 type: string, format: email 不会匹配 not-email
  • PSR7 消息相关
    • \League\OpenAPIValidation\PSR7\Exception\NoContentType - HTTP 消息(请求/响应)不包含 Content-Type 标头。通用 HTTP 错误。
    • \League\OpenAPIValidation\PSR7\Exception\NoPath - 路径在规范中未找到
    • \League\OpenAPIValidation\PSR7\Exception\NoOperation - 路径中未找到操作
    • \League\OpenAPIValidation\PSR7\Exception\NoResponseCode - 规范中未找到操作下的响应代码
    • 验证异常(检查父异常以确定可能的原因)
      • \League\OpenAPIValidation\PSR7\Exception\ValidationFailed - 失败的 PSR-7 消息的通用异常
      • \League\OpenAPIValidation\PSR7\Exception\Validation\InvalidBody - 主体与模式不匹配
      • \League\OpenAPIValidation\PSR7\Exception\Validation\InvalidCookies - cookies 与模式不匹配或缺少必需的 cookie
      • \League\OpenAPIValidation\PSR7\Exception\Validation\InvalidHeaders - 标头与模式不匹配或缺少必需的标头
      • \League\OpenAPIValidation\PSR7\Exception\Validation\InvalidPath - 路径与模式不匹配或模式值不匹配模式
      • \League\OpenAPIValidation\PSR7\Exception\Validation\InvalidQueryArgs - 查询参数与模式不匹配或缺少必需的参数
      • \League\OpenAPIValidation\PSR7\Exception\Validation\InvalidSecurity - 请求与安全模式不匹配或不正确的安全标头
    • 请求相关
      • \League\OpenAPIValidation\PSR7\Exception\MultipleOperationsMismatchForRequest - 请求与规范中的多个操作匹配,但所有操作的验证均失败。

测试

您可以使用以下命令运行测试

vendor/bin/phpunit

贡献指南

请随意提交 Issue 或添加 Pull request。该包遵循一定的代码风格: doctrine/coding-standard

要符合此风格,请使用随该包一起提供的 git hook,位于 .githooks/pre-commit

如何使用它

  1. 在本地克隆包并导航到文件夹
  2. 创建到钩子的符号链接,如下所示: ln -s -f ../../.githooks/pre-commit .git/hooks/pre-commit
  3. 添加执行权限: chmod +x .git/hooks/pre-commit
  4. 现在提交任何新更改,代码将相应地进行检查和格式化。
  5. 如果您的代码有任何问题,请在此处检查日志: .phpcs-report.txt

鸣谢

人物

资源

  • 图标由 Freepik 制作,经 CC 3.0 BY 许可
  • cebe/php-openapi 包用于读取 OpenAPI 文件
  • slim3-psr15 包用于 Slim 中间件适配器

许可证

MIT 许可证(MIT)。请参阅 License.md 文件以获取更多信息。

待办事项

  • 支持区分符对象(注意:显然,这并不简单,因为区分符可以指向任何外部模式)