快速验证和对象注水功能的简单表单系统

v0.1.0 2023-05-06 17:12 UTC

This package is auto-updated.

Last update: 2024-09-21 15:52:49 UTC


README

GitHub Actions status Codecov status Packagist version

这是一个具有快速验证和对象注水功能的简单表单系统。此库提供了一种简单且高效的方式来验证表单输入数据,并用验证后的数据填充对象。它使用代码生成来提高性能,并提供了开发时的运行时回退。

库的哲学是

  • 使用PHP结构来定义您的表单,而不是使用DSL(如构建器或YAML文件)构建它们。
  • 声明的结构是唯一实际用于验证和注水的结构。不会使用任何神秘的内部结构,也不会重复数据。
  • 组件主要是不可变的,允许缓存表单实例。
  • 大量使用代码生成来提高性能。

安装

您可以使用Composer安装此库

composer require vincent4vx/form

用法

Bootstrap

要使用表单验证库,首先需要初始化注册表。可以使用适配器到PSR-11兼容的依赖注入容器,例如PHP-DI或Symfony DI。如果您不使用依赖注入容器,则可以使用DefaultRegistry类。

初始化您的容器后,您可以使用它来实例化库的表单工厂。以下是一个示例,展示了如何使用库提供的ContainerRegistry类来实例化工厂

use Quatrevieux\Form\ContainerRegistry;
use Quatrevieux\Form\DefaultFormFactory;
use Quatrevieux\Form\Util\Functions;

// Initialize the container registry with your PSR-11 container.
$registry = new ContainerRegistry($container);

// Instantiate the runtime form factory (for development)
$factory = DefaultFormFactory::runtime($registry);

// Instantiate the generated form factory (for production)
$factory = DefaultFormFactory::generated(
    registry: $registry,
    savePathResolver: Functions::savePathResolver(self::GENERATED_DIR),
);

简单用法

实例化表单工厂后,您可以使用它来创建您的表单。首先,您需要通过声明包含的字段以及验证和转换规则属性来声明表单。

以下是一个用户注册表单的声明示例

use Quatrevieux\Form\Validator\Constraint\Length;
use Quatrevieux\Form\Validator\Constraint\Regex;
use Quatrevieux\Form\Validator\Constraint\PasswordStrength;
use Quatrevieux\Form\Validator\Constraint\EqualsWith;
use Quatrevieux\Form\Validator\Constraint\ValidateVar;
use Quatrevieux\Form\Component\Csrf\Csrf;

class RegistrationRequest
{
    // Non-nullable field will be considered as required
    #[Length(min: 3, max: 256), Regex('/^[a-zA-Z0-9_]+$/')]
    public string $username; 

    #[PasswordStrength]
    public string $password;

    // You can define custom messages for each constraint
    #[EqualsWith('password', message: 'Password confirmation must match the password'))]
    public string $passwordConfirmation;

    #[ValidateVar(ValidateVar::EMAIL)]
    public string $email;
    
    // Optional fields can be defined using the "?" operator, making them nullable
    #[Length(min: 3, max: 256)]
    public ?string $name;
    
    // You can use the Csrf component to add a CSRF token to your form (requires symfony/security-csrf)
    #[Csrf]
    public string $csrf;
}

要使用此表单,您可以使用表单工厂创建其实例,然后使用它来验证和注水您的数据

$form = $factory->create(RegistrationRequest::class);

// submit() will validate the data and return a SubmittedForm object
// The value must be stored in a variable, because the $form object is immutable
$submitted = $form->submit($_POST);

if ($submitted->valid()) {
    // If the submitted data is valid, you can access the form data, which is an instance of the RegistrationRequest class:
    $value = $submitted->value();
    
    $value->username;
    $value->password;
    // ...
} else {
    // If the submitted data is invalid, you can access the errors like this:
    $errors = $submitted->errors();
    
    /** @var \Quatrevieux\Form\Validator\FieldError $error */
    foreach ($errors as $field => $error) {
        $error->localizedMessage(); // The error message, translated using the translator
        $error->code; // UUID of the constraint that failed
        $error->parameters; // Failed constraint parameters. For example, the "min" and "max" parameters for the Length constraint
    }
}

嵌入式表单

您可以使用EmbeddedArrayOf组件将表单嵌入到其他表单中。

use Quatrevieux\Form\Embedded\ArrayOf;
use Quatrevieux\Form\Embedded\Embedded;
use Quatrevieux\Form\Validator\Constraint\Length;
use Quatrevieux\Form\Validator\Constraint\PasswordStrength;

class User
{
    #[Length(min: 3, max: 12)]
    public string $pseudo;

    // Use the Embedded attribute component to embed a form into another form
    // Note: you can mark the property as nullable to make it optional
    #[Embedded(Credentials::class)]
    public Credentials $credentials;

