Form组件

表单组件让你轻松地创建、处理和复用表单。

表单组件是一个工具,用于帮你解决问题,让最终用户能够与你程序中的数据进行互动或是修改它们。传统上这是通过HTML表单完成的,本组件专注于处理“往复于你的程序和客户端之间”的数据,无论这些数据是来自于常规表单,还是来自于一个API。

安装 

你可以通过下述两种方式安装:

然后,包容 vendor/autoload.php 文件,以开启Composer提供的自动加载机制。否则,你的程序将无法找到这个Symfony组件的类。

配置 

如果你使用的是完整版Symfony,框架已经帮你把表单组件配好了。若是如此,可直接跳到 创建一个简易表单

在Symfony中,表单被打造成对象,这些对象的构建则是通过一个form factory 来完成。创建一个表单是很简单的

1
2
3
use Symfony\Component\Form\Forms;
 
$formFactory = Forms::createFormFactory();

工厂(factory)可用于创建基本表单,但对一些极为重要的功能却缺少支持:

  • 处理请求: 支持处理请求和文件上传;

  • CSRF保护: 支持对跨站攻击(Cross-Site-Request-Forgery)的防护;

  • 模板: 整合模板层,让你在渲染模板时能够复用HTML片段;

  • 翻译: 支持对报错信息、字段标签以及其他字符串的翻译;

  • 验证: 整合了验证库,用于提交的信息生成验证失败的信息;

Symfony表单组件依赖于其他类库以解决上述问题。多数时候,你会利用Twig和Symfony的 HttpFoundation组件、Translation组件、Validator组件,但如果愿意,你也可以选择其他类库来替换它们。

下面诸小节解释了如何把这些类库整合到form factory之中。

如果需要一个可运行的表单示例,请参考 https://github.com/webmozart/standalone-forms

处理请求 

为了处理表单数据,你需要调用 handleRequest() 方法:

1
$form->handleRequest();

在这个方法的背后,是基于表单中所配置的HTTP方法(默认是POST)来借助正确的PHP超全局变量($_POST$_GET),进而使用 NativeRequestHandler 对象来读取数据。

如果你需要精准控制表单的提交连同其数据,可以使用 submit() 方法。更多内容请参考 如何使用submit()函数来处理表单提交

与HttpFoundation组件进行整合

如果你使用了HttpFoundation组件,那么应该添加 HttpFoundationExtension 到你的表单工厂(form factory):

1
2
3
4
5
6
use Symfony\Component\Form\Forms;
use Symfony\Component\Form\Extension\HttpFoundation\HttpFoundationExtension;
 
$formFactory = Forms::createFormFactoryBuilder()
->addExtension(new HttpFoundationExtension())
->getFormFactory();

现在,当你处理表单进程时,即可把 Request 对象传入 HandleRequest() 中:

1
$form->handleRequest($request);

要了解更多关于HttpFoundation组件的信息,或者如果安装它,请参考 HttpFoundation组件组件。

CSRF保护 

防护CSRF攻击,已被内建于表单组件,但你需要显式开启之,或者用定制方法来替代它。如果你要使用内建的防护,执行 composer require symfony/security-csrf 命令,即可包容Security CSRF Component。(译注:此为Security四大子组件之一。本文多半篇幅讲的是组件之间的整合,这是以单独使用诸多组件为前提的,若是使用完整版框架,就什么都不用管了……但Symfony组件的最大优势就是可以单独使用在任何程序中)

下面的例程向form factory中添加了CSRF保护:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use Symfony\Component\Form\Forms;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Form\Extension\Csrf\CsrfExtension;
use Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage;
use Symfony\Component\Security\Csrf\TokenGenerator\UriSafeTokenGenerator;
use Symfony\Component\Security\Csrf\CsrfTokenManager;
 
// create a Session object from the HttpFoundation component
// 从HttpFoundation组件中创建一个Session对象
$session = new Session();
 
$csrfGenerator = new UriSafeTokenGenerator();
$csrfStorage = new SessionTokenStorage($session);
$csrfManager = new CsrfTokenManager($csrfGenerator, $csrfStorage);
 
