squirrelphp/validator-cascade

Symfony Validator 的级联属性,以更灵活和易懂的方式重新实现了 Valid 约束

v3.1 2023-12-01 09:09 UTC

This package is auto-updated.

Last update: 2024-08-30 01:43:49 UTC


README

Build Status Test Coverage PHPStan Packagist Version PHP Version Software License

重新实现了 Symfony validator 组件中的 Valid 约束,作为 Cascade 属性,使用起来比 Valid 更直接,且没有令人惊讶的行为。

此组件与 5.x 版本的 Symfony validator 组件兼容(v2.x 支持注解/属性),与 6.x/7.x 版本兼容(v3.x 仅支持属性)并且将适应支持 Symfony 的未来版本(如果需要,将进行更改)。

安装

composer require squirrelphp/validator-cascade

目录

使用方法

Cascade 是一种约束验证,确保对象或对象数组通过 Symfony validator 组件进行验证,从而实现级联验证。

只有两个选项

  • groups 定义 Cascade 约束属于哪个验证组,与任何常规验证约束的行为相同。如果不定义 groups,则默认设置为 Default。只有当其中一个验证组匹配时,Cascade 约束才会执行。

  • trigger 定义要触发的子对象上的验证组。默认情况下仅触发 Default,因此如果您想触发其他验证组,您必须使用 trigger 指定它们。父对象的验证组永远不会级联。

就是这样!

示例

以下是一个实际应用中的常见示例:您可能有一个订单和多个可能的订单地址(一个用于发货,一个用于发票)以及不同的要求,某些地址可能为可选,但如果指定,则应进行验证。

$shippingAddress 展示了如何在子对象中触发特定验证组,在这种情况下,使电话号码成为信息的一部分的强制要求(通常适用于发货,但通常对于其他用途不是必需的),除了“Default”约束。

$invoiceAddress 仅在将验证组“alternateInvoiceAddress”传递给验证器时进行验证(如果用户选择了类似于“选择不同的发票地址”的选项)。电话号码是可选的,因为我们没有传递 trigger 选项,因此仅在 Address 对象中验证 Default 组。

use Squirrel\ValidatorCascade\Cascade;
use Symfony\Component\Validator\Constraints as Assert;

class Order
{
    /**
     * Validate $shippingAddress if validation with no validation
     * group or the "Default" validation group is triggered
     *
     * Validates "Default" and "phoneNumberMandatory" validation groups in $shippingAddress
     */
    #[Cascade(trigger: ['Default', 'phoneNumberMandatory'])]
    public Address $shippingAddress;

    /**
     * Validate $invoiceAddress only if validation group
     * "alternateInvoiceAddress" is passed to validator
     *
     * Validates only "Default" validation group in $invoiceAddress, so phone number is optional
     */
    #[Assert\NotNull(groups: ['alternateInvoiceAddress'])]
    #[Cascade(groups: ['alternateInvoiceAddress'])]
    public ?Address $invoiceAddress = null;
}

class Address
{
    #[Assert\Length(min: 1, max: 50)]
    public string $street = '';

    #[Assert\Length(min: 1, max: 50)]
    public string $city = '';

    #[Assert\Length(min: 1, max: 50, groups: ['phoneNumberMandatory'])]
    public string $phoneNumber = '';
}

$order = new Order();
$order->shippingAddress = new Address();
$order->invoiceAddress = new Address();

// This validates with the "Default" validation group,
// so only shippingAddress must be specified
$symfonyValidator->validate($order);

// This also validates the invoice address in addition
// to the shipping address
$symfonyValidator->validate($order, null, [
    "Default",
    "alternateInvoiceAddress",
]);

为什么不使用 Valid 约束?

当涉及到验证组时,Symfony validator 组件中 Valid 约束的实现存在严重限制,并且与其他约束的行为不同。

没有验证组的 Valid 约束

#[Assert\Valid]
public $someobject;

上述代码看起来像一个常规断言,但其行为不同

  • 无论您给验证器提供什么验证组,断言总是会执行
  • 因此,该断言不属于“Default”组

对于简单的对象或您不需要任何验证组的情况,这很好,但它仍然与其他断言不同,因为即使您以后添加验证组,也无法“跳过”此约束。

具有验证组的 Valid 约束

#[Assert\Valid(groups: ['invoice'])]
public $someobject;

上面的 Valid 断言仅在您验证“invoice”验证组时才会触发,这是您所期望的。然而,有很多意想不到的行为

  • 它只触发$someobject中的“发票”验证组,没有其他验证组传递给该对象(例如,如果您正在验证“默认”和“发票”组,则“默认”组永远不会到达$someobject,只有“发票”组)
  • 无法更改在$someobject中触发的验证组
  • 当定义验证组时,不使用Valid的“遍历”选项。尽管在一般情况下,“遍历”选项可能不应该使用或需要

将验证组同时用作触发器和过滤器会严重限制其使用方式,并且使得大多数使用情况(例如我们的地址示例)无法使用Valid实现。即使您设法使其工作,您的代码也不会自解释,并且容易出错或误解属性。

在此组件中定义的Cascade区分了约束属于哪个验证组以及子对象中触发的哪些验证组。它无法做到的是将父对象的验证组级联到子对象,因为此信息仅在验证组件的RecursiveContextualValidator类中可用,并且无法在不改变验证组件内部大量内容的情况下访问(遗憾的是)。