    // Works in the same way for arrays by using the ArrayOf attribute
    #[ArrayOf(Address::class)]
    public array $addresses;
}

class Credentials
{
    #[Length(min: 3, max: 256)]
    public string $username;

    #[PasswordStrength]
    public string $password;
}

class Address
{
    public string $street;
    public string $city;
    public string $zipCode;
    public string $country;
}

自定义验证器

库提供了一套内置验证器,如您所看到的这里,但在实际应用中,您可能需要创建自己的验证器。

创建自定义验证器有多种方法,具体取决于您的需求(和时间限制)。

注意:不要忘记在您的自定义验证器中处理可选字段,因为它们可能为null。

快速但简单的方法

创建自定义验证器最简单的方法是在您的表单类中创建一个验证方法,并用ValidationMethod约束注解属性

use Quatrevieux\Form\Validator\Constraint\ValidationMethod;

class MyForm
{
    #[ValidationMethod('validateFoo')]
    public string $foo;
    
    public function validateFoo(string $value): ?string
    {
        // Do your validation here
        if (...) {
            // Return an error message if the value is invalid (it will be translated using the translator)
            // Note: the return value may also be a boolean (use message defined in the constraint) or a FieldError object
            // See the documentation for more information
            return 'Foo is invalid'; 
        }

        return null;
    }
}

但这种方法有一些缺点

  • 将验证方法污染到表单类中
  • 参数的类型安全性不会在编译时强制执行,方法名称也不会在编译时检查
  • 可重用性是可能的,但需要为每个验证方法声明一个具有静态方法的类
  • 无法进行依赖注入

因此,仅将此方法用于原型或临时代码。

不干净,但有依赖注入

如果您需要在验证方法中注入依赖项,您可以使用具有验证器类名的 ValidateBy 约束,并实现 ConstraintValidatorInterface

use Quatrevieux\Form\Validator\Constraint\ConstraintValidatorInterface;
use Quatrevieux\Form\Validator\Constraint\ConstraintInterface;
use Quatrevieux\Form\Validator\Constraint\ValidateBy;

class MyForm
{
    #[ValidateBy(MyValidator::class)]
    public string $foo;
}

// Declare your validator class
class MyValidator implements ConstraintValidatorInterface
{
    public function __construct(
        // Inject your dependencies here
        private readonly MyFooService $service,
    ) {
    }

    public function validate(ConstraintInterface $constraint, mixed $value, object $data): ?FieldError
    {
        // $constraint is the ValidateBy constraint, which can be used to access the parameters
        // $value is the value of the field
        // $data is the form object (MyForm in this case)

        if (!$this->service->isValid($value)) {
            // No sugar here, you have to create the FieldError object yourself
            // Note: it will be automatically translated using the translator
            return new FieldError('Foo is invalid');
        }

        return null;
    }
}

此方法修复了之前方法的大部分缺点,但它仍然无法强制参数的类型安全。因此,如果验证不需要任何参数,则使用此方法是合理的。

清洁的方法

根据您的需求,有两种方法可以创建一个干净的验证器

SelfValidatedConstraint
use Quatrevieux\Form\Validator\Constraint\ConstraintInterface;
use Quatrevieux\Form\Validator\Constraint\SelfValidatedConstraint;
use Quatrevieux\Form\Validator\FieldError;
use Attribute;

#[Attribute(Attribute::TARGET_PROPERTY)]
class MyConstraint extends SelfValidatedConstraint
{
    // It's recommended to declare an unique code for your constraint
    public const CODE = 'c6241cc4-c7f5-4951-96b5-bf3e69f9ed15';

    public function __construct(
        // Declare your parameters here
        public readonly string $foo,
        // It's recommended to declare a message parameter, which will be used as the default error message
        // Placeholders can be used to display parameters values
        public readonly string $message = 'Foo is invalid : {{ foo }}',
    ) {
    }

    public function validate(ConstraintInterface $constraint, mixed $value, object $data): ?FieldError
    {
        // $constraint is same as $this
        // $value is the value of the field
        // $data is the form object (MyForm in this case)

        if (...) {
            return new FieldError($constraint->message, ['foo' => $constraint->foo], self::CODE);
        }

        return null;
    }
}
ConstraintInterface 和 ConstraintValidatorInterface
use Quatrevieux\Form\Validator\Constraint\ConstraintInterface;
use Quatrevieux\Form\Validator\Constraint\ConstraintValidatorInterface;
use Quatrevieux\Form\Validator\FieldError;
use Quatrevieux\Form\RegistryInterface;
use Attribute;

#[Attribute(Attribute::TARGET_PROPERTY)]
class MyConstraint implements ConstraintInterface
{
    // It's recommended to declare an unique code for your constraint
    public const CODE = 'c6241cc4-c7f5-4951-96b5-bf3e69f9ed15';

