marcosh/php-validation-dsl

一种用于以函数式方式验证数据的DSL

0.4.0 2021-09-17 15:01 UTC

This package is auto-updated.

Last update: 2024-09-17 21:41:57 UTC


README

Scrutinizer Code Quality Code Climate Codacy Badge

一个用于以函数式方式验证通用数据的库。

基本思想

这个想法非常简单。所有操作都围绕以下接口进行

interface Validation
{
    public function validate($data): ValidationResult;
}

其中某个 $data 输入进来,并输出一个 ValidationResult

ValidationResult 是一个类型,可以是有效的,包含一些有效的 $data,或者无效的,包含一些错误信息。

这意味着验证要么成功,在这种情况下你会有一个有效的结果可供使用,要么失败,并且你有错误信息来处理。

不可变性

一切都是不可变的,所以一旦你创建了一个验证器,就不能修改它,你只能创建一个新的。

另一方面,不可变性意味着无状态,因此你可以安全地多次使用相同的验证器来处理不同的数据。

组合性

该库提供了两种不同的机制,可以组合使用,允许通过组合简单的验证器来创建复杂的验证器。

不言而喻,你可以创建新的验证器,与现有的验证器一起使用。

基本验证器

该库提供了一些基本验证器,用于验证原生数据结构。它们都实现了上述描述的 Validation 接口。

上下文

如何在构建验证器时使用运行时值来验证一些数据?这就是上下文的作用。

让我们提供一个激励示例:假设我们想写一个验证器来检查我们在更新集合中的成员时是否违反了唯一性条件。我们需要检查数据是否已经存在于我们的集合中,排除我们当前正在更新的记录。因此,除了数据本身外,我们还需要一个记录标识符,即我们当前正在更新的记录。我们可以将此信息传递到上下文中。然后验证器可能如下所示

class CheckDuplicateExceptCurrentRecord
{
    private $recordRepository;
    
    public function __construct($recordRepository)
    {
        $this->recordRepository = $recordRepository;
    }

    public function validate($data, $context)
    {
        if ($recordRepository->containsDataExcludingRecord($data, $context['id'])) {
            return ValidationResult::errors(['DUPLICATE RECORD']);
        }
        
        return ValidationResult::valid($data);
    }
}

然后我们可以像这样使用我们的验证器

$validator = new CheckDuplicateExceptCurrentRecord($recordRepository);

$validator->validate($data, ['id' => $currentRecordId]);

自定义错误格式化器

库本身不想强制如何构建和格式化错误信息。因此,它允许用户定义自己的错误信息和自己的错误信息结构。

为此,库中包含的每个验证器都可以使用自定义错误格式化器构建。

错误格式化器只是一个可调用的函数,它接收验证器已知的所有数据作为输入。通常,这些参数将只是需要验证的数据,但有时错误格式化器也可能接收验证器的配置参数。

例如,IsInstanceOf 验证器有一个命名的构造函数

public static function withClassNameAndFormatter(
    string $className,
    callable $errorFormatter
):

其中,$errorFormatter 需要是一个可调用的函数,接受参数 $className 和验证 $data。例如,我们可以这样使用它

$myValidator = IsInstanceOf::withClassNameAndFormatter(
    Foo::class,
    function ($className, $data) {
        return [
            sprintf(
                'The data %s is not an instance of %s',
                json_encode($data),
                $className
            )
        ];
    }
);

翻译器

自定义错误格式化器的一个特定用途是翻译错误信息。

每个验证器还有一个接受 Translator 的命名构造函数,以翻译库定义的错误信息。

如果您不想为每个定义的验证器指定翻译器,还有一个 TranslateErrors 组合器可以翻译返回信息中出现的所有字符串。

创建更复杂的验证器

上文提到,组合性是超越这个库的主要思想之一。事实上,我们不仅提供了一个机制,而是提供了两个机制,从基本验证器开始创建复杂验证器。

  • 使用组合器;组合器就是一个函数,以非常明确的方式从更简单的验证器创建一个新的验证器。这个库提供的组合器有:
  • 使用在Result/functions.php中提供的liftsdofdo函数来模拟在函数式编程中使用的应用性和单子验证。