$formFactory = Forms::createFormFactoryBuilder()
// ...
->addExtension(new CsrfExtension($csrfStorage))
->getFormFactory();

从内部看,该扩展将自动添加一个隐藏字段到每一个表单(字段名称默认是_token),而字段值是通过CSRF generator自动生成的,表单在binding时,这个字段要被验证通过才行。

如果你并未使用HttpFoundation组件,你可以用 NativeSessionTokenStorage 来替代(上述代码中的SessionTokenStorage),它利用的是PHP原生session处理机制:

1
2
3
4
use Symfony\Component\Security\Csrf\TokenStorage\NativeSessionTokenStorage;
 
$csrfStorage = new NativeSessionTokenStorage();
// ...

Twig模板 

如果你正在使用Form组件来处理HTML表单,你需要某种方式,来轻松把你的表单对象渲染成HTML表单字段(包括字段value、字段error、字段label)。如果你愿意使用 Twig 来作为模板引擎的话,表单组件给出的整合方案(在操作表单时)是多种多样的。

使用 TwigBridge 来进行整合,而桥的作用就是在Twig和多个Symfony组件之间进行整合。若你在用Composerd,可以安装最新的3.x版,把下面的 require 命令添加到你的 composer.json 文件中:

1
2
3
4
5
{
"require": {
"symfony/twig-bridge": "~3.0"
}
}

TwigBridge的整合,为你带来了一些 Twig函数,用于帮你渲染每个字段所需的HTML widget(表单小部件)、label和error信息(还有其他一些东东也包括在内)。要配置整合,你需要启动(bootstrap)或访问(access)Twig,然后添加 FormExtension

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
use Symfony\Component\Form\Forms;
use Symfony\Bridge\Twig\Extension\FormExtension;
use Symfony\Bridge\Twig\Form\TwigRenderer;
use Symfony\Bridge\Twig\Form\TwigRendererEngine;
 
// the Twig file that holds all the default markup for rendering forms
// this file comes with TwigBridge
// TwigBridge自带的、拥有表单渲染所需的全部默认的HTML标记
$defaultFormTheme = 'form_div_layout.html.twig';
 
$vendorDir = realpath(__DIR__.'/../vendor');
// the path to TwigBridge library so Twig can locate the
// form_div_layout.html.twig file
// TwigBride类库的路径,用于让Twig锁定form_div_layout.html.twig文件
$appVariableReflection = new \ReflectionClass('\Symfony\Bridge\Twig\AppVariable');
$vendorTwigBridgeDir = dirname($appVariableReflection->getFileName());
// the path to your other templates
// 你自己的模板之路径
$viewsDir = realpath(__DIR__.'/../views');
 
$twig = new Twig_Environment(new Twig_Loader_Filesystem(array(
$viewsDir,
$vendorTwigBridgeDir.'/Resources/views/Form',
)));
$formEngine = new TwigRendererEngine(array($defaultFormTheme));
$formEngine->setEnvironment($twig);
 
// ... (see the previous CSRF Protection section for more information)
// ...(参考前述CSRF防护小节以了解更多)
// add the FormExtension to Twig
// 添加FormExtension到Twig
$twig->addExtension(
new FormExtension(new TwigRenderer($formEngine, $csrfManager))
);
 
// create your form factory as normal
// 按照常规创建form factory
$formFactory = Forms::createFormFactoryBuilder()
// ...
->getFormFactory();

Twig配置 的确切细节是多种多样的,但最终目标都是添加 FormExtension 给Twig,这个扩展能够让你访问Twig函数以便渲染表单(译注:render被译为渲染,是指把表单对象在模板中进行精确输出)。要实现目标,你首先要创建一个 TwigRendererEngine,这里是你定义 form themes 表单主题的地方(即是一些定义了表单的HTML标记块的资源或文件)。

渲染表单时的完整细节请参考 如何自定义表单渲染