    public function __construct(
        // Declare your parameters here
        public readonly string $foo,
        // It's recommended to declare a message parameter, which will be used as the default error message
        // Placeholders can be used to display parameters values
        public readonly string $message = 'Foo is invalid : {{ foo }}',
    ) {
    }
    
    public function getValidator(RegistryInterface $registry): ConstraintValidatorInterface
    {
        // Resolve the validator from the registry
        return $registry->getConstraintValidator(MyConstraintValidator::class);
    }
}

class MyConstraintValidator implements ConstraintValidatorInterface
{
    public function __construct(
        // Inject your dependencies here
        private readonly MyFooService $service,
    ) {
    }

    public function validate(ConstraintInterface $constraint, mixed $value, object $data): ?FieldError
    {
        // $constraint is the MyConstraint constraint, which can be used to access the parameters
        // $value is the value of the field
        // $data is the form object (MyForm in this case)

        if (!$this->service->isValid($value, $constraint->foo)) {
            return new FieldError($constraint->message, ['foo' => $constraint->foo], MyConstraint::CODE);
        }

        return null;
    }
}

代码生成

默认情况下,也会通过内联约束类的实例化来对自定义约束执行代码生成。例如,以下属性

#[MyConstraint('bar')]
public ?string $foo;

将被编译成以下代码

if (($error = ($fooConstraint = new MyConstraint('bar'))->getValidator($this->registry)->validate($fooConstraint, $data->foo ?? null, $data)) !== null) {
    $errors['foo'] = $error;
}

这提供了相当不错的性能,但它不是最优的。因此,您可能希望在您的表单中大量使用简单约束时自己生成验证代码。

为此,您可以在验证器类(或在自验证约束的情况下为约束类)上实现 ConstraintValidatorGeneratorInterface

请参阅库 源代码 中的示例。

自定义转换器

与验证器不同,没有提供快速且简单的创建自定义转换器的方法。因此,有两种方法可以创建自定义转换器

FieldTransformerInterface

这是创建自定义转换器的最简单方法,但它不允许依赖注入。您只需

  • 创建一个实现 FieldTransformerInterface 接口的类
  • 实现 transformFromHttp() 用于 HTTP 到表单对象的转换
  • 实现 transformToHttp() 用于表单对象到 HTTP 的转换(如果需要)
  • 将其声明为表单属性的属性。
use Quatrevieux\Form\Transformer\Field\FieldTransformerInterface;
use Attribute;

#[Attribute(Attribute::TARGET_PROPERTY)]
class MyTransformer implements FieldTransformerInterface
{
    public function __construct(
        // Declare your parameters here
        private readonly string $foo,
    ) {
    }

    public function transformFromHttp(mixed $value): mixed
    {
        // $value is the HTTP value
        // The value will be null if the field is not present in the HTTP request

        // Transform the value here
        return $value;
    }

    public function transformToHttp(mixed $value): mixed
    {
        // $value is the form object value

        // Transform the value here
        return $value;
    }

    public function canThrowError(): bool
    {
        // Return true if the transformer can throw an error
        // If true, the transformer will be wrapped in a try/catch block to mark the field as invalid
        return false;
    }
}

class MyForm
{
    #[MyTransformer('bar')]
    public ?string $foo;
}

DelegatedFieldTransformerInterface

当您在转换逻辑中需要一些依赖项时,应使用 DelegatedFieldTransformerInterface 接口。转换逻辑(及其依赖项)将由实现 ConfigurableFieldTransformerInterface 接口的类提供。

use Quatrevieux\Form\Transformer\Field\DelegatedFieldTransformerInterface;
use Quatrevieux\Form\Transformer\Field\ConfigurableFieldTransformerInterface;
use Quatrevieux\Form\RegistryInterface;
use Attribute;

#[Attribute(Attribute::TARGET_PROPERTY)]
class MyTransformer implements DelegatedFieldTransformerInterface
{
    public function __construct(
        // Declare your parameters here
        public readonly string $foo,
    ) {
    }

    public function getTransformer(RegistryInterface $registry): ConfigurableFieldTransformerInterface
    {
        // Resolve the transformer from the registry
        return $registry->getFieldTransformer(MyTransformerImpl::class);
    }
}

class MyTransformerImpl implements ConfigurableFieldTransformerInterface
{
    public function __construct(
        // Declare your dependencies here
        private readonly MyFooService $service,
    ) {
    }

    public function transformFromHttp(DelegatedFieldTransformerInterface $configuration, mixed $value): mixed
    {
        // $configuration is the MyTransformer attribute instance
        // $value is the HTTP value
        // The value will be null if the field is not present in the HTTP request

        // Transform the value here
        return $value;
    }