在接下来的几节中,我们将提供如何使用这两种方法的示例。

使用组合器

假设你想验证以下格式的数据

[
    'name' => ... // non empty string
    'age' => ... // non-negative integer
]

为了用纯文本描述我们想要检查的内容,我们需要验证以下内容:

  • 我们接收的数据是一个数组
  • 我们有一个name字段,它应该是一个非空字符串
  • 我们有一个age字段,它应该是一个正整数

执行此检查的验证器应该如下所示

Sequence::validations([
    new IsArray(),
    All::validations([
        Sequence::validations([
            HasKey::withKey('name'),
            Focus::on(
                function ($data) {
                    return $data['name'];
                },
                Sequence::validations([
                    new IsString(),
                    new NonEmpty()
                ])
            )
        ]),
        Sequence::validations([
            HasKey::withKey('age'),
            Focus::on(
                function ($data) {
                    return $data['age'];
                },
                Sequence::validations([
                    new IsInteger(),
                    IsGreaterThan::withBound(0)
                ])
            )
        ])
    ])
]);

让我们一步一步地走,理解每一个细节。

我们从一个Sequence验证器开始。其语义是它将按顺序执行一系列验证,一个接一个,一旦出现错误就返回一个错误。

在我们的例子中,我们首先使用IsArray验证器检查我们的数据是否为数组。一旦我们知道我们有数组,我们可以检查是否存在这两个必需的字段。我们可以独立执行这两个操作,如果两者都失败,我们希望得到所有的错误信息。我们使用All验证器正是为了这个目的,表示所有列出的条件都需要验证,并且我们想要所有错误信息。

到这一点,我们有了nameage的验证。对于前者,我们首先使用HasKey验证器检查name键是否存在。然后我们想要验证name键的值;为此,我们需要将注意力集中在单个值上,而不是整个数据结构。我们使用Focus验证器来指定一个可调用的函数,允许检查特定的值。此时我们再次使用Sequence来检查该值是否为字符串,使用IsString验证器,并使用NonEmpty验证器断言它不为空。

对于age字段,我们做类似的事情。唯一的区别是我们使用IsInteger验证器检查它是否为整数,并使用一个用用户定义的可调用函数构建的IsAsAsserted验证器来检查值是否为正整数。

这个例子解释了如何使用多个组合器创建复杂的验证器。实际上,我们在这里考虑的具体验证器可以写成更简单的形式

Associative::validations([
    'name' => Sequence::validations([
        new IsString(),
        new NonEmpty()
    ]),
    'age' => Sequence::validations([
        new IsInteger(),
        IsGreaterThan::withBound(0)
    ])
]);

从客户端代码中移除大量模板代码,并允许集中精力处理验证的具体内容。

应用性和单子验证

应用性和单子验证是函数式编程中的常见技术。两者都涉及拥有多个验证器,应用它们,并组合它们的结果。它们之间的主要区别在于如何处理失败。

在应用风格中,所有验证器都是独立计算的,在失败的情况下,所有错误都返回。当需要验证独立数据并检索遇到的所有错误时,这很有用。

在单子风格中,验证器是顺序应用的,一个验证器的结果作为下一个验证器的输入。当验证依赖于上一个验证的结果时,这是必要的。

在这种情况下,一些代码可能比很多模糊的词语更能说明问题。你可以通过查看 ApplicativeMonadicSpec 测试 来了解应用性和单子验证是如何工作的。

如何使用这个库

这个库不是为了成为一个可以直接安装并立即使用的现成工具。相反,它只提供了一些基本元素,你可以使用这些元素轻松地构建自己的验证器。

的想法是,每个人都应该能够创建自己的验证器库,专门针对自己的领域和用例,通过提供的组合器来组合基本验证器和自定义验证器。