如果你在表单组件中整合了Twig,下面的翻译小节中,有“表单翻译”所需的translation filters相关内容。(译注:Twig所说的filter,就是传统smarty模板中的变量调节)

翻译 

如果你整合了Twig到表单组件,并且使用了默认的“表单主题”文件之一(如form_div_layout.html.twig),可以使用两个Twig调节器(transtransChoice),它们可以用于翻译表单的labels、errors、选项文本(译注:choices类型的)和其他字符串。

要添加这俩Twig filters,你既可以使用内置的整合了Symfony的Translation组件的 TranslationExtension,也可以通过创建Twig Extention(Twig扩展)来自行添加之。

若使用内建的翻译扩展,确保你在项目中安装了Symfony的Translation组件和 Config 组件。使用Composer时,你可以安装最新的3.x版组件,只需在composer.json文件中添加下列代码:

1
2
3
4
5
6
{
"require": {
"symfony/translation": "~3.0",
"symfony/config": "~3.0"
}
}

下一步,添加 TranslationExtensionTwig_Enviroment实例中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use Symfony\Component\Form\Forms;
use Symfony\Component\Translation\Translator;
use Symfony\Component\Translation\Loader\XliffFileLoader;
use Symfony\Bridge\Twig\Extension\TranslationExtension;
 
// create the Translator 创建Translator
$translator = new Translator('en');
// somehow load some translations into it 加载翻译信息源到translator
$translator->addLoader('xlf', new XliffFileLoader());
$translator->addResource(
'xlf',
__DIR__.'/path/to/translations/messages.en.xlf',
'en'
);
 
// add the TranslationExtension (gives us trans and transChoice filters)
// 添加Twig模板所需之TranslationExtension(以便能够使用|trans和|transChoice调节器)
$twig->addExtension(new TranslationExtension($translator));
 
$formFactory = Forms::createFormFactoryBuilder()
// ...
->getFormFactory();

根据所加载的翻译源信息之种类不同,现在你已经能够添加字符串的keys,比如表单labels等,连同其翻译信息,到你的翻译源文件中。

更多翻译内容请参考:翻译

验证 

表单组件中紧密整合(但却是可选的)Symfony的Validator组件。你要使用其他验证方案,也没问题!只需把表单(可以是form type对象,或数组)中“已提交的/已绑定的”数据,传入你自己的验证体系。

如果选择Symfony的Validator组件,先安装它。你可以使用Composer来安装最新的3.x版,把它添加到 composer.json 中:

1
2
3
4
5
{
"require": {
"symfony/validator": "~3.0"
}
}

如果你对Symfony的Validator组件还不很熟悉的话,请参考 Validation 章节。表单组件内置了一个 ValidatorExtension 类,自动地将验证应用到你的已绑定数据上。然后,(验证未通过的)错误信息将被映射到相应字段,整合好的表单验证可能是下面代码这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
use Symfony\Component\Form\Forms;
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
use Symfony\Component\Validator\Validation;
 
$vendorDir = realpath(__DIR__.'/../vendor');
$vendorFormDir = $vendorDir.'/symfony/form';
$vendorValidatorDir = $vendorDir.'/symfony/validator';
 
// create the validator - details will vary
// 创建一个validator - 具体可以不同
$validator = Validation::createValidator();
 
// there are built-in translations for the core error messages
// 对于核心错误信息已经有内置的翻译
$translator->addResource(
'xlf',
$vendorFormDir.'/Resources/translations/validators.en.xlf',
'en',
'validators'
);
$translator->addResource(
'xlf',
$vendorValidatorDir.'/Resources/translations/validators.en.xlf',
'en',
'validators'
);
 
$formFactory = Forms::createFormFactoryBuilder()
// ...
->addExtension(new ValidatorExtension($validator))
->getFormFactory();

可以跳到 表单验证 小节以了解更多。

访问表单工厂 

你的程序仅仅需要一个form factory,然后一个工厂对象被用于创建程序所需的“任意、全部”的表单对象。这着你应该在程序中的一些中心点和起始点来创建工厂,然后可以在任何需要构建表单的时候来访问它。