    public function transformToHttp(DelegatedFieldTransformerInterface $configuration, mixed $value): mixed
    {
        // $configuration is the MyTransformer attribute instance
        // $value is the form object value

        // Transform the value here
        return $value;
    }
}

代码生成

代码生成与验证器类似,因此您可以参考验证器部分获取更多信息。

为此,您可以在转换实现类(即实现 FieldTransformerInterfaceConfigurableFieldTransformerInterface 但不是 DelegatedFieldTransformerInterface 的类)上实现 FieldTransformerGeneratorInterface

请参阅库源代码中的示例。

API

验证

数组形状

来源:src/Validator/Constraint/ArrayShape.php

ArrayShape 类是一个 PHP 属性,可用于验证当前字段是否为数组,以及它是否具有预期的键和值类型。

示例

class MyForm
{
    #[ArrayShape([
        'firstName' => 'string',
        'lastName' => 'string',
        // Use ? to mark the field as optional
        'age?' => 'int',
        // You can declare a sub array shape
        'address' => [
            'street' => 'string',
            'city' => 'string',
            'zipCode' => 'string|int', // You can use multiple types
        ],
    ])]
    public array $person;

    // You can define array as dynamic list
    #[ArrayShape(key: 'int', value: 'int|float')]
    public array $listOfNumbers;

    // You can disable extra keys
    #[ArrayShape(['foo' => 'string', 'bar' => 'int'], allowExtraKeys: false)]
    public array $fixed;
}

构造函数

ArrayShape 类构造函数接受以下参数

选择

来源:src/Validator/Constraint/Choice.php

检查值是否在给定的选项中。

值将进行严格比较,因此请确保值被正确转换。此约束支持多个选项(即输入值是数组)。您可以通过在选项数组中使用字符串键来为选项定义标签。

示例

class MyForm
{
    #[Choice(['foo', 'bar'])]
    public string $foo;

    // Define labels for the choices
    #[Choice([
        'My first label' => 'foo',
        'My other label' => 'bar',
    ])]
    public string $bar;
}

构造函数

等于与

来源:src/Validator/Constraint/EqualsWith.php

检查当前字段值是否等于其他字段值。

示例

class MyForm
{
    #[EqualsWith('password', message: 'Passwords must be equals')]
    public string $passwordConfirm;
    public string $password;
}

构造函数

等于

来源:src/Validator/Constraint/EqualTo.php

检查字段值是否等于给定值。此比较使用简单比较运算符(==)而不是严格比较运算符(===)。

支持数字和字符串值。为了确保比较在同一类型上完成,请向字段添加类型提示,并在约束的值上使用相同类型。

示例

class MyForm
{
    #[EqualTo(10)]
    public int $foo;
}

构造函数

另请参阅

大于

来源:src/Validator/Constraint/GreaterThan.php

检查字段值是否大于给定值。

支持数字和字符串值。为了确保比较在同一类型上完成,请向字段添加类型提示,并在约束的值上使用相同类型。

示例

class MyForm
{
    #[GreaterThan(10)]
    public int $foo;
}

构造函数

另请参阅

大于或等于

来源:src/Validator/Constraint/GreaterThanOrEqual.php

检查字段值是否大于或等于给定值。支持数字和字符串值。为确保比较在相同类型上完成,请向字段添加类型提示并使用约束值上的相同类型。

示例

class MyForm
{
    #[GreaterThanOrEqual(10)]
    public int $foo;
}

构造函数

另请参阅

IdenticalTo

来源:src/Validator/Constraint/IdenticalTo.php

检查字段值是否与给定值相同。此比较使用严格的比较运算符(===)。

支持数字和字符串值。值类型必须与字段类型相同,并且必须使用类型提示或 Cast 转换器将字段值转换为类型。

示例

class MyForm
{
    #[IdenticalTo(10)]
    public int $foo;
}

构造函数

另请参阅

Length

来源:src/Validator/Constraint/Length.php

验证字符串字段的长度。如果字段不是字符串,则此验证器将被忽略。

此约束允许您检查字符串字段的长度。您可以定义最小和最大长度,并自定义错误信息。

示例

class MyForm
{
    // Only check the minimum length
    #[Length(min: 2)]
    public string $foo;

    // Only check the maximum length
    #[Length(max: 32)]
    public string $bar;

    // For a fixed length
    #[Length(min: 12, max: 12)]
    public string $baz;

    // Check the length is between 2 and 32 (included)
    #[Length(min: 2, max: 32)]
    public string $qux;
}

构造函数

LessThan

来源:src/Validator/Constraint/LessThan.php

LessThan 约束检查字段值是否小于给定值。比较使用严格的比较运算符 < 以确保比较在相同类型上完成。

示例

class MyForm
{
    #[LessThan(10)]
    public int $foo;
}

构造函数

另请参阅

LessThanOrEqual

