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以获取指南。