shmax / graphql-php-validation-toolkit
对 graphql 查询和突变中的字段和参数进行验证,并动态生成用户错误类型
Requires
- php: ^8.1
- ext-intl: *
- ext-json: *
- ext-mbstring: *
Requires (Dev)
- mll-lab/php-cs-fixer-config: ^5.0
- phpbench/phpbench: ^1.2.0
- phpstan/phpstan: 1.10.6
- phpstan/phpstan-phpunit: 1.3.10
- phpstan/phpstan-strict-rules: 1.5.0
- phpunit/phpunit: ^9.5
- webonyx/graphql-php: ^v15.3.0
README
GraphQL 在验证类型和检查语法方面非常出色,但在提供用户输入的额外验证方面并不太有帮助。GraphQL 的作者普遍认为,对于错误的用户输入,正确的响应不是抛出异常,而是返回任何验证反馈以及结果。
正如 Lee Byron 在这里解释的
...允许在突变的数据负载中包含用户界面的报告数据。通常情况下,突变的数据负载包括一个 "didSucceed" 字段和一个 "userError" 字段。如果你的 UI 需要有关潜在错误的丰富信息,那么你应该也在数据负载中包含这些信息。
这就是这个小库的用武之地。
graphql-php-validation-toolkit 通过新的 ValidatedFieldDefinition 类扩展了由出色的 graphql-php 库提供的内置定义。只需在常规字段配置中实例化这些类之一,将 validate 回调属性添加到你的 args 定义中,你的字段的 type 将被替换为一个新的、动态生成的 ResultType,其中包含每个参数的可查询错误字段。这是一个递归过程,所以你的 args 可以具有具有子字段和 validate 回调的 InputObjectType 类型。你最初定义的 type 将移动到生成的类型的 result 字段。
安装
通过 composer
composer require shmax/graphql-php-validation-toolkit
文档
基本用法
简而言之,用 ValidatedFieldDefinition 的实例替换你的常规字段定义,并将 validate 回调添加到一个或多个 args 配置中。比如说你想创建一个名为 updateBook 的突变
//... 'updateBook' => new ValidatedFieldDefinition([ 'name' => 'updateBook', 'type' => Types::book(), 'args' => [ 'bookId' => [ 'type' => Type::id(), 'validate' => function ($bookId) { global $books; if (!Book::find($bookId) { return 0; } return [1, 'Unknown book!']; }, ], ], 'resolve' => static function ($value, $args) : bool { return Book::find($args['bookId']); }, ],
在上面的示例中,你的字段定义的 book 类型属性将被替换为一个新的动态生成的类型,称为 UpdateBookResultType。
类型生成过程是递归的,遍历任何嵌套的 InputObjectType 或 ListOf 类型,并检查它们的 fields 以查找更多的 validate 回调。每个具有 validate 回调的字段定义--包括最顶层的一个--都将表示为具有以下可查询字段的自定义生成的类型
顶层的 <field-name>ResultType 将有一些额外的字段
然后你可以简单地查询这些字段以及 result
mutation { updateAuthor( authorId: 1 ) { valid result { id name } code msg suberrors { authorId { code msg } } } }
验证回调
任何字段定义都可以有一个 validate 回调。传递给 validate 回调的第一个参数是要验证的值。如果值有效,返回 0,否则返回 1。
//... 'updateAuthor' => new ValidatedFieldDefinition([ 'type' => Types::author(), 'args' => [ 'authorId' => [ 'validate' => function(string $authorId) { if(Author::find($authorId)) { return 0; } return 1; } ] ] ])
required 属性
您可以标记任何字段为必填,如果未提供值,则会自动进行验证(从而无需您使用null类型来降低验证回调的验证强度)。您可以将其设置为true,或者提供类似于您验证回调返回的错误数组。您还可以将其设置为返回相同布尔值或错误数组的可调用对象。
//... 'updateThing' => new ValidatedFieldDefinition([ 'type' => Types::thing(), 'args' => [ 'foo' => [ 'required' => true, // if not provided, then an error of the form [1, 'foo is required'] will be returned. 'validate' => function(string $foo) { if(Foo::find($foo)) { return 0; } return 1; } ], 'bar' => [ 'required' => [1, 'Oh, where is the bar?!'], 'validate' => function(string $bar) { if(Bar::find($bar)) { return 0; } return 1; } ], 'naz' => [ 'required' => static fn() => !Moderator::loggedIn(), 'validate' => function(string $naz) { if(Naz::find($naz)) { return 0; } return 1; } ] ] ])
如果您想返回错误信息,请返回一个包含信息的数组,该信息位于第二个存储桶中
//... 'updateAuthor' => new ValidatedFieldDefinition([ 'type' => Types::author(), 'args' => [ 'authorId' => [ 'validate' => function(string $authorId) { if(Author::find($authorId)) { return 0; } return [1, "We can't find that author"]; } ] ] ])
生成的ListOf错误类型还具有一个path字段,您可以查询该字段,以便知道每个验证失败项在多维数组中的确切地址
//... 'setPhoneNumbers' => new ValidatedFieldDefinition([ 'type' => Types::bool(), 'args' => [ 'phoneNumbers' => [ 'type' => Type::listOf(Type::string()), 'validate' => function(string $phoneNumber) { $res = preg_match('/^[0-9\-]+$/', $phoneNumber) === 1; if (!$res) { return [1, 'That does not seem to be a valid phone number']; } return 0; } ] ] ])
自定义错误代码
如果您想使用自定义错误代码,在验证回调同一级别添加一个errorCodes属性,并提供PHP原生枚举的路径
enum AuthorErrors { case AuthorNotFound; } 'updateAuthor' => [ 'type' => Types::author(), 'errorCodes' => AuthorErrors::class, 'validate' => function(string $authorId) { if(Author::find($authorId)) { return 0; } return [AuthorErrors::AuthorNotFound, "We can't find that author"]; } ]
请注意,库将为错误代码类型生成唯一名称,并且它们的长度可能会很长,这取决于它们在字段结构中的嵌套深度
echo $errorType->name; // Author_Attributes_FirstName_PriceErrorCode
如果这成为您的问题,请确保提供一个类型设置器(请参阅示例),它返回已设置的类型,然后生成的名称将是传入枚举类的名称加上"ErrorCode"。
echo $errorType->name; // PriceErrorCode
管理创建的类型
此库将根据需要创建新类型。如果您使用某种类型管理器来存储和检索类型,可以通过提供typeSetter回调将其集成。确保它返回已设置的类型。
new ValidatedFieldDefinition([ 'typeSetter' => static function ($type) { return Types::set($type); }, ]);
示例
了解所有这些功能的最佳方式是进行实验。在/examples文件夹中有一系列越来越复杂的单页示例。每个示例都附有自己的README.md,其中包含运行代码的说明。运行每个示例,并确保在ChromeiQL中检查动态生成的类型。
贡献
欢迎贡献。请参阅CONTRIBUTING.md以获取指南。