来源:src/Validator/Constraint/LessThanOrEqual.php

检查字段值是否小于或等于给定值

支持数字和字符串值。为了确保比较在同一类型上完成,请向字段添加类型提示,并在约束的值上使用相同类型。

示例

class MyForm
{
    #[LessThanOrEqual(10)]
    public int $foo;
}

构造函数

另请参阅

NotEqualTo

来源:src/Validator/Constraint/NotEqualTo.php

检查字段值是否等于给定值。此比较使用简单比较运算符 != 而不是严格的 !==

支持数字和字符串值。为了确保比较在同一类型上完成,请向字段添加类型提示,并在约束的值上使用相同类型。

示例

class MyForm
{
    #[NotEqualTo(10)]
    public int $foo;
}

构造函数

另请参阅

NotIdenticalTo

来源:src/Validator/Constraint/NotIdenticalTo.php

检查字段值是否等于给定值。此比较使用严格的比较运算符 !==。支持数字和字符串值。值类型必须与字段类型相同,并且必须使用类型提示或 Cast 转换器将字段值转换为类型。

示例

class MyForm
{
    #[NotIdenticalTo(10)]
    public int $foo;
}

构造函数

另请参阅

PasswordStrength

来源: src/Validator/Constraint/PasswordStrength.php

检查密码字段的强度。强度是一个对数值,表示可能的组合数量的近似值。

此算法考虑以下因素:

  • 存在小写字母
  • 存在大写字母
  • 存在数字
  • 存在特殊字符
  • 密码长度

此检查不会强制用户使用特定的字符集,同时仍然提供良好的安全级别。强度应根据所使用的密码哈希算法选择:较慢的算法会使暴力攻击变慢,因此较低的强度是可以接受的。建议的强度为51,在每秒一十亿的尝试下大约需要一个月才能破解。

示例

class User
{
    #[PasswordStrength(min:51, message:"Your password is too weak")]
    private $password;
}

构造函数

正则表达式

来源: src/Validator/Constraint/Regex.php

使用PCRE语法检查字段值是否与给定的正则表达式匹配。如果可能,生成HTML5 "pattern" 属性。

示例

class MyForm
{
    #[Regex('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')]
    public ?string $uuid;

    #[Regex('^[a-z]+$', flags: 'i')]
    public string $foo;
}

构造函数

必需

来源: src/Validator/Constraint/Required.php

此约束将字段标记为必需。如果字段不是null,也不是空字符串或数组,则认为字段有效。任何其他值都将被认为有效,如 0false 等。

注意:如果字段已定义为非null,则此约束不是必需的。

示例

class MyForm
{
    // Explicitly mark the field as required because it is nullable
    #[Required]
    public $foo;

    // The field is required because it is not nullable
    public string $bar;

    // You can define a custom error message to override the default one
    #[Required('This field is required')]
    public string $baz;
}

构造函数

上传文件

来源: src/Validator/Constraint/UploadedFile.php

检查上传的文件是否有效。使用PSR-7 UploadedFileInterface,因此需要使用 psr/http-message 包才能使用此约束。

示例

class MyForm
{
    // Constraint for simply check that the file is successfully uploaded
    #[UploadedFile]
    public UploadedFileInterface $file;
     // You can also define a file size limit
    #[UploadedFile(maxSize: 1024 * 1024)]
    public UploadedFileInterface $withLimit;
     // You can also define the file type using mime type filter, or file extension filter
    // Note: this is not a security feature, you should always check the actual file type on the server side
    #[UploadedFile(
        allowedMimeTypes: ['image/*', 'application/pdf'], // wildcard is allowed for subtype
        allowedExtensions: ['jpg', 'png', 'pdf'] // you can specify the file extension (without the dot)
    )]
    public UploadedFileInterface $withMimeTypes;
}

// Create and submit the form
// Here, $request is a PSR-7 ServerRequestInterface
$form = $factory->create(MyForm::class);
$submitted = $form->submit($request->getParsedBody() + $request->getUploadedFiles());

构造函数

验证数组

来源: src/Validator/Constraint/ValidateArray.php

数组验证约束。将对数组的每个元素应用验证。

示例

class MyRequest
{
    #[ValidateArray(constraints: [
        new Length(min: 3, max: 10),
        new Regex(pattern: '/^[a-z]+$/'),
    ])]
    public ?array $values;
}

构造函数

通过验证

来源: src/Validator/Constraint/ValidateBy.php

这是一个通用约束,用于使用自定义验证器实例验证字段值。此类允许您指定要使用的验证器类以及传递给验证器的一组选项。请注意,与使用选项数组相比,使用自定义约束类更为可取。

示例

class MyForm
{
   #[ValidateBy(MyValidator::class, ['checksum' => 15])]
   public string $foo;
}

