对于一个Web开发者来说,处理HTML表单是一个最为普通又具挑战的任务。Symfony2集成了一个Form组件,让处理表单变的容易起来。在这一节里,我们将从基础开始创建一个复杂的表单,学习表单类库中最重要的内容。
Symfony2 的Form组件是一个独立的类库,你可以在Symfony2项目之外使用它。
创建一个简单的表单
假设你正在构建一个简单的待办事项列表,来显示一些‘任务’。你需要创建一个表单来让你的用户能够编辑和创建任务。在这之前,先来看看Task类,他用来存储任务数据。
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 |
// src/AppBundle/Entity/Task.php namespace AppBundle\Entity; class Task { protected $task; protected $dueDate; public function getTask() { return $this->task; } public function setTask($task) { $this->task = $task; } public function getDueDate() { return $this->dueDate; } public function setDueDate(\DateTime $dueDate = null) { $this->dueDate = $dueDate; } } |
该类是一个普通的PHP对象类,因为他们没有任何Symfony或者其它类库引用。它是非常简单的一个PHP对象类,它直接解决了你程序中task的数据问题。当然,在本节的最后,你将能够通过HTML表单提交一个Task实例数据,校验它的数值,并把它持久化到数据库。
构建表单
现在我们已经有了一个Task类,下一步就是创建和渲染一个真正的html表单了。再symfony2中,它是通过创建一个表单对象来渲染到模版的。现在,我们要在controller内部处理form表单:
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 |
// src/AppBundle/Controller/DefaultController.php namespace AppBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use AppBundle\Entity\Task; use Symfony\Component\HttpFoundation\Request; class DefaultController extends Controller { public function newAction(Request $request) { // create a task and give it some dummy data for this example $task = new Task(); $task->setTask('Write a blog post'); $task->setDueDate(new \DateTime('tomorrow')); $form = $this->createFormBuilder($task) ->add('task', 'text') ->add('dueDate', 'date') ->add('save', 'submit', array('label' => 'Create Task')) ->getForm(); return $this->render('default/new.html.twig', array( 'form' => $form->createView(), )); } } |
这个例子说明了如何在控制器中直接建立自己的form表单。在后面,“创建表单类”我们将使用一个独立的类来构建form表单,这样可以让表单可以重用,我们推荐这样做。
因为Symfony2通过一个表单生成器“form builder”来创建表单对象,所以你可以使用很少的代码就能完成创建表单任务。表单生成器的目的是让你能编写简单的表单创建方法,让它来负责繁重的创建任务。
在这个示例中,你已经添加了两个字段到你的表单,一个是task一个是dueDate。它们关联到Task类的task和dueDate属性。你已经为它们分别指定了类型(比如,text,date等),由这些类型来决定为这些字段生成什么样的HTML表单标签。
最后,你增加了一个提交按钮并自定义了label名称。
2.3时提交按钮被引入到symfony。在此之前,你必须要手动添加按钮到html页面。
symfony附带了很多的内建form类型(Form类型手册)
渲染一个表单
表单创建后,下一步就是渲染它。这是通过传递一个特定的表单”view”对象(就是上例中的 $form->createView()返回的view对象)到你的模板并通过一系列的表单帮助函数来实现的。
1 2 3 4 |
{# app/Resources/views/default/new.html.twig #} {{ form_start(form) }} {{ form_widget(form) }} {{ form_end(form) }} |
这个示例假设你提交的POST请求和现在的URL是相同。后面我们将教您如何改变请求的方法和表单提交的URL。
就是这样!只需要短短三行就可以渲染完整的form表单:
form_start(form)
呈现出form表单的开始标签,包含当文件上传时配置的正确enctype属性。
form_widget(form)
呈现出所有的字段,其中包括字段本身,一个label和一些验证的错误信息。
form_end(form)
当你自己生成每个字段时可能会有遗漏,这个函数就是生成表单结束标签,以及表单中所有没有被生成的字段的。这个生成隐藏字段时,以及在利用自动CSRF保护机制时,将会非常有用。
是不是很简单,但是他并不很灵活。通常情况下,我们渴望单独渲染表单中的每一个字段,这样我们可以更好的控制表单的样式。我们会在在模板中渲染表单一节介绍。
在继续下去之前,我们注意到,为什么我们渲染出来的task输入框中有一个来自$task对象的属性值“Write a blog post”。这是表单的第一个工作:从一个对象中获取数据并把它转换为合适的格式渲染到一个HTML表单中。
注意,表单系统已经足够聪明,它们能够通过像getTask()和setTask()方法来访问Task类中受保护的属性task。除非一个是公共属性,否则必须有一个getter和setter方法被定义来用于表单组件从这些属性中获取和保持数据。对于布尔型的属性,你可以使用一个”isser”和“hasser”方法(比如 isPublished()和hasReminder())替代getter方法(getPublished()和getReminder())。
处理表单提交
表单系统的第二个任务就是传递用户提交的数据回到一个对象的属性中。要做到这一点,用户提交的数据必须绑定到表单才行。添加如下代码到你的Controller类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// ... use Symfony\Component\HttpFoundation\Request; public function newAction(Request $request) { // just setup a fresh $task object (remove the dummy data) $task = new Task(); $form = $this->createFormBuilder($task) ->add('task', 'text') ->add('dueDate', 'date') ->add('save', 'submit', array('label' => 'Create Task')) ->getForm(); $form->handleRequest($request); if ($form->isValid()) { // perform some action, such as saving the task to the database return $this->redirectToRoute('task_success'); } // ... } |
在symfony2.3中handleRequest()被引入进来。在此之前,这个$request会被传入到submit方法-这种方式已经弃用了,会在symfony3.0被彻底移除。有关这个方法的详细信息,请参见传递一个$request到Form::submit()(以弃用)。
controller一般会遵循一个通用的模式来处理表单,它有三个可能的途径:
1.当在浏览器初始加载一个页面时,请求方法是GET,表单处理仅仅是创建和渲染。handleRequest()承认表单没有提交并且没有变化。如果表单没有提交,isValid()返回false。
2.当用户提交表单,handleRequest()会识别这些并立即将提交数据返回到$task对象的task和duedate属性。然后对这个对象进行验证。如果它是无效的(验证在下一章)isVaild()会返回false,所以该form错误验证会一起渲染。
无论提交的数据是否有效,你都能使用isSubmitted()检测表单是否提交。
3.当用户提交的表单带有的数据均合法时,提交的数据会被再次写入到表单,但这一次isValid()返回ture。表单绑定并且在页面跳转之前你有机会去使用数据去执行一些业务逻辑活动,比如持久化它$task对象到数据库)。
用户提交表单后重定向可以防止用户的浏览器刷新并转发数据。
当表单提交或者数据传递时,你需要更多的控制,你就可以使用submit().更多请查看cookbook。
多个按钮提交表单
再symfony2.3中form中可以支持按钮了。
当你的表单支持多个提交按钮,你需要检测哪个按钮被点击了,去挑战控制器程序逻辑。现在我们要再添加一个“Save and add”按钮到表单:
1 2 3 4 5 6 |
$form = $this->createFormBuilder($task) ->add('task', 'text') ->add('dueDate', 'date') ->add('save', 'submit', array('label' => 'Create Task')) ->add('saveAndAdd', 'submit', array('label' => 'Save and Add')) ->getForm(); |
再你的控制器中使用isClicked()方法来判断“Save and add”是否被点击:
1 2 3 4 5 6 7 8 9 |
if ($form->isValid()) { // ... perform some action, such as saving the task to the database $nextAction = $form->get('saveAndAdd')->isClicked() ? 'task_new' : 'task_success'; return $this->redirectToRoute($nextAction); } |
表单验证
在上一节中,你学会了如何让表单提交有效和无效数据。在symfony中,验证环节是在底层的对象中进行(例如Task)。换句话说,form表单合法与否不重要,主要看在表单提交数据以后,底层对象比如$task对象是否合法。调用$form->isvalid() 是一个询问底层对象是否获得合法数据的快捷方式。
校验是通过添加一些列规则(约束)到一个类来完成的。我们给Task类添加规则和约束,使它的task属性不能为空,duDate字段不能空并且是一个合法的DateTime对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// AppBundle/Entity/Task.php use Symfony\Component\Validator\Constraints as Assert; class Task { /** * @Assert\NotBlank() */ public $task; /** * @Assert\NotBlank() * @Assert\Type("\DateTime") */ protected $dueDate; } |
就是这样了,如果你现在再提交包含非法数据的表单,你将会看到相应的错误被打印在表单上。
html5验证
作为HTML5,许多浏览器都加强了客户端某些校验约束。最常用的校验活动是在一个必须的字段上渲染一个required属性。对于支持HTML5的浏览器来说,如果用户此时提交一个空字段到表单时,浏览器会显示提示信息。
生成的表单广泛吸收了这些新内容的优点,通过添加一些HTML属性来监控校验。客户端校验可以通过添加novalidate属性到form标签或者formnovalidate 到提交标签而关闭。这对你想检查服务端校验规则,却被浏览器阻止时,非常有用。
12 {# app/Resources/views/default/new.html.twig #}{{ form(form, {'attr': {'novalidate': 'novalidate'}}) }}
验证在symfony中是一个非常强大的功能,并拥有自己的专属章节。
验证分组
如果你的对象想从验证组中受益,你需要指定你的表单使用哪个校验组。
1 2 3 |
$form = $this->createFormBuilder($users, array( 'validation_groups' => array('registration'), ))->add(...); |
如果你创建表单类,你需要添加以下的getDefaultOptions()方法:
1 2 3 4 5 6 7 8 |
use Symfony\Component\OptionsResolver\OptionsResolverInterface; public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'validation_groups' => array('registration'), )); } |
在这两种情况下,只有registration验证组将被用于验证底层对象。
禁用验证
在symfony2.3中validation_groups才开始可以设置为false。
有时你可能想完全禁用表单验证。对于这种情况,您可以设置validation_groups为false:
1 2 3 4 5 6 7 8 |
use Symfony\Component\OptionsResolver\OptionsResolverInterface; public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'validation_groups' => false, )); } |
需要注意的是,当你这样做,form仍将运行基本的验证,例如上传文件过大,还有表单提交的字段根本不存在等。如果你想彻底去除验证,你可以使用POST_SUBMIT 事件。
组根据提交的数据
如果在验证组你需要一些高级的逻辑(例如根据提交的数据),你可以设置validation_groups为数组:
1 2 3 4 5 6 7 8 9 10 11 12 |
use Symfony\Component\OptionsResolver\OptionsResolverInterface; // ... public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'validation_groups' => array( 'AppBundle\Entity\Client', 'determineValidationGroups', ), )); } |
这将在表单提交后,执行验证之前,调用Client类的静态方法determineValidationGroups()。表单对象作为一个参数传入到该方法(下面例子)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
use Acme\AcmeBundle\Entity\Client; use Symfony\Component\Form\FormInterface; use Symfony\Component\OptionsResolver\OptionsResolverInterface; // ... public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'validation_groups' => function(FormInterface $form) { $data = $form->getData(); if (Client::TYPE_PERSON == $data->getType()) { return array('person'); } return array('company'); }, )); } |
使用validation_groups配置去覆盖正在使用的默认验证组。如果你想要去验证实体的默认约束就必须调整选项如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
use Acme\AcmeBundle\Entity\Client; use Symfony\Component\Form\FormInterface; use Symfony\Component\OptionsResolver\OptionsResolverInterface; // ... public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'validation_groups' => function(FormInterface $form) { $data = $form->getData(); if (Client::TYPE_PERSON == $data->getType()) { return array('Default', 'person'); } return array('Default', 'company'); }, )); } |
在book的验证组章节你可以找到更多验证组和默认约束的细节。
组根据点击按钮
在symfony2.3中表单可以添加按钮
当你的表单包含多个按钮,你可以根据哪个按钮提交来改变表单验证组。例如,一个表单引导,允许您前进到下一步或返回到上一步。当返回到上一步,表单的数据应该被保存,但不能验证。
首先,我们要添加两个按钮到窗体:
1 2 3 4 5 |
$form = $this->createFormBuilder($task) // ... ->add('nextStep', 'submit') ->add('previousStep', 'submit') ->getForm(); |
这时我们返回上一步就会运行特定的验证组。在这个例子中,如果你想去不让验证,我们就得将validation_groups选项设置为false:
1 2 3 4 5 6 |
$form = $this->createFormBuilder($task) // ... ->add('previousStep', 'submit', array( 'validation_groups' => false, )) ->getForm(); |
现在form将跳过验证约束。但是他仍将验证完整的基本约束,如检查一个上传文件太大或者你提交无效字段等。
内建字段类型
Symfony标准版含有大量的字段类型,它们几乎涵盖了所有通用表单的字段和数据类型。
字段组
基础字段
当然,你也可以定义自己的字段类型。可以查看 How to Create a Custom Form Field Type。
字段类型选项
每一个字段类型都有一定数量的选项用于配置。比如,dueDate字段当前被渲染成3个选择框。而日期字段可以被配置渲染成一个单一的文本框,用户可以输入字符串作为日期。
1 |
->add('dueDate', 'date', array('widget' => 'single_text')) |
每个字段类型都有很多不同的选项,可以传递进来。关于字段类型的细节都可以在每个类型的文档中找到。
required选项:
最常用到的选项是required选项,它可以应用于任何字段。默认情况下它被设置为true。这就意味着支持HTML5的浏览器会使用客户端校验来判断字段是否为空。如果你不想让它发生,或者把在你的字段上把required选项设置为false,或者关闭HTML5校验。
设置required为true并不意味着服务端校验被应用。换句话说,如果用户提交一个空数值到该字段,它将接受这个控制除非你使用Symfony的NotBlank或者NotNull校验约束。也就是说,required选项是很好,但是服务端校验还是要继续用。
label选项:
表单字段可以使用label选项设置显示字符标签,可以应用于任何字段:
1 2 3 4 |
->add('dueDate', 'date', array( 'widget' => 'single_text', 'label' => 'Due Date', )) |
这个label是一个字段也能在模版渲染表单时设置,看下文。如果你不需要label关联到你的input,你可以设置他的value为false。
字段类型猜测
现在你已经添加了验证元数据到Task类,Symfony早已经了解一点关于你的字段了。如果你允许,Symfony可以猜到你的字段数据类型并为你设置它。在下面的例子中,Symfony可以根据校验规则猜测到task字段是一个标准的text字段,dueDate是date字段。
1 2 3 4 5 6 7 8 9 10 |
public function newAction() { $task = new Task(); $form = $this->createFormBuilder($task) ->add('task') ->add('dueDate', null, array('widget' => 'single_text')) ->add('save', 'submit') ->getForm(); } |
当你省略了add方法的第二个参数(或者你输入null)时,Symfony的猜测能力就起作用了。如果你输入一个选项数组作为第三个参数(比如上面的dueDate),那么这些选项会成为Symfony猜测的依据。如果你的表单使用了指定的验证数组,字段类型猜测器将还是要考虑所有的验证规则来综合猜测你的字段类型。
如果你的表单使用特定的验证组,猜测字段类型时仍将考虑所有验证约束(包含不在这部分的约束)。
字段类型可选项猜测
除了猜测字段类型,Symfony还能是这猜测一些可选项字段值。
当这些可选项被设置时,字段将会被渲染到特定HTML属性中,让HTML5客户端来提供验证。然而,它们不会在服务端生成相应的验证规则(例如Assert\Length)。尽管你需要手动的在服务端添加这些规则,但是这些字段类型选项还是能根据这些信息猜测到。
required
required可以在验证规则(例如NotBlank和
NotNull
)或者Doctrine元数据(例如字段是nullable)的基础上猜测到。这当你的客户端校验将自动匹配你的校验规则时很有用。
max_length
如果字段是一些列文本字段,那么max_length选项可以从验证规则(如果使用Length和Range)或者Doctrine元数据(例如字段长度)中猜到。
symfony要猜测这些字段就必须要在add()方法为空或者不设置时生效。
如果你喜欢改变一个猜到的数值,你可以通过在可选项数组中传递该选项来重写它。
1 |
->add('task', null, array('attr' => array('maxlength' => 4))) |
在模版中渲染表单
到目前为止,我们已经看了一个完整的表单是如何通过一行代码被渲染的。当然,你通常需要更加灵活的渲染方式:
1 2 3 4 5 6 7 |
{# app/Resources/views/default/new.html.twig #} {{ form_start(form) }} {{ form_errors(form) }} {{ form_row(form.task) }} {{ form_row(form.dueDate) }} {{ form_end(form) }} |
你已经知道form_start()和form_end()函数,但是其他功能是做什么的呢?
form_errors(form)
渲染任何整个form的错误信息(特定字段的错误,会显示在每个字段的下面一行)。
form_row(form.dueDate)
默认情况下,为给定的字段在一个div中渲染一个文本标签,错误信息,和HTML表单部件。
大部分工作是由form_row帮助方法类完成的,它默认在一个div中为每个字段渲染显示标签,错误信息和HTML表单部件。在表单主题部分,你将学会如果在form_row定制不同风格。
注意,你可以通过form.vars.value 来访问你的当前数据:
1 {{ form.vars.value.task }}
手动配置每一个字段
form_row帮助器是伟大的,能让你很快的渲染你表单中的每一个字段(并且每一行可以被自定义化)。但是生活不总是那么简单的,你也可能要手动的渲染每一个字段。下面的代码呈现的样子和你使用form_row呈现的样子是一样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
{{ form_start(form) }} {{ form_errors(form) }} <div> {{ form_label(form.task) }} {{ form_errors(form.task) }} {{ form_widget(form.task) }} </div> <div> {{ form_label(form.dueDate) }} {{ form_errors(form.dueDate) }} {{ form_widget(form.dueDate) }} </div> <div> {{ form_widget(form.save) }} </div> {{ form_end(form) }} |
如果自动生成的label不是你想要的,你可以指定它:
1 |
{{ form_label(form.task, 'Task Description') }} |
一些字段类型有一些额外的渲染选项可以传入widget,一个常用的选项为attr,它允许你修改表单元素的属性。下面的示例将添加task_field class到渲染的文本输入字段:
1 |
{{ form_widget(form.task, {'attr': {'class': 'task_field'}}) }} |
如果你想手工渲染表单字段,你可以单独访问每个字段的值,比如id,name和label,这里我们获取id
1 |
{{ form.task.vars.id }} |
需要获取表单字段名称属性你需要使用full_name值:
1 |
{{ form.task.vars.full_name }} |
Twig模版函数查考
如果你使用twig,表单渲染功能完整文档在reference manual。
更改Action和表单方法
到目前为止,form_start()助手被用于渲染表单的开始标记,而且我们的表单都会提交同一个URL的post请求。有时你想改变这些参数。你有很多方法。如果你的表单创建在控制器中,你就可以使用setAction()和setMethod():
1 2 3 4 5 6 7 |
$form = $this->createFormBuilder($task) ->setAction($this->generateUrl('target_route')) ->setMethod('GET') ->add('task', 'text') ->add('dueDate', 'date') ->add('save', 'submit') ->getForm(); |
这个例子假设您已经创建了一个名为target_route的路由,指向处理表单的控制器。
在创建表单类,您将学会如何移除formbuild中的代码到一个单独的类。当在controller中使用一个外部的form时,你能够把action和method作为form配置传入:
1 2 3 4 |
$form = $this->createForm(new TaskType(), $task, array( 'action' => $this->generateUrl('target_route'), 'method' => 'GET', )); |
最后,你可以通过form()和form_start()去覆盖模版中的action和method:
1 2 |
{# app/Resources/views/default/new.html.twig #} {{ form_start(form, {'action': path('target_route'), 'method': 'GET'}) }} |
如果这些表单方法不是GET活着POST,但是是PUT、PATCH和DELETE,那么symfony会插入一个name为_method的隐藏字段来存储方法。如果form表单提交一个普通的POST请求,symfony路由会检测_method参数并解析PUT、PATCH和DELETE请求。更多细节请阅读cookbook”How to Use HTTP Methods beyond GET and POST in Routes“章节。
创建表单类
正如你看到的,一个表单可以直接在controller类中被创建和使用。然而,一个更好的做法是在一个单独的PHP类中创建表单。它可以被重用到你应用程序的任何地方。创建一个新类来保存生成task表单的逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// src/AppBundle/Form/Type/TaskType.php namespace AppBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; class TaskType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('task') ->add('dueDate', null, array('widget' => 'single_text')) ->add('save', 'submit'); } public function getName() { return 'task'; } } |
注意getName()方法将返回一个该表单类型的唯一标识。这些标识符必须在应用程序中是唯一的。除非你想要去覆盖一个内置类型,他们应该不同于默认的symfony类型并且一些类型是第三方库安装到你应用程序中的。考虑加前缀使用app_以避免冲突。
这个新类包含了所有创建一个task表单所需要的内容,用于快速创建该表单。
1 2 3 4 5 6 7 8 9 10 11 12 |
// src/AppBundle/Controller/DefaultController.php // add this new use statement at the top of the class use AppBundle\Form\Type\TaskType; public function newAction() { $task = ...; $form = $this->createForm(new TaskType(), $task); // ... } |
这种方式可以让你的form很容易的在你的项目其他地方重用。这是创建表单最好的方式,但是最终的决定权在于你。
设置data_class
每个表单都需要知道它底层保存数据的类名称,(比如Acme\TaskBundle\Entity\Task)。通常情况下,是根据createForm方法的第二个参数来猜测的(例如$task)。以后,当你开始嵌入表单时,这个可能就不怎么充分了,所以,通常一个好的方法是通过添加下面代码到你的表单类型类来显式的指定data_class 选项。
12345678 use Symfony\Component\OptionsResolver\OptionsResolverInterface;public function setDefaultOptions(OptionsResolverInterface $resolver){$resolver->setDefaults(array('data_class' => 'AppBundle\Entity\Task',));}
当你映射表单到一个对象,所有的字段都被映射。 表单的任何字段如果在映射的对象上不存在那么就会造成抛出异常。
在这种情况下,你需要在表单中获取字段(比如,一个“你同意这些说法吗?”复选框)将不能映射到底层对象,那么你需要设置mapped为false以避免抛出异常。
123456789 use Symfony\Component\Form\FormBuilderInterface;public function buildForm(FormBuilderInterface $builder, array $options){$builder->add('task')->add('dueDate', null, array('mapped' => false))->add('save', 'submit');}另外,如果有任何的表单字段没有被包含着提交的数据中,那么这些字段需要显式的设置为null。
在controller类中我们可以访问字段数据:
1 $form->get('dueDate')->getData();此外,未映射字段的数据,也可直接修改:
1 $form->get('dueDate')->setData(new \DateTime());
定义你的表单作为服务
定义你的formType作为一个服务是很好的做法,使得它很容易在你的应用程序中使用。
服务和服务容器在后面的book中讲解。你后面读到后会对这一章更加清晰。
1 2 3 4 5 6 |
# src/AppBundle/Resources/config/services.yml services: acme_demo.form.type.task: class: AppBundle\Form\Type\TaskType tags: - { name: form.type, alias: task } |
就是这样!现在你可以在控制器中直接使用你的formType:
1 2 3 4 5 6 7 8 9 10 |
// src/AppBundle/Controller/DefaultController.php // ... public function newAction() { $task = ...; $form = $this->createForm('task', $task); // ... } |
甚至在其他的form中使用form type;
1 2 3 4 5 6 7 8 9 10 11 12 |
// src/AppBundle/Form/Type/ListType.php // ... class ListType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { // ... $builder->add('someTask', 'task'); } } |
阅读Creating your Field Type as a Service了解更多信息。
Forms和Doctrine
表单的目的是把数据从一个底层对象(例如Task)传递给一个HTML表单然后把用户提交的数据传回到原先的底层对象。因此,底层对象把数据持久化到数据库就跟表单没有任何的关系了。但是,如果你已经配置了底层类是通过Doctrine来持久化,(你已经定义了映射元数据在底层类),接下来当表单提交数据后,当表单合法后就可以持久化它了。
1 2 3 4 5 6 7 |
if ($form->isValid()) { $em = $this->getDoctrine()->getManager(); $em->persist($task); $em->flush(); return $this->redirectToRoute('task_success'); } |
如果处于某种原因,你不想访问原有的$task对象,你可以从表单中直接获取数据:
1 |
$task = $form->getData(); |
更多细节,请查看 Doctrine ORM chapter.
在这里,关键要理解当表单跟底层对象绑定后,用户提交的数据会立刻传递给底层对象。如果你想持久化这些数据,你只需要持久化对象本身即可(已经包含了提交的数据)。
嵌入式表单
通常,你可能想生成一个表单,它包含来自不同对象的字段。比如,一个注册表单可能包含属于User对象和Address对象的字段。幸运的是,这些对于form组件来说都是很容易很自然的事。
嵌入一个单独对象
假设每个Task属于一个Category对象,首先创建这个Category对象:
1 2 3 4 5 6 7 8 9 10 11 12 |
// src/AppBundle/Entity/Category.php namespace AppBundle\Entity; use Symfony\Component\Validator\Constraints as Assert; class Category { /** * @Assert\NotBlank() */ public $name; } |
接下来,添加一个新的category属性到Task类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// ... class Task { // ... /** * @Assert\Type(type="AppBundle\Entity\Category") * @Assert\Valid() */ protected $category; // ... public function getCategory() { return $this->category; } public function setCategory(Category $category = null) { $this->category = $category; } } |
有个Valid约束被添加到category属性。这个级联会验证相关实体。如果忽略此约束子实体就不能够进行验证。
现在,你的应用程序被更新,并显示一个新需求,需要创建一个表单,并可以让用户修改Category对象。
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 |
// src/AppBundle/Form/Type/CategoryType.php namespace AppBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolverInterface; class CategoryType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder->add('name'); } public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'data_class' => 'AppBundle\Entity\Category', )); } public function getName() { return 'category'; } } |
我们的最终目的是能够让用户在Task表单中修改Category对象,所以,我们需要添加一个类型为CategoryType表单类的category字段到TaskType 表单类。
1 2 3 4 5 6 7 8 |
use Symfony\Component\Form\FormBuilderInterface; public function buildForm(FormBuilderInterface $builder, array $options) { // ... $builder->add('category', new CategoryType()); } |
这时,CategoryType就可以和TaskType类一起渲染了。
和呈现原来的Task字段一样呈现Category字段:
1 2 3 4 5 6 7 8 |
{# ... #} <h3>Category</h3> <div class="category"> {{ form_row(form.category.name) }} </div> {# ... #} |
当用户提交表单时,提交的Category字段数据被用于创建一个Category实例,然后被设置到Task实例的category字段。
该Category实例可以通过Task实例来访问$task->getCategory(),同时也能被持久化到数据或者用作它用。
嵌入一个表单集合
你也可以将一个表单集合嵌入到一个表单(想象一个Category 表单和许多Product子表单)。它是通过一个字段类型集合类实现的。
想了解更多请参阅cookbook “How to Embed a Collection of Forms“和collection字段类型参考。
表单样式
表单的每一部分渲染都是可以被个性化的自定义。你可以自由的改变每一个表单行的渲染,改变渲染错误的标志,更或者是textarea标签应该怎样显示等。没有任何限制,不同的个性化设置能用到不同的区域。
Symfony使用模板渲染每一个或者部分表单,比如label标签,input标签,错误信息以及任何其它内容。
在Twig中,每个表单片段会被一个Twig block来渲染。要个性化渲染表单,你只需要重写相应的block即可。
在PHP模板中,它是通过单独的模板文件来渲染表单片段的,所以你需要通过编写新的模板来替代旧的模板即可。
在理解了它们是怎么工作的之后,让我们来个性化form_row片段并添加一个class属性到包裹每一表单行的div元素。首先创建一个新模板文件用于存放新的标志:
1 2 3 4 5 6 7 8 9 10 |
{# app/Resources/views/form/fields.html.twig #} {% block form_row %} {% spaceless %} <div class="form_row"> {{ form_label(form) }} {{ form_errors(form) }} {{ form_widget(form) }} </div> {% endspaceless %} {% endblock form_row %} |
field_row表单片段会在通过form_row函数渲染大部分的表单字段时使用。 要告诉你的表单组件使用你的新的field_row片段,需要添加下面的内容到你渲染的表单的模板顶部:
1 2 3 4 5 6 |
{# app/Resources/views/default/new.html.twig #} {% form_theme form 'form/fields.html.twig' %} {% form_theme form 'form/fields.html.twig' 'form/fields2.html.twig' %} {# ... render the form #} |
其中的form_theme 标签导入前面定义的片段。换句话说,当form_row函数在模板中被调用后,它将从你的自定义主题中使用field_row 块(替代Symfony已有的field_row 块)。
你的个性化主题不必重写所有的块。当渲染一个你没有重写过的块时,主题引擎会找全局的主题(定义在bundle级的主题)使用。
在拥有多个个性化主题的情况下,它会在使用全局主题之前查找定制列表。
要个性化你表单的任何部分,你只需要重写相关的片段即可。下一部分你将准确知道哪些块和文件可用来重写。
想了解更多信息,请参阅How to Customize Form Rendering.
表单片段命名
在symfony中,表单的每一部分都会被渲染,HTML表单元素,错误消息,显示标签等这些都是被定义在基础主题里的。它组成了一个Twig的块集合和一个PHP模板集合。
在Twig中,每个需要的块都被定义到一个单独的模板文件中(form_div_layout.html.twig),它们被保存在Twig Bridge里。在这个文件中,你可以看到渲染一个表单,需要的每一个block和默认的字段类型。
在PHP模板中,片段是单独的模板文件。 默认情况下它们位于框架bundle的Resources/views/Form 目录下。
每个片段名称都遵循相同的基本模式,用一个下划线(_)分为两部分,比如:
- field_row 用于form_row渲染大部分的字段
- textarea_widget 用于form_widget渲染一个textarea字段类型
- field_errors 用于form_errors渲染一个字段的错误信息
每个片段都命名都遵循:type_part 模式。type部分对应被渲染的字段类型(比如textarea,checkbox,date等),而part部分对应着是什么被渲染(比如label,widget,errors等)
默认情况下,有4种可能的表单part被用来渲染:
label |
(e.g. form_label ) |
渲染字段标签 |
widget |
(e.g. form_widget ) |
渲染字段的html显示 |
errors |
(e.g. form_errors ) |
渲染错误信息 |
row |
(e.g. form_row ) |
渲染字段一整行(包括label、widget、errors) |
还有其它2个part类型,分别是rows,rest,不过这两个一般不会用到。
通过知道字段类型(比如:textarea)和你想渲染那一部分(比如:widget),你可以创建一个你需要重写的片段名称(比如:textarea_widget).
模板片段继承
在某些情况下,你个性化的片段可能会丢失。比如,在Symfony提供的默认主题中没有提供textarea_errors片段。那么如何来渲染一个textarea字段的错误信息呢?
答案是通过form_errors片段。当Symfony渲染一个textarea类型的错误时,它首先查找一个textarea_errors片段,如果没有找到则会回到form_errors片段。每个field类型有一个parent type(textarea本身是text,它的父类型为form),Symfony如果没有发现本身的片段,就会转而使用父类片段。
所以,要重写textarea字段的errors,拷贝form_errors片段,重命名为textarea_errors并个性化它们。为所有字段重写默认的error渲染,则需要直接拷贝和个性化form_errors片段。
可以在form type reference里找到每个字段类型的“parent”类型。
全局表单样式
在上面的示例中,我们使用了form_theme 助手来导入自定义个的表单片段到表单。你也可以告诉Symfony在跨整个项目中导入自定义的form。
Twig
为了从所有之前创建的fileds.html.twig模板中自动包含个性化的block,修改你的应用程序配置文件:
1 2 3 4 5 |
# app/config/config.yml twig: form_themes: - 'form/fields.html.twig' # ... |
现在在fields.html.twig模板中的任何块都可以被全局使用来定义表单输出了。
自定义表单输出到一个单一的Twig文件中
在Twig中,你也可以个性化一个表单块在模板中
123456789101112131415 {% extends 'base.html.twig' %}{# import "_self" as the form theme #}{% form_theme form _self %}{# make the form fragment customization #}{% block form_row %}{# custom field row output #}{% endblock form_row %}{% block content %}{# ... #}{{ form_row(form.task) }}{% endblock %}{% form_theme form _self %}标签允许个性化表单区块直接在使用它的模版中被定制。这个方法可以在当前需要(个性化表单)的模版中快速实现表单的定制化输出。
注意,{% form_theme form _self %}的功能只有在继承其它模板时才能起作用,如果不是继承其它模板,则需要指出form_theme 到单独模板中。
php
从以前创建的Acme/TaskBundle/Resources/views/Form 目录中所有模板,自动导入个性化模板。修改你的配置文件:
1 2 3 4 5 6 7 |
# app/config/config.yml framework: templating: form: resources: - 'Form' # ... |
此时在Acme/TaskBundle/Resources/views/Form目录中的任何片段都可以全局范围内定义表单输出了。
CSRF 保护
CSRF–Cross-site request forgery,跨站伪造请求 是恶意攻击者试图让你的合法用户在不知不觉中提交他们本不想提交的数据的一种方法。幸运的是,CSRF攻击可以通过在你的表单中使用CSRF 记号来阻止。
默认情况下,Symfony自动为你嵌入一个合法的CSRF令牌。这就意味着你不需要做任何事情就可以得到CSRF保护。
CSRF保护是通过在你的表单中添加一个隐藏字段,默认的名叫_token。它包含一个值,这个值只有你和你的用户知道。这确保了是用户而不是其它实体在提交数据。Symfony自动校验该token是否存在以及其准确性。
_token 字段是一个隐藏字段并且会自动的渲染,只要你在你的模板中包含了form_rest()函数。它确保了没有被渲染过的字段全部渲染出来。
CSRF令牌可以按照表单来个性化,比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
use Symfony\Component\OptionsResolver\OptionsResolverInterface; class TaskType extends AbstractType { // ... public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'data_class' => 'AppBundle\Entity\Task', 'csrf_protection' => true, 'csrf_field_name' => '_token', // a unique key to help generate the secret token 'intention' => 'task_item', )); } // ... } |
要关闭CSRF保护,设置csrf_protection 选项为false。如果想了解更多信息,请参见form configuration reference。
intentsion选项是可选的,但为不同的表单生成不同的令牌极大的加强了安全性。
CSRF令牌对于每个用户都是不同的。如果你试图缓存页面,你就需要谨慎了,因为form使用了这种保护。想了解更多信息请查看Caching Pages that Contain CSRF Protected Forms.
使用一个无底层类的表单
大多数情况下,一个表单要绑定一个对象的,并且表单中所有的字段获取或者保存它们的数据到该对象属性。
但有时候,你可能只想使用一个没有类的表单,返回一个提交数据的数组,这个非常容易实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// make sure you've imported the Request namespace above the class use Symfony\Component\HttpFoundation\Request; // ... public function contactAction(Request $request) { $defaultData = array('message' => 'Type your message here'); $form = $this->createFormBuilder($defaultData) ->add('name', 'text') ->add('email', 'email') ->add('message', 'textarea') ->add('send', 'submit') ->getForm(); $form->handleRequest($request); if ($form->isValid()) { // data is an array with "name", "email", and "message" keys $data = $form->getData(); } // ... render the form } |
默认情况下,一个表单真的假设你想要一个数据数组而不是数据对象。
这里有两种方式你可以改变它的行为并绑定一个对象;
- 当创建表单时传入一个对象(作为createFormBuilder的第一个参数或者createForm的第二个参数)。
- 在你的表单中声明data_class 选项
如果以上两种方式都没有,那么表单会返回一个数组数据。在这个示例中因为$defaultData不是一个对象,又没有设置data_class选项,则$form->getData()最终返回一个数组。
你也可以通过Request对象直接访问POST的值
1 $request->request->get('name');注意,大多数的情况下我们使用getData()方法是更好一点的选择。因为它返回的是经过表单框架转换过的数据。
添加验证
唯一遗漏的地方就是校验规则了,通常当你调用$form->isvalid()时,对象会调用你在类中提供的验证规则进行验证。如果你的表单被映射到一个对象(例如,你正在使用data_class配置或者对象作为参数传入到你的form),这些方法使用非常的勤。请参阅 Validation。
但如果表单没有映射到一个对象,你要检索提交过来的一个简单数组,你怎么来添加对你表单数据的约束规则呢?
答案是自己创建约束,然后传入到表单。整体的描述在validation章节,但是这里有一个简单的案例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\NotBlank; $builder ->add('firstName', 'text', array( 'constraints' => new Length(array('min' => 3)), )) ->add('lastName', 'text', array( 'constraints' => array( new NotBlank(), new Length(array('min' => 3)), ), )) ; |
如果你使用验证组,你需要在创建表单时引用默认组,或者你要添加在约束中设置正确的组。
1 |
new NotBlank(array('groups' => array('create', 'update')) |
最后的思考
你现在已经了解了所有建造复杂功能性的表单所需要的所有建造块。当生成表单时,记住一个表单的首要目标是从一个对象(task)把数据传递给一个HTML表单以方便用户修改它们。第二个目标就是把用户提交的数据重写提交回对象。
form很强大还有更多需要学习,比如如何出来文件上传和doctrine还有就是如何创建一个动态的表单(例如待办事项列表,你可以添加很多字段通过javascript提交)。多看看cookbook。此外,一定要依赖field type reference documentation,因为里面包括如何使用和配置每个字段类型的例子。
一个表单搞这么复杂