b2pweb/bdf-form

简单灵活的表单库

v1.5.1 2024-09-25 13:55 UTC

This package is auto-updated.

Last update: 2024-09-25 14:08:57 UTC


README

处理表单和请求验证的库。

build codecov Packagist Version Total Downloads Type Coverage

目录表

使用composer安装

composer require b2pweb/bdf-form

基本用法

要创建表单,只需扩展类CustomForm并实现方法CustomForm::configure()

<?php

namespace App\Form;

use Bdf\Form\Aggregate\FormBuilderInterface;
use Bdf\Form\Custom\CustomForm;

class LoginForm extends CustomForm
{
    protected function configure(FormBuilderInterface $builder) : void
    {
        // Register inputs using builder
        // required() specify than the input value cannot be empty
        // setter() specify that the value will be exported when calling $form->value()
        $builder->string('username')->required()->setter();
        $builder->string('password')->required()->setter();

        // A button can also be declared (useful for handle multiple actions in one form)
        $builder->submit('login');
    }
}

要显示表单,请在表单对象上调用ElementInterface::view()方法,并使用视图对象

<?php
// Instantiate the form (a container can be use for handle dependency injection)
$form = new LoginForm();
$view = $form->view(); // Get the form view

?>

<form method="post" action="login.php">
    <!-- Use array access for get form elements -->
    <!-- The onError() method will return the parameter only if the element is on error. This method also supports a callback as parameter -->
    <div class="input-group<?php echo $view['username']->onError(' has-error'); ?>">
        <label for="login-username">Username</label>
        <!-- You can configure attributes using magic method call : here it will add class="form-control" id="login-username" -->
        <!-- The view element can be transformed to string. The input html element, the value and the name will be renderer -->
        <?php echo $view['username']->class('form-control')->id('login-username'); ?>
        <!-- Render the error message -->
        <div class="form-control-feedback"><?php echo $view['username']->error(); ?></div>
    </div>
    <div class="input-group<?php echo $view['password']->onError(' has-error'); ?>">
        <label for="login-password">Password</label>
        <!-- If there is a conflict with a method name for add an attribute, you can use the method set() -->
        <?php echo $view['password']->class('form-control')->id('login-password')->set('type', 'password'); ?>
        <div class="form-control-feedback"><?php echo $view['password']->error(); ?></div>
    </div>
    
    <!-- Render the button -->
    <?php echo $view['login']->class('btn btn-primary')->inner('Login'); ?>
</form>

现在,您可以提交数据到表单,并执行验证

<?php

// Instantiate the form (a container can be use for handle dependency injection)
$form = new LoginForm();

// Submit and check if the form is valid
if (!$form->submit($_POST)->valid()) {
    // The form has an error : use `ElementInterface::error()` to get the error and render it
    echo 'Error : ', $form->error();
    return;
}

// The form is valid : get the value
$credentials = $form->value();

// $credentials is an array with elements values 
performLogin($credentials['username'], $credentials['password']);

处理实体

表单系统可以使用访问器导入、创建或填充实体

  • 对于FormInterface::import()实体,在相应的字段上使用ChildInterface::getter()。此方法将使用Getter作为提取器。
  • 要填充实体,使用FormInterface::attach()后跟FormInterface::value(),使用ChildInterface::setter()。此方法将使用Setter作为填充器。
  • 要创建实体的新实例,使用FormInterface::value(),不使用attach(),使用FormBuilderInterface::generates()。此方法将使用ValueGenerator

声明

<?php

// Declare the entity
// The properties should be public, or has public accessors to be handled by the form
class Person
{
    /** @var string */
    public $firstName;
    /** @var string */
    public $lastName;
    /** @var DateTimeInterface|null */
    public $birthDate;
    /** @var Country|null */
    public $country;
}

class PersonForm extends \Bdf\Form\Custom\CustomForm
{
    protected function configure(\Bdf\Form\Aggregate\FormBuilderInterface $builder) : void
    {
        // Define that PersonForm::value() should return a Person instance 
        $builder->generates(Person::class);
        
        // Declare fields with getter and setter
        $builder->string('firstName')->required()->getter()->setter();
        $builder->string('lastName')->required()->getter()->setter();
        $builder->dateTime('birthDate')->immutable()->getter()->setter();
        
        // Custom transformer can be declared with a callback as first parameter on getter() and setter() methods
        $builder->string('country')
            ->getter(function (Country $value) { return $value->code; })
            ->setter(function (string $value) { return Country::findByCode($value); })
        ;
    }
}

用法

<?php