class MyValidator implements ConstraintValidatorInterface
{
    public function __construct(private ChecksumAlgorithmInterface $algo) {}

    public function validate(ConstraintInterface $constraint, mixed $value, object $data): ?FieldError
    {
        if ($this->algo->checksum($value) !== $constraint->options['checksum']) {
            return new FieldError('Invalid checksum');
        }

        return null;
    }
}

构造函数

另请参阅

  • 自定义验证器 说明了如何创建自定义验证器。以下是如何将 ValidateVar 类的文档块转换为说明部分的一个示例

验证变量

来源: src/Validator/Constraint/ValidateVar.php

使用 filter_var()FILTER_VALIDATE_* 常量验证字段值。

示例

class MyRequest
{
    #[ValidateVar(ValidateVar::EMAIL)]
    public ?string $email;

    #[ValidateVar(ValidateVar::DOMAIN, options: FILTER_FLAG_HOSTNAME)] // You can add flags as an int
    public ?string $domain;

    #[ValidateVar(ValidateVar::INT, options: ['options' => ['min_range' => 0, 'max_range' => 100]])] // You can add options as an array
    public ?float $int;
}

构造函数

验证方法

来源: src/Validator/Constraint/ValidationMethod.php

使用方法调用验证字段值。

此方法可以是给定类上的静态方法或数据对象上声明的实例方法。方法将使用以下参数调用:

  • 字段值
  • 数据对象(在实例方法中,此参数与 $this 相同)
  • 额外的参数,作为可变参数传递

方法可以返回以下之一:

  • null:字段有效
  • true:字段有效
  • 一个字符串:字段无效,该字符串是错误消息
  • 一个 FieldError 实例:字段无效
  • false:字段无效,错误消息是默认的

示例

class MyForm
{
    // Calling validateFoo() on this instance
    #[ValidateMethod(method: 'validateFoo', parameters: [15], message: 'Invalid checksum')]
    public string $foo;

    // Calling Functions::validateFoo()
    #[ValidateMethod(class: Functions::class, method: 'validateFoo', parameters: [15], message: 'Invalid checksum')]
    public string $foo;

    // Return a boolean, so the default error message is used
    public function validateFoo(string $value, object $data, int $checksum)
    {
        return crc32($value) % 32 === $checksum;
    }

    // Return a string, so the string is used as error message
    public function validateFoo(string $value, object $data, int $checksum)
    {
        if (crc32($value) % 32 !== $checksum) {
            return 'Invalid checksum';
        }

        return null;
    }

    // Return a FieldError instance
    public function validateFoo(string $value, object $data, int $checksum)
    {
        if (crc32($value) % 32 !== $checksum) {
            return new FieldError('Invalid checksum');
        }

        return null;
    }
}

class Functions
{
    // You can also use a static method
    public static function validateFoo(string $value, object $data, int $checksum): bool
    {
        return crc32($value) % 32 === $checksum;
    }
}

构造函数

另请参阅

转换

ArrayCast

来源:src/Transformer/Field/ArrayCast.php

转换数组值

执行转换是一个安全操作

  • 如果无法转换值,将返回 null
  • 对于数值类型,无效的字符串将返回 0(或浮点数的 0.0)

将转换转换为 HTTP 值将简单地将非空值转换为数组。

示例

class MyForm
{
    #[ArrayCast(CastType::Int)]
    public array $foo;
    
    // Ignore original keys : the result will be a list of floats
    #[ArrayCast(CastType::Float, preserveKeys: false)]
    public array $bar;
}

构造函数

转换

来源:src/Transformer/Field/Cast.php

将 HTTP 值转换为目标类型

此转换器会自动添加到类型属性上。执行转换是一个安全操作

  • 如果无法转换值,将返回 null
  • 对于数值类型,无效的字符串将返回 0(或浮点数的 0.0)

转换到 HTTP 值将简单地假设值已经是规范化值

示例

class MyForm
{
    #[Cast(CastType::Int)]
    public $foo;
    
    // By default, the cast is performed on the property type, so it's not needed here
    public float $bar;
}

Csv

来源:src/Transformer/Field/Csv.php

将 CSV 字符串转换为数组。此转换器使用 RFC 4180 的实现,因此支持引号内的值。

示例

class MyForm
{
    // Will transform "foo,bar,baz" to ["foo", "bar", "baz"]
    #[Csv]
    public array $foo;

    // You can specify separator
    #[Csv(separator: ';')]
    public array $bar;

    // You can use ArrayCast to cast values
    #[Csv, ArrayCast(CastType::INT)]
    public array $baz;
}

构造函数

DateTimeTransformer

来源:src/Transformer/Field/DateTimeTransformer.php

将字符串转换为 DateTimeInterface 对象。

示例

class MyForm
{
    // Parse an HTML5 datetime-local input
    #[DateTimeTransformer]
    public ?DateTimeInterface $date;