在这个文档中,表单工厂始终是一个本地变量,被称为 $formFactory 。这表明,你也许需要以一种更加“全局化(global)”的方式来创建该对象,以便能够在任何地方来访问它。

具体如何来访问form factory取决于你自己。如果你使用了服务容器(比如 DependencyInjection组件),你应该把表单工厂添加到容器中,然后在你需要时取得。如果你的程序使用的是全局或静态变量(这不是什么好方案),那么你可以存储工厂对象到静态类或是类似的东西中。

创建一个简易表单 

若你使用了Symfony框架,表单工厂已经被自动注册成 form.factory 服务。同时,默认的控制器基类(base controller class)提供了一个 createFormBuilder() 快捷方法,用于取出form factory并调用其 createBuiler

FormBuilder 对象负责创建表单,它允许你构建和配置不同的字段。而form builder本身是被form factory创建的。

1
2
3
4
5
6
7
8
9
10
11
12
13
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
 
// ...
 
$form = $formFactory->createBuilder()
->add('task', TextType::class)
->add('dueDate', DateType::class)
->getForm();
 
var_dump($twig->render('new.html.twig', array(
'form' => $form->createView(),
)));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// src/Acme/TaskBundle/Controller/DefaultController.php
namespace Acme\TaskBundle\Controller;
 
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
 
class DefaultController extends Controller
{
public function newAction(Request $request)
{
// createFormBuilder is a shortcut to get the "form factory"
// and then call "createBuilder()" on it
 
$form = $this->createFormBuilder()
->add('task', TextType::class)
->add('dueDate', DateType::class)
->getForm();
 
return $this->render('AcmeTaskBundle:Default:new.html.twig', array(
'form' => $form->createView(),
));
}
}

如同你看到的,创建表单就像写一个食谱:对每一个你想建立的字段调用 add() 方法。add() 的第一个参数是字段名,第二个则是(译注:该字段对象的)FQCN类名。表单组件拥有大量的 内置表单类型

现在,你构造出自己的表单,下面要学习如何把它 渲染出来 以及 处理表单提交

设置默认值 

如果你想让表单带出一些默认值(或者你在构建一个“编辑”表单时),直接把这些默认值传入表单的builder即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
 
// ...
 
$defaults = array(
'dueDate' => new \DateTime('tomorrow'),
);
 
$form = $formFactory->createBuilder(FormType::class, $defaults)
->add('task', TextType::class)
->add('dueDate', DateType::class)
->getForm();
1
2
3
4
5
6
7
8
9
10
11
12
13
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
 
// ...
 
$defaults = array(
'dueDate' => new \DateTime('tomorrow'),
);
 
$form = $this->createFormBuilder($defaults)
->add('task', TextType::class)
->add('dueDate', DateType::class)
->getForm();

本例中,表单的默认数据是一个数组。后面,你可以使用 data_class 选项来直接绑定数据到form type对象中,默认数据化身为那个表单对象的实例。

渲染表单 

现在,表单已经创建完成,下一步是渲染它。这是通过传递一个特殊的“view”对象到你的模板中来完成的(注意上面代码中,控制器的 $form->createView() 方法),然后使用一组(Twig提供的)表单helper函数:

1
2
3
4
5
{{ form_start(form) }}
{{ form_widget(form) }}
 
<input type="submit" />
{{ form_end(form) }}

就是这样!使用 form_widget(form) 时,表单中的每个字段都会被渲染,连同其label和错误信息(如果有的话)等。这虽然简单,尚且缺乏灵活性(暂时)。通常,你想要独立渲染每一个表单字段,以便控制它们的呈现样式。你可以在学习如何在模板中输出表单

改变表单的Method和Action 

默认情况是,表单以HTTP POST方式提交到“渲染它的URI”中去。这个行为可以用 actionmethod 选项来改变(mehtod 选项也适用于 handleRequest() 方法,以决定表单是否被提交):

1
2
3
4
5
6
7
8
9
10
use Symfony\Component\Form\Extension\Core\Type\FormType;
 