class PersonController extends Controller
{
    private $repository;
    
    // Get a form view with entity values
    public function editForm($request)
    {
        // Get the entity
        $person = $this->repository->find($request->query->get('id'));
        
        // Create the form, import the entity data, and create the view object
        $form = new PersonForm();
        $view = $form->import($person)->view();
        
        // The form view can be used: fields values are set
        return $this->render('person/form', ['form' => $view]);
    }
    
    // Use the form to create the entity
    public function create($request)
    {
        // Get the form instance
        $form = new PersonForm();
        
        // Submit form data
        if (!$form->submit($request->post())->valid()) {
            throw new FormError($form->error());
        }
        
        // $form->value() will return the filled entity
        $this->repository->insert($form->value());
    }
    
    // Update an existent entity: simply attach the entity to fill
    public function update($request)
    {
        // Get the entity
        $person = $this->repository->find($request->query->get('id'));
        
        // Get the form instance and attach the entity to update
        $form = new PersonForm();
        $form->attach($person);

        // Submit form data
        if (!$form->submit($request->post())->valid()) {
            throw new FormError($form->error());
        }
        
        // $form->value() will return the filled entity
        $this->repository->insert($form->value());
    }
    
    // Works like update, but apply only provided fields (HTTP PATCH method)
    // The entity must be import()'ed instead of attach()'ed
    public function patch($request)
    {
        // Get the entity
        $person = $this->repository->find($request->query->get('id'));
        
        // Get the form instance and import the entity to patch
        $form = new PersonForm();
        $form->import($person);

        // Submit form data
        if (!$form->patch($request->post())->valid()) {
            throw new FormError($form->error());
        }
        
        // $form->value() will return the filled entity
        $this->repository->insert($form->value());
    }
}

转换过程

以下是转换过程的描述,从HTTP值到模型值。此过程可逆,用于从模型生成HTTP值。

注意:此示例适用于表单中包含的叶元素。对于嵌入式表单或数组,只需将叶元素过程替换为表单过程。

1 - 提交到按钮(范围:RootForm)

第一步由RootForm执行,检查提交按钮。如果HTTP数据包含按钮名称,并且值与配置值匹配,则按钮被标记为已点击。

注意:在反向过程中,点击的按钮值将添加到HTTP值中

  • ButtonInterface::submit()
  • ButtonInterface::clicked()
  • RootFormInterface::submitButton()

2 - 调用表单的转换器(范围:Form)

调用容器表单的转换器。它们用于将输入HTTP数据规范化为可用的数组值。

注意:转换器按反向顺序调用(即最后注册的第一个执行),当从HTTP转换为PHP时,并且按顺序调用PHP到HTTP转换

// Declare a transformer
class JsonTransformer implements \Bdf\Form\Transformer\TransformerInterface
{
    public function transformToHttp($value,\Bdf\Form\ElementInterface $input)
    {
        return json_encode($value);
    }

    public function transformFromHttp($value,\Bdf\Form\ElementInterface $input)
    {
        return json_decode($value, true);
    }
}

class MyForm extends CustomForm
{
    protected function configure(FormBuilderInterface $builder): void
    {
        // Transform JSON input to associative array
        $builder->transformer(new JsonTransformer());
    }
}

  • TransformerInterface
  • FormBuilderInterface::transformer()

3 - 提取HTTP字段值(范围:Child)

在此步骤中,规范化的HTTP值传递给子元素,并提取当前字段值。如果值不可用,则返回null。

有两种提取策略

  • 数组偏移量:这是默认策略。使用简单的数组访问(如$httpValue[$name])提取字段值。默认情况下,HTTP字段名称与子名称相同。
  • 数组前缀:此策略仅适用于聚合元素,如for循环或数组。过滤HTTP值,仅保留以给定前缀开头的字段。

  • HttpFieldsInterface::extract()
  • ChildBuilder::httpFields()
  • ChildBuilder::prefix()
  • ChildInterface::submit()

4 - 应用过滤器(作用域:子)

一旦字段值被提取,就会应用过滤器。它们用于规范化和删除非法值。这是一个破坏性操作(无法逆转),与转换器不同。它们可以用于执行trimarray_filter

注意:与转换器不同,过滤器仅在从HTTP到PHP的转换过程中应用。如果是一个“视图”操作,如解码字符串,请不要使用。

// Filter for keep only alpha numeric characters
class AlphaNumFilter implements \Bdf\Form\Filter\FilterInterface
{
    public function filter($value,\Bdf\Form\Child\ChildInterface $input,$default)
    {
        if (!is_string($value)) {
            return null;
        }

        return preg_replace('/[^a-z0-9]/i', '', $value);
    }
}