    // Use a custom format, class, and timezone
    #[DateTimeTransformer(class: DateTime::class, format: 'd/m/Y', timezone: 'Europe/Paris')]
    public ?DateTime $date;
}

构造函数

DefaultValue

来源:src/Transformer/Field/DefaultValue.php

为字段定义默认值。

当字段未提交时使用默认值。如果默认值不为 null,则此转换器将自动添加到字段。您可以通过显式定义此转换器来忽略默认行为。

注意:注意转换器顺序。如果在此属性之前定义了其他转换器,则值应为 HTTP(即未转换)值。如果在此属性之后定义了其他转换器,则值应为 PHP(即已转换)值。

示例

class MyForm
{
    public int $implicit = 42; // Implicitly define the default value. Will be applied after all other transformers.

    #[DefaultValue(12.3)] // Explicitly define the default value. Default property value will be ignored.
    public float $explicit = 0.0;

    #[DefaultValue('foo,bar')] // When defined before other transformers, the value should be an HTTP value.
    #[Csv]
    public array $values;
}

Enum

来源:src/Transformer/Field/Enum.php

将值转换为相应的枚举实例。它支持 UnitEnumBackedEnum,并使用值或名称解析枚举实例。

为了定义选项的标签,在枚举类上实现 LabelInterface 接口。

注意:此转换器区分大小写

示例

class MyForm
{
    // If MyEnum is a UnitEnum, the name will be used to get the enum instance
    // Else, the value will be used
    // If the value is not found, the field will be considered as invalid
    #[Enum(MyEnum::class)]
    public ?MyEnum $myEnum;

    // Use the name instead of the value on BackedEnum
    #[Enum(MyEnum::class, useName: true)]
    public ?MyEnum $byName;

    // If the value is not found, the field will be set to null without error
    #[Enum(MyEnum::class, errorOnInvalid: false)]
    public ?MyEnum $noError;
}

构造函数

Json

来源:src/Transformer/Field/Json.php

将 JSON 字符串解析为 PHP 值。

注意:如果 JSON 无效,转换将失败。使用 TransformationError 来更改此行为。

示例

class MyRequest
{
    #[Json] // By default, JSON objects will be returned as associative arrays.
    public ?array $myArray;

    #[Json(assoc: false, depth: 5)] // JSON objects will be returned as stdClass, and limit the depth to 5.
    public ?object $myObject;

    #[Json(encodeOptions: JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)] // Change the display of the JSON.
    public mixed $pretty;
    
    // Use ArrayShape to validate the JSON structure
    #[Json, ArrayShape(['foo' => 'string', 'bar' => 'int'])] 
    public array $withShape;
}

构造函数

另请参阅

TransformEach

来源:src/Transformer/Field/TransformEach.php

对数组中的每个元素应用转换器。如果输入值不是数组,则在应用转换器之前将其转换为数组。

示例

class MyForm
{
    #[TransformEach([
        new Trim(),
        new Csv(separator: ';'),
    ])]
    public ?array $tags = null;
}

构造函数

Trim

来源:src/Transformer/Field/Trim.php

去除输入值。

该变压器将删除值开头和结尾的所有空格。此转换仅在从HTTP转换时应用。

示例

class MyForm
{
    #[Trim]
    public ?string $myString = null;
}

转换错误

来源: src/Transformer/Field/TransformationError.php

配置转换错误的错误处理。

默认情况下,当转换失败时,字段将被视为无效,并将错误消息设置为第一个失败的转换器的错误消息。

注意:这不是一个变压器,而是一个配置类。

示例

class MyForm
{
    // You can customize the error message, and code, just like validation errors
    #[TransformationError(message: 'This JSON is invalid', code: 'f59e2415-0b70-4177-9bc1-66ebbb65c75c'), Json]
    public string $json;

    // Fail silently: the field will be set to null, and no error will be displayed
    #[TransformationError(ignore: true), Json]
    public ?string $foo;

    // Keep the original value instead of setting it to null
    #[TransformationError(ignore: true, keepOriginalValue: true), Json]
    public mixed $bar;
}

构造函数

组件

复选框

来源: src/Component/Checkbox.php

处理HTTP复选框输入。

当HTTP请求中存在值并且等于给定的httpValue(默认为"1")时,字段值为true。当HTTP请求中不存在值或不等于给定的httpValue时,字段值为false。因此,字段值始终是非空的布尔值。

注意:http值在比较之前将转换为字符串。

示例

final class MyForm
{
    #[Checkbox]
    public bool $isAccepted;

    // You can also define a custom http value
    #[Checkbox(httpValue: 'yes')]
    public bool $withCustomHttpValue;

