b2pweb / bdf-form
简单灵活的表单库
Requires
- php: >=7.1
- symfony/polyfill-php80: ~1.22
- symfony/property-access: ~4.3|~5.0|~6.0|~7.0
- symfony/validator: ~4.3|~5.0|~6.0|~7.0
Requires (Dev)
- giggsey/libphonenumber-for-php: ~8.0
- phpunit/phpunit: ~7.0|~8.0|~9.0
- symfony/form: ~4.3|~5.0|~6.0|~7.0
- symfony/http-foundation: ~4.3|~5.0|~6.0|~7.0
- symfony/security-csrf: ~4.3|~5.0|~6.0|~7.0
- vimeo/psalm: ~4.30|~5.22
Suggests
- giggsey/libphonenumber-for-php: Required to use phone type (~8.0)
- symfony/security-csrf: For enable CSRF element
README
处理表单和请求验证的库。
目录表
使用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 - 应用过滤器(作用域:子)
一旦字段值被提取,就会应用过滤器。它们用于规范化和删除非法值。这是一个破坏性操作(无法逆转),与转换器不同。它们可以用于执行trim
或array_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 - 设置默认值(作用域:子)
如果过滤后的字段值被认为是空的并且提供了默认值,则设置默认值。一个值是空的,如果它是一个空字符串''
或数组[]
,或者它是null
。0
、0.0
或false
不被视为空。如果没有提供默认值,则使用过滤后的值。
注意:要设置默认值,应使用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 约束的字符串元素
$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())]);