class MyForm extends CustomForm
{
    protected function configure(FormBuilderInterface $builder): void
    {
        $builder->string('foo')->filter(new AlphaNumFilter())->setter();
    }
}

$form = new MyForm();
$form->submit(['foo' => '$$$abc123___']);
$form->value()['foo'] === 'abc123'; // The string is filtered

  • FilterInterface
  • ChildBuilderInterface::filter()

5 - 设置默认值(作用域:子)

如果过滤后的字段值被认为是空的并且提供了默认值,则设置默认值。一个值是空的,如果它是一个空字符串''或数组[],或者它是null00.0false不被视为空。如果没有提供默认值,则使用过滤后的值。

注意:要设置默认值,应使用PHP值调用ChildBuilderInterface::default()

  • HttpValue::orDefault()
  • ChildBuilderInterface::default()

6 - 调用元素转换器(作用域:元素)

与表单转换器(步骤2)类似,但针对元素值。如果转换器抛出异常,则提交过程将停止,将保留原始HTTP值,并将元素标记为无效,异常消息作为错误。

可以在ElementBuilder上更改转换器异常行为。如果通过在构建器上调用ignoreTransformerException()忽略异常,则将在原始HTTP值上执行验证过程。

// Declare a transformer
class Base64Transformer implements \Bdf\Form\Transformer\TransformerInterface
{
    public function transformToHttp($value,\Bdf\Form\ElementInterface $input)
    {
        return base64_encode($value);
    }

    public function transformFromHttp($value,\Bdf\Form\ElementInterface $input)
    {
        $value = base64_decode($value);
        
        // Throw exception on invalid value
        if ($value === false) {
            throw new InvalidArgumentException('Invalid base64 data');
        }

        return $value;
    }
}

class MyForm extends CustomForm
{
    protected function configure(FormBuilderInterface $builder): void
    {
        // "foo" is a base64 string input
        $builder
            ->string('foo')
            ->transformer(new Base64Transformer())
            ->transformerErrorMessage('Expecting base 64 data') // Define custom transformer error code and message
            ->transformerErrorCode('INVALID_BASE64_ERROR')
        ;
    }
}

  • ElementBuilderInterface::transformer()
  • ValidatorBuilderTrait::ignoreTransformerException()
  • ValidatorBuilderTrait::transformerErrorMessage()
  • ValidatorBuilderTrait::transformerErrorCode()

7 - 转换为PHP值(作用域:元素)

将值从HTTP值转换为可用的PHP值,例如在IntegerElement上执行转换为int。

注意:此步骤仅在LeafElement实现上执行

  • LeafElement::toPhp()
  • LeafElement::fromPhp()

8 - 验证(作用域:元素)

使用约束验证元素的PHP值。

  • ElementInterface::error()
  • ElementInterface::valid()
  • ElementBuilderInterface::satisfy()

9 - 生成表单值(作用域:表单)

创建通过表单值填充的实体。

  • ValueGeneratorInterface
  • FormInterface::value()
  • FormBuilderInterface::generator()
  • FormBuilderInterface::generates()

10 - 应用模型转换器(作用域:子)

调用模型转换器,以将输入数据转换为模型数据。

class MyForm extends CustomForm
{
    protected function configure(FormBuilderInterface $builder): void
    {
        // The date should be saved as timestamp on the entity
        $builder
            ->dateTime('date')
            ->saveAsTimestamp()
            ->setter()
        ;

        // Save data as mongodb Binary        
        $builder
            ->string('data')
            ->modelTransformer(function ($value, $input, $toModel) {
                return $toModel ? new Binary($value, Binary::TYPE_GENERIC) : $value->getData();
            })
            ->setter()
        ;
    }
}

  • ChildBuilderInterface::modelTransformer()

11 - 调用访问器(作用域:子)

访问器用于填充实体(如果是HTTP到PHP),或从实体导入表单(如果是PHP到表单)。

  • ChildBuilder::setter()
  • ChildBuilder::getter()
  • ChildBuilder::hydrator()
  • ChildBuilder::extractor()

12 - 验证表单值(作用域:表单)

一旦值被填充,它将通过表单约束进行验证。