    // You can use validator to ensure the field is checked (or any other validation)
    #[Checkbox, IsIdenticalTo(true, message: 'You must check this box')]
    public bool $mustBeChecked;
}

Csrf

来源: src/Component/Csrf/Csrf.php

向表单添加令牌以防止CSRF攻击。

要使用此约束,您必须安装Symfony Security组件(即symfony/security-csrf),并在表单注册表中注册CsrfManager服务。

可以通过将"refresh"选项设置为true在每次请求中重新生成CSRF令牌。

示例

class MyForm
{
    // The CSRF token will be generated once per session
    // Required constraint as no effect here : csrf token is always validated
    #[Csrf]
    public string $csrf;

    // The CSRF token will be regenerated on each request
    #[Csrf(refresh: true)]
    public string $csrfRefresh;
}

// Add CsrfManager to the form registry (use PSR-11 container in this example)
$container->register(new CsrfManager($container->get(CsrfTokenManagerInterface::class)));
$registry = new ContainerRegistry($container);
$factory = DefaultFormFactory::create($registry);

// Create and submit the form
$form = $factory->create(MyForm::class);
$submitted = $form->submit($_POST);

构造函数

内部工作

验证和填充表单的过程分为3步,外加一个可选的第四步

  • 转换步骤,将原始数据转换为PHP可以处理的规范格式
  • 填充步骤,用规范数据填充表单对象
  • 验证步骤,验证表单对象
  • 可选地,生成视图对象,可以用于生成HTML表单

每个步骤都可以使用代码生成编译成PHP类,这提高了性能。在从表单对象导入数据时,将按相反的顺序执行步骤。

转换

当调用FormInterface::submit()方法时,转换是表单过程的第一个步骤。当调用FormInterface::import()方法时,它也是过程的最后一个步骤。

转换步骤负责将原始数据转换为可以用于在下一个步骤中填充表单对象的属性数组。

因此,它将执行

  • 数据规范化,例如将字符串转换为整数,或将日期字符串转换为DateTime对象
  • 过滤,例如删除未声明的字段
  • 别名,例如将字段重命名为与表单对象属性名匹配

例如,以下HTTP表单数据

name=john&roles=5,42&created_at=2020-01-01T00:00:00Z

可以转换为以下数组

[
    'name' => 'john',
    'roles' => [5, 42],
    'createdAt' => new \DateTime('2020-01-01T00:00:00Z'),
]

import()步骤期间,转换将执行反向操作,因此前面的数组将转换为以下数组

[
    'name' => 'john',
    'roles' => '5,42',
    'created_at' => '2020-01-01T00:00:00Z',
]

总结,转换步骤将HTTP表单数据数组转换为规范且安全的数组,可以用于填充表单对象,反之亦然。

参见

填充

水合步骤是在调用FormInterface::submit()方法时表单处理过程的第二步,以及在调用FormInterface::import()方法时的第一步。

在提交过程中,水合步骤负责从转换步骤中的数组实例化和填充表单对象。在导入过程中,水合步骤负责从表单对象中提取属性数组。

参见

验证

验证步骤是提交过程的最后一步。在从表单对象导入数据时,不会执行验证。

此步骤将简单地使用表单字段上声明的约束验证表单对象的每个属性。它将返回一个由属性名称索引的FieldError对象数组。

注意:转换步骤可能会引发一些错误。在这种情况下,将跳过字段的验证,并返回转换步骤的错误。

参见

视图生成

视图生成是一个可选步骤,可以用于从表单对象生成视图对象。此步骤由调用FormInterface::view()方法触发。

如果表单是已提交或已导入的,视图将根据FilledFormInterface::httpValue()生成。

参见

不可变性

大多数表单组件都是不可变的,这意味着当你调用一个修改对象的方法时,它们将返回一个新的实例。

因此,每个表单过程都会返回一个新的实例

  • 默认表单工厂将返回一个新的FormInterface实例
  • FormInterface::submit()将返回一个新的SubmittedFormInterface实例
  • FormInterface::import()将返回一个新的ImportedFormInterface实例

每种类型只提供实际可用于该类型的 方法,因此你不能调用例如FormInterface::value()

不可变性允许重用或缓存表单实例(默认工厂始终缓存实例)。但它也带来了一些缺点:需要新的变量来存储每个表单过程的输出。

$form = $factory->create(MyForm::class);

// The following code will not work:
$form->submit($_POST);

if (!$form->valid()) {
    return $this->showErrors($form->errors());
}

return $this->process($form->value());

// The following code will work:
$submitted = $form->submit($_POST);

if (!$submitted->valid()) {
    return $this->showErrors($submitted->errors());
}

return $this->process($submitted->value());

性能

有关最新基准测试结果,请参阅GitHub actions

使用phpbench进行了基准测试比较,比较了此库与Symfony表单和纯PHP。

许可协议

此库受MIT许可协议许可。