shmax/graphql-php-validation-toolkit

对 graphql 查询和突变中的字段和参数进行验证,并动态生成用户错误类型

v2.2.1 2024-08-28 23:40 UTC

README

License PHPStan lvl-6 Coverage Status Latest Stable Version

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

类型生成过程是递归的,遍历任何嵌套的 InputObjectTypeListOf 类型,并检查它们的 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中检查动态生成的类型。

  1. 基本标量验证
  2. 自定义错误类型
  3. 输入对象验证
  4. 列表验证

贡献

欢迎贡献。请参阅CONTRIBUTING.md以获取指南。