class MyForm extends CustomForm
{
    protected function configure(FormBuilderInterface $builder): void
    {
        $builder->generates(MyEntity::class);

        $builder->string('foo')->setter();
        $builder->string('bar')->setter();
        
        $builder->satisfy(function (MyEntity $entity) {
            // Validate the hydrated entity
            if (!$entity->isValid()) {
                return 'Invalid entity';
            }
        });
    }
}

  • FormInterface::valid()
  • FormInterface::error()
  • FormBuilderInterface::satisfy()

嵌入式和数组

可以使用嵌入式表单和通用数组元素创建复杂表单结构。嵌入式表单对于在另一个表单中重用表单很有用。

<?php 

class UserForm extends \Bdf\Form\Custom\CustomForm
{
    protected function configure(\Bdf\Form\Aggregate\FormBuilderInterface $builder): void
    {
        // Define a sub-form "credentials", which generates a Credentials object
        $builder->embedded('credentials', function (\Bdf\Form\Child\ChildBuilderInterface $builder) {
            // $builder is type of ChildBuilderInterface, but forward call to FormBuilderInterface
            // So it can be used like a simple form builder
            
            $builder->generates(Credentials::class);
            $builder->string('username')->required()->length(['min' => 3])->getter()->setter();
            $builder->string('password')->required()->length(['min' => 6])->getter()->setter();
        });
        
        // Define an array of Address instances
        $builder->array('addresses')->form(function (FormBuilderInterface $builder) {
            $builder->generates(Address::class);
            $builder->string('address')->required()->getter()->setter();
            $builder->string('city')->required()->getter()->setter();
            $builder->string('zipcode')->required()->getter()->setter();
            $builder->string('country')->required()->getter()->setter();
        });

        // embedded and leaf fields can be mixed on the same form 
        $builder->string('email')->required()->getter()->setter();
    }
}

此表单将处理如下数据:

[
    'credentials' => [
        'username' => 'jdoe',
        'password' => 'p@ssw04d'
    ],
    'addresses' => [
        ['address' => '147 Avenue du Parc', 'city' => 'Villes-sur-Auzon', 'zipcode' => '84148', 'country' => 'FR'],
        ['address' => '20 Rue de la paix', 'city' => 'Gordes', 'zipcode' => '84220', 'country' => 'FR'],
    ],
    'email' => 'jdoe@example.com',
]

或者以HTTP格式

credentials[username]=jdoe
&credentials[password]=p@ssw04d
&addresses[0][address]=147 Avenue du Parc
&addresses[0][city]=Villes-sur-Auzon
&addresses[0][zipcode]=84148
&addresses[0][country]=FR
&addresses[1][address]=20 Rue de la paix 
&addresses[1][city]=Gordes
&addresses[1][zipcode]=84220 
&addresses[1][country]=FR
&email=jdoe@example.com

为了提高可读性和可重用性,每个嵌入式表单都可以在其自己的类中声明

<?php 

class CredentialsForm extends \Bdf\Form\Custom\CustomForm
{
    protected function configure(\Bdf\Form\Aggregate\FormBuilderInterface $builder) : void
    {
        $builder->generates(Credentials::class);
        $builder->string('username')->required()->length(['min' => 3])->getter()->setter();
        $builder->string('password')->required()->length(['min' => 6])->getter()->setter();
    }
}

class AddressForm extends \Bdf\Form\Custom\CustomForm
{
    protected function configure(\Bdf\Form\Aggregate\FormBuilderInterface $builder) : void
    {
        $builder->generates(Address::class);
        $builder->string('address')->required()->getter()->setter();
        $builder->string('city')->required()->getter()->setter();
        $builder->string('zipcode')->required()->getter()->setter();
        $builder->string('country')->required()->getter()->setter();
    }
}

class UserForm extends \Bdf\Form\Custom\CustomForm
{
    protected function configure(\Bdf\Form\Aggregate\FormBuilderInterface $builder) : void
    {
        // Simply define element with the embedded form class name
        $builder->add('credentials', CredentialsForm::class);
        $builder->array('addresses', AddressForm::class);
        $builder->string('email')->required()->getter()->setter();
    }
}

您还可以使用ChildBuilderInterface::prefix()将HTTP字段“扁平化”。嵌入式表单将使用前缀而不是子数组。

<?php

class UserForm extends \Bdf\Form\Custom\CustomForm
{
    protected function configure(\Bdf\Form\Aggregate\FormBuilderInterface $builder) : void
    {
        // Simply define element with the embedded form class name
        $builder->add('credentials', CredentialsForm::class)->prefix();
        $builder->array('addresses', AddressForm::class);
        $builder->string('email')->required()->getter()->setter();
    }
}

使用前缀,新的数据格式将是

[
    'credentials_username' => 'jdoe',
    'credentials_password' => 'p@ssw04d'
    'addresses' => [
        ['address' => '147 Avenue du Parc', 'city' => 'Villes-sur-Auzon', 'zipcode' => '84148', 'country' => 'FR'],
        ['address' => '20 Rue de la paix', 'city' => 'Gordes', 'zipcode' => '84220', 'country' => 'FR'],
    ],
    'email' => 'jdoe@example.com',
]

或者以HTTP格式

credentials_username=jdoe
&credentials_password=p@ssw04d
&addresses[0][address]=147 Avenue du Parc
&addresses[0][city]=Villes-sur-Auzon
&addresses[0][zipcode]=84148
&addresses[0][country]=FR
&addresses[1][address]=20 Rue de la paix 
&addresses[1][city]=Gordes
&addresses[1][zipcode]=84220 
&addresses[1][country]=FR
&email=jdoe@example.com

字段路径和依赖

在某些情况下,应使用另一个字段的值来验证或转换字段值。为此目的,添加了字段依赖项:当一个字段依赖于另一个字段时,您可以使用 ChildBuilderInterface::depends() 来声明它。字段路径用于访问特定字段。

注意:依赖项会增加表单的复杂性,如果可能的话,建议在父表单上使用约束。

要创建字段路径(并访问所需的字段),您应使用 FieldPath::parse(),或 FieldFinderTrait

该格式类似于 Unix 文件系统路径,其中 / 作为字段分隔符,. 表示当前字段,.. 表示父级。使用路径开头的 / 将定义绝对路径。与 Unix 路径不同,默认情况下,路径从字段的父级开始(即相当于 ../)。

格式

[.|..|/] [fieldName] [/fieldName]...

使用

  • . 从当前元素开始路径(而不是从其父级开始)。当前元素必须是聚合元素,如表单,才能工作
  • .. 从当前元素的父级开始路径。这是默认行为,因此不需要在路径开始时使用 "../"。
  • / 是字段分隔符。当用于路径开头时,它表示路径是绝对的(即从根元素开始)。
  • fieldName 是字段名。名称是声明的名称,不是 HTTP 字段名称。

用法

<?php 
// Using "low level" FieldPath helper
class CredentialsForm extends \Bdf\Form\Custom\CustomForm
{
    protected function configure(\Bdf\Form\Aggregate\FormBuilderInterface $builder) : void
    {
        $builder->string('username');
        $builder->string('password');
        $builder->string('confirm')
            ->depends('password') // Password must be submitted before confirm
            ->satisfy(function ($value, \Bdf\Form\ElementInterface $input) {
                // Get sibling field value using FieldPath
                // Note: with FieldPath, the path is relative to the parent of the current field
                if ($value !== \Bdf\Form\Util\FieldPath::parse('password')->value($input)) {
                    return 'Confirm must be same as password';
                }
            })
        ;
    }
}

// Using FieldFinderTrait on custom form
class CredentialsForm extends \Bdf\Form\Custom\CustomForm
{
    use \Bdf\Form\Util\FieldFinderTrait;

    protected function configure(\Bdf\Form\Aggregate\FormBuilderInterface $builder) : void
    {
        $builder->string('username');
        $builder->string('password');
        $builder->string('confirm')
            ->depends('password') // Password must be submitted before confirm
            ->satisfy(function ($value, \Bdf\Form\ElementInterface $input) {
                // Get sibling field value using findFieldValue
                // Note: with FieldFinderTrait, the path is relative to the custom form
                if ($value !== $this->findFieldValue('password')) {
                    return 'Confirm must be same as password';
                }
            })
        ;
    }
}

FieldPath 也可以在表单外部和嵌入式表单中使用。

use Bdf\Form\Util\FieldPath;

$form = new UserForm();

// Find the username field, starting from the root
// Start the expression with "." to not start the path from the parent of UserForm (which do not exists)
$username = FieldPath::parse('./embedded/username')->resolve($form);

// Also works from a "leaf" field
$password = FieldPath::parse('password')->resolve($username);
// Same as above
$password = FieldPath::parse('../password')->resolve($username);

// Absolute path : get "email" field of the root form
$email = FieldPath::parse('/email')->resolve($username);

选择

选择系统用于允许一组值,例如,与 HTML <selecte> 元素一起使用。要定义选择,只需在元素构建器上调用 choice()(如果支持)。可以使用关联数组中值的键来定义标签。

$builder->string('country')
    ->choices([
        'France' => 'FR',
        'United-Kingdom' => 'UK',
        'Spain' => 'ES',
    ])