// ...
 
$formBuilder = $formFactory->createBuilder(FormType::class, null, array(
'action' => '/search',
'method' => 'GET',
));
 
// ...
1
2
3
4
5
6
7
8
9
10
11
12
13
use Symfony\Component\Form\Extension\Core\Type\FormType;
 
// ...
 
public function searchAction()
{
$formBuilder = $this->createFormBuilder(null, array(
'action' => '/search',
'method' => 'GET',
));
 
// ...
}

处理表单提交 

处理表单的提交,用的是 handleRequest()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
 
// ...
 
$form = $formFactory->createBuilder()
->add('task', TextType::class)
->add('dueDate', DateType::class)
->getForm();
 
$request = Request::createFromGlobals();
 
$form->handleRequest($request);
 
if ($form->isValid()) {
$data = $form->getData();
 
// ... perform some action, such as saving the data to the database
// ... 进行一些操作,比如把数据存到数据库中
$response = new RedirectResponse('/task/success');
$response->prepare($request);
 
return $response->send();
}
 
// ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
 
// ...
 
public function newAction(Request $request)
{
$form = $this->createFormBuilder()
->add('task', TextType::class)
->add('dueDate', DateType::class)
->getForm();
 
$form->handleRequest($request);
 
if ($form->isValid()) {
$data = $form->getData();
 
// ... perform some action, such as saving the data to the database
// ... 进行一些操作,比如把数据存到数据库中
 
return $this->redirectToRoute('task_success');
}
 
// ...
}

这里定义了一个普通表单的“工作流”,包括以下3种可能性:

  1. 在原本的GET请求(比如,当用户“来到”你的页面)上构建表单并渲染之:

如果请求是POST,对提交过来的数据进行处理(通过 handleRequest() 方法),然后:

  1. 如果表单是无效的,重新渲染表单(此时的表单会包含错误信息)

  2. 如果表单有效,进行某些操作,然后重定向。

幸运的是,你毋需判断表单是否被提交,只要把当前的request传到 handleRequest() 方法即可。表单组件会帮你打点剩下的必要工作。

表单验证 

为你的表单添加验证的最简单办法,就是在绑定每一个字段(到表单对象)时,使用 constraints 选项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Type;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
 
$form = $formFactory->createBuilder()
->add('task', TextType::class, array(
'constraints' => new NotBlank(),
))
->add('dueDate', DateType::class, array(
'constraints' => array(
new NotBlank(),
new Type('\DateTime'),
)
))
->getForm();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\Type;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
 
$form = $this->createFormBuilder()
->add('task', TextType::class, array(
'constraints' => new NotBlank(),
))
->add('dueDate', DateType::class, array(
'constraints' => array(
new NotBlank(),
new Type('\DateTime'),
)
))
->getForm();

要查看全部的内置validation constraints之列表,请移步 Validation Constraints Reference 验证约束参考。

获取表单错误 

你可以使用 getErrors() 方法来获取错误列表。该方法返回的是 FormErrorIterator 实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$form = ...;
 
// ...
 
// a FormErrorIterator instance, but only errors attached to this
// form level (e.g. "global errors)
// FormErrorIterator实例,包含了整个表单(form level)的错误
$errors = $form->getErrors();
 
// a FormErrorIterator instance, but only errors attached to the
// "firstName" field
// FormErrorIterator实例,仅含有指定字段的错误
$errors = $form['firstName']->getErrors();
 
// a FormErrorIterator instance in a flattened structure
// use getOrigin() to determine the form causing the error
// FormErrorIterator实例,是扁平数组结构
// 使用getOrigin()方法来找出导致错误的表单
$errors = $form->getErrors(true);
 
// a FormErrorIterator instance representing the form tree structure
// FormErrorIterator实例,以表单树状结构来呈现
$errors = $form->getErrors(true, false);

本文,包括例程代码在内,采用的是 Creative Commons BY-SA 3.0 创作共用授权。

登录symfonychina 发表评论或留下问题(我们会尽量回复)