;

一旦定义,视图系统将自动将简单输入元素转换为 <select>。要手动渲染选择,您还可以调用 FieldViewInterface::choices() 以获取选择数组。

<select name="<?php echo $view->name(); ?>">
 <?php foreach ($view->choices() as $choice): ?>
     <option value="<?php echo $choice->value(); ?>"<?php echo $choice->selected() ? ' selected' : ''; ?>><?php echo $choice->label(); ?></option>
 <?php endforeach; ?>
</select>

按钮

可以定义提交按钮以处理同一表单上的多个操作。

表单

<?php

class MyForm extends \Bdf\Form\Custom\CustomForm
{
    const SAVE_BTN = 'save';
    const DELETE_BTN = 'delete';

    protected function configure(\Bdf\Form\Aggregate\FormBuilderInterface $builder) : void
    {
        $builder->string('foo');
        
        // Define buttons
        $builder->submit(self::SAVE_BTN);
        $builder->submit(self::DELETE_BTN);
    }
}

视图

<?php 
$view = (new MyForm())->view();
?>

<form action="action.php" method="post">
    <?php echo $view['foo']; ?>
    
    <!-- Render the buttons. Use inner() to define the button text -->
    <?php echo $view[MyForm::SAVE_BTN]->inner('Save'); ?>
    <?php echo $view[MyForm::DELETE_BTN]->inner('Delete'); ?>
</form>

控制器

<?php

$form = new MyForm();

// Submit the form 
$form->submit($_POST);

// Get the submitted button name
// The submit button is handled by the root element
switch ($btn = $form->root()->submitButton() ? $btn->name() : null) {
    case MyForm::SAVE_BTN:
        doSave($form->value());
        break;

    case MyForm::DELETE_BTN:
        doDelete($form->value());
        break;
        
    default:
        throw new Exception();
}

元素

StringElement

$builder->string('username')
    ->length(['min' => 3, 'max' => 32]) // Define length options
    ->regex('/[a-z0-9_-]+/i') // Define a validation regex
;

Email

带有 Email 约束的字符串元素

$builder->email('username')->mode(Email::VALIDATION_MODE_HTML5);

Url

带有 Url 约束的字符串元素

$builder->url('server')->protocols('ftp', 'sftp');

IntegerElement

$builder->integer('number')
    ->posivite() // Same as ->min(0)
    ->min(5) // The number must be >= 5
    ->max(9999) // The element must be <= 9999
    ->grouping(true) // The HTTP value will group values by thousands (i.e. 145 000 instead of 145000)
    ->raw(false) // If set to true, the input will be simply caster to int, and not transformed following the locale
;

FloatElement

$builder->float('number')
    ->posivite() // Same as ->min(0)
    ->min(1.1) // The number must be >= 1.1
    ->max(99.99) // The element must be <= 99.99
    ->grouping(true) // The HTTP value will group values by thousands (i.e. 145 000 instead of 145000)
    ->scale(2) // Only consider 2 digit on the decimal part
    ->raw(false) // If set to true, the input will be simply caster to int, and not transformed following the locale
;

BooleanElement

处理布尔值,如复选框。如果值存在且等于定义的值(默认为 1),则将值视为 true。

注意:在 HTTP 中,一个假值是一个不存在的值,因此不能在布尔元素上定义默认值。要定义“视图默认”值,请使用 ElementBuilderInterface::value() 而不是 ChildElementBuilder::default()

默认渲染器将渲染一个具有定义的 http 值和选中状态的 <input type="checkbox" />

$builder->boolean('enabled')
    ->httpValue('enabled') // Define the requireds HTTP field value
;

DateTimeElement

$builder->dateTime('eventDate')
    ->className(Carbon::class) // Define a custom date class
    ->immutable() // Same as ->className(DateTimeImmutable::class)
    ->format('d/m/Y H:i') // Define the parsed date format
    ->timezone('Europe/Paris') // Define the parse timezone. The element PHP value will be on this timezone
    ->before(new DateTime('+1 year')) // eventDate must be before next year
    ->beforeField('eventDateEnd') // Compare the field value to a sibling field (eventDateEnd)
    ->after(new DateTime()) // eventDate must be after now
    ->afterField('accessDate') // Compare the field value to a sibling field (accessDate)

PhoneElement

处理电话号码。此元素需要 giggsey/libphonenumber-for-php 包才能使用。此元素将返回一个 PhoneNumber 实例而不是 string

$builder->phone('contact')
    ->regionResolver(function () {
        return $this->user()->country(); // Resolve the phone region using a custom resolver
    })
    ->region('FR') // Force the region value for parse the phone number
    ->regionInput('address/country') // Use a sibling input for parse the number (do not forget to call `depends()`)
    ->allowInvalidNumber(true) // Do not check the phone number
    ->validateNumber('My error message') // Enable validation, and define the validator options (here the error message)
    ->setter()->saveAsString() // Save the phone number as string on the entity
;

CsrfElement

此元素允许减轻 CSRF。此元素需要 symfony/security-csrf 包。某些元素方法被禁止,如 import()、任何约束、转换器或默认值。视图将渲染为 <input type="hidden" />

$builder->csrf() // No need to set a name. by default "_token"
    ->tokenId('my_token') // Define the token id. By default is use CsrfElement::class, but define a custom tokenId permit to not share CSRF token between forms
    ->message('The token is invalid') // Define a custom error message
    ->invalidate(true) // The token is always invalidated after check, and should be regenerated
;

AnyElement

用于处理任何值类型的元素。这对于创建内联自定义元素很有用。但强烈建议不要这样做:优先使用原生元素或创建自定义元素。

$builder->add('foo', AnyElement::class) // No helper method are present
    ->satisfy(function ($value) { ... }) // Configure the element
    ->transform(function ($value) { ... })
;

创建自定义元素

您可以使用自定义元素来处理复杂类型,并在任何表单中重用。下面的例子展示了如何声明一个处理 UriInterface 对象的元素,这些对象使用 PSR-17 UriFactoryInterface 创建。

使用自定义表单

您可以使用 CustomForm 来声明一个元素。这种方法的优势是不需要实现底层接口,只需要扩展 CustomForm 类。但这种方法会消耗更多资源,灵活性较低,并且无法定义自定义构建器。

use Bdf\Form\Aggregate\FormBuilderInterface;
use Bdf\Form\Custom\CustomForm;
use Bdf\Form\ElementInterface;
use Bdf\Form\Transformer\TransformerInterface;

// Declare the element class
class UriElement extends CustomForm
{
    const INNER_ELEMENT = 'uri';

    /**
    * @var UriFactoryInterface 
    */
    private $uriFactory;
    
    // Set the UriFactoryInterface at the constructor
    public function __construct(?FormBuilderInterface $builder = null, ?UriFactoryInterface $uriFactory = null) 
    {
        parent::__construct($builder);
        
        $this->uriFactory = $uriFactory ?? new DefaultUriFactory(); // Default instance for the factory
    }

    protected function configure(FormBuilderInterface $builder) : void
    {
        // Define inner element to store value
        // The URI is basically a string
        $builder->string(self::INNER_ELEMENT);

        // The UriElement is a form, and supports only array values
        // Define a transformer to remap value to the inner element
        $builder->transformer(new class implements TransformerInterface {
            public function transformToHttp($value, ElementInterface $input)
            {
                // $value is an array of children value
                // Return only the inner element value
                return $value[UriElement::INNER_ELEMENT];
            }
            
            public function transformFromHttp($value, ElementInterface $input)
            {
                // $value is the URI string
                // Map it as array to the inner element
                return [UriElement::INNER_ELEMENT => $value];
            }           
        });

        // Define the value generator : parse the inner element value to UriInterface using the factory        
        $builder->generates(function () {
            return $this->uriFactory->createUri($this[self::INNER_ELEMENT]->element()->value());
        });
    }
}

使用LeafElement

如果使用 CustomForm 声明元素不够,或者您想优化此元素,可以使用底层的 LeafElement 类来声明自定义元素。

use Bdf\Form\Choice\ChoiceInterface;
use Bdf\Form\Leaf\LeafElement;
use Bdf\Form\Transformer\TransformerInterface;
use Bdf\Form\Validator\ValueValidatorInterface;

// Declare the element using LeafElement class
class UriElement extends LeafElement
{
    /**
     * @var UriFactory 
     */
    private $uriFactory;

    // Overrides constructor to add the factory
    public function __construct(?ValueValidatorInterface $validator = null, ?TransformerInterface $transformer = null, ?ChoiceInterface $choices = null, ?UriFactory $uriFactory = null) 
    {
        parent::__construct($validator, $transformer, $choices);
        
        $this->uriFactory = $uriFactory ?? new DefaultUriFactory(); // Use default instance
    }

    // Parse the HTTP string value using the factory
    protected function toPhp($httpValue): ?UriInterface
    {
        return $httpValue ? $this->uriFactory->createUri($httpValue) : null;
    }
    
    // Stringify the UriInterface instance
    protected function toHttp($phpValue): ?string
    {
        return $phpValue === null ? null : (string) $phpValue;
    }
}

use Bdf\Form\AbstractElementBuilder;
use Bdf\Form\Choice\ChoiceBuilderTrait;
use Bdf\Form\ElementInterface;

// Declare the builder
class UriElementBuilder extends AbstractElementBuilder
{
    use ChoiceBuilderTrait; // Enable choices building
    
    /**
     * @var UriFactory
     */
    private $uriFactory;
    
    public function __construct(?RegistryInterface $registry = null, ?UriFactory $uriFactory = null) 
    {
        parent::__construct($registry);

        $this->uriFactory = $uriFactory ?? new DefaultUriFactory();
    }
    
    // You can define custom builder methods for constrains or 
    public function host(string $hostName): self
    {
        return $this->satisfy(function (?UriInterface $uri) use($hostName) {
            if ($uri->getHost() !== $hostName) {
                return 'Invalid host name';
            }
        });
    }

    // Create the element
    protected function createElement(ValueValidatorInterface $validator, TransformerInterface $transformer) : ElementInterface
    {
        return new UriElement($validator, $transformer, $this->getChoices(), $this->uriFactory);
    }
}

// Register the element builder on the registry
// Use container to inject dependencies (here the UriFactoryInterface)
$registry->register(UriElement::class, function(Registry $registry) use($container) {
    return new UriElementBuilder($registry, $container->get(UriFactoryInterface::class));
});

用法

要使用自定义元素,只需调用 FormBuilderInterface::add() 方法,并将元素类名作为第二个参数。

$builder->add('uri', UriElement::class);

自定义子构建器

在某些情况下,定义自定义子构建器可能相关,例如注册模型转换器。要声明子构建器,只需扩展 ChildBuilder 类,并将其注册到 Registry

class MyCustomChildBuilder extends ChildBuilder
{
    public function __construct(string $name, ElementBuilderInterface $elementBuilder, RegistryInterface $registry = null)
    {
        parent::__construct($name, $elementBuilder, $registry);

        // Add a filter provider
        $this->addFilterProvider([$this, 'provideFilter']);
    }
    
    // Model transformer helper method
    public function saveAsCustom()
    {
        return $this->modelTransformer(new MyCustomTransformer());
    }
    
    // Provide default filter
    protected function provideFilter()
    {
        return [new MyFilter()];
    }
}

// Now you can register the child builder with the element builder
$registry->register(CustomElement::class, CustomElementBuilder::class, MyCustomChildBuilder::class);

错误处理

当表单出现错误时,会创建一个包含所有错误的 FormError 对象。

简单用法

如果子元素上有错误,使用 FormError::children() 来获取错误。如果没有子元素上的错误,而是在父元素上,请使用 FormError::global()。要获取错误的简单字符串表示,将错误转换为字符串。要获取形式为 [fieldName => error] 的数组表示,请使用 FormError::toArray()

if (!$form->error()->empty()) {
    // Simple render errors as string
    return new Response('<div class="alert alert-danger">'.$form->error().'</div>');
    
    // For simple API error system, use toArray()
    return new JsonReponse(['error' => $form->error()->toArray()]);
    
    // Or use accessors
    foreach ($form->error()->children() as $name => $error) {
        echo $error->field().' : '.$error->global().' ('.$error->code().')'.PHP_EOL;
    }
}

打印机

为了以可重用的方式格式化错误,可以实现一个 FormErrorPrinterInterface

/**
 * Print errors in format [httpField => ['code' => errorCode, 'message' => errorMessage]]
 */
class ApiPrinter implements \Bdf\Form\Error\FormErrorPrinterInterface
{
    private $errors = [];
    private $current;

    // Define the error message for the current element
    public function global(string $error) : void
    {
        $this->errors[$this->current]['message'] = $error;
    }
    
    // Define the error code for the current element
    public function code(string $code) : void
    {
        $this->errors[$this->current]['code'] = $code;
    }
    
    // Define the error element http field
    public function field(\Bdf\Form\Child\Http\HttpFieldPath $field) : void
    {
        $this->current = $field->get();
    }
    
    // Iterate on element's children
    public function child(string $name,\Bdf\Form\Error\FormError $error) : void
    {
        $error->print($this);
    }
    
    // Get all errors
    public function print()
    {
        return $this->errors;
    }
}

// Usage
return new JsonReponse(['errors' => $form->error()->print(new ApiPrinter())]);