表单
视频教程
你喜欢视频教程吗?查看 Symfony 表单视频教程系列。
创建和处理 HTML 表单既困难又重复。你需要处理渲染 HTML 表单字段、验证提交的数据、将表单数据映射到对象等等。Symfony 包含强大的表单功能,提供了所有这些功能以及更多针对真正复杂场景的功能。
用法
使用 Symfony 表单时,推荐的工作流程如下
- 在 Symfony 控制器中或使用专用的表单类构建表单;
- 在模板中渲染表单,以便用户可以编辑和提交它;
- 处理表单以验证提交的数据,将其转换为 PHP 数据并对其执行某些操作(例如,将其持久化到数据库中)。
以下部分将详细解释这些步骤中的每一个。为了使示例更易于理解,所有示例都假设你正在构建一个显示“任务”的小型 Todo 列表应用程序。
用户使用 Symfony 表单创建和编辑任务。每个任务都是以下 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 29
// src/Entity/Task.php
namespace App\Entity;
class Task
{
protected string $task;
protected ?\DateTimeInterface $dueDate;
public function getTask(): string
{
return $this->task;
}
public function setTask(string $task): void
{
$this->task = $task;
}
public function getDueDate(): ?\DateTimeInterface
{
return $this->dueDate;
}
public function setDueDate(?\DateTimeInterface $dueDate): void
{
$this->dueDate = $dueDate;
}
}
这个类是一个“普通的 PHP 对象”,因为到目前为止,它与 Symfony 或任何其他库无关。它是一个普通的 PHP 对象,直接解决你的应用程序内部的问题(即在你的应用程序中表示任务的需求)。但是你也可以以相同的方式编辑 Doctrine 实体。
表单类型
在创建你的第一个 Symfony 表单之前,重要的是要理解“表单类型”的概念。在其他项目中,通常区分“表单”和“表单字段”。在 Symfony 中,它们都是“表单类型”
- 单个
<input type="text">
表单字段是一个“表单类型”(例如TextType
); - 用于输入邮政地址的几个 HTML 字段的组是一个“表单类型”(例如
PostalAddressType
); - 包含多个字段以编辑用户配置文件的整个
<form>
是一个“表单类型”(例如UserProfileType
)。
起初这可能会令人困惑,但很快你就会觉得很自然。此外,它简化了代码,并使“组合”和“嵌入”表单字段更容易实现。
Symfony 提供了数十种表单类型,你也可以创建自己的表单类型。
提示
你可以使用 debug:form
列出你的应用程序中所有可用的类型、类型扩展和类型猜测器
1 2 3 4 5 6 7 8
$ php bin/console debug:form
# pass the form type FQCN to only show the options for that type, its parents and extensions.
# For built-in types, you can pass the short classname instead of the FQCN
$ php bin/console debug:form BirthdayType
# pass also an option name to only display the full definition of that option
$ php bin/console debug:form BirthdayType label_attr
构建表单
Symfony 提供了一个“表单构建器”对象,它允许你使用流畅的接口描述表单字段。稍后,此构建器将创建用于渲染和处理内容的实际表单对象。
在控制器中创建表单
如果你的控制器继承自 AbstractController,请使用 createFormBuilder()
助手函数
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
// src/Controller/TaskController.php
namespace App\Controller;
use App\Entity\Task;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class TaskController extends AbstractController
{
public function new(Request $request): Response
{
// creates a task object and initializes some data for this example
$task = new Task();
$task->setTask('Write a blog post');
$task->setDueDate(new \DateTimeImmutable('tomorrow'));
$form = $this->createFormBuilder($task)
->add('task', TextType::class)
->add('dueDate', DateType::class)
->add('save', SubmitType::class, ['label' => 'Create Task'])
->getForm();
// ...
}
}
如果你的控制器没有继承自 AbstractController
,你需要在你的控制器中获取服务,并使用 form.factory
服务的 createBuilder()
方法。
在这个例子中,你向表单添加了两个字段 - task
和 dueDate
- 对应于 Task
类的 task 和 dueDate 属性。你还为每个字段分配了一个表单类型(例如 TextType
和 DateType
),由其完全限定的类名表示。最后,你添加了一个带有自定义标签的提交按钮,用于将表单提交到服务器。
创建表单类
Symfony 建议在控制器中尽可能少地放置逻辑。这就是为什么最好将复杂的表单移动到专用类中,而不是在控制器操作中定义它们。此外,在类中定义的表单可以在多个操作和服务中重用。
表单类是实现 FormTypeInterface 的表单类型。但是,最好从 AbstractType 扩展,它已经实现了该接口并提供了一些实用程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
// src/Form/Type/TaskType.php
namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('task', TextType::class)
->add('dueDate', DateType::class)
->add('save', SubmitType::class)
;
}
}
提示
在你的项目中安装 MakerBundle,以使用 make:form
和 make:registration-form
命令生成表单类。
表单类包含创建任务表单所需的所有指令。在继承自 AbstractController 的控制器中,使用 createForm()
助手函数(否则,使用 form.factory
服务的 create()
方法)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
// src/Controller/TaskController.php
namespace App\Controller;
use App\Form\Type\TaskType;
// ...
class TaskController extends AbstractController
{
public function new(): Response
{
// creates a task object and initializes some data for this example
$task = new Task();
$task->setTask('Write a blog post');
$task->setDueDate(new \DateTimeImmutable('tomorrow'));
$form = $this->createForm(TaskType::class, $task);
// ...
}
}
每个表单都需要知道保存底层数据的类的名称(例如 App\Entity\Task
)。通常,这只是根据传递给 createForm()
的第二个参数的对象(即 $task
)进行猜测的。稍后,当你开始嵌入表单时,这将不再足够。
因此,虽然并非总是必要,但通常最好通过将以下内容添加到你的表单类型类中来显式指定 data_class
选项
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// src/Form/Type/TaskType.php
namespace App\Form\Type;
use App\Entity\Task;
use Symfony\Component\OptionsResolver\OptionsResolver;
// ...
class TaskType extends AbstractType
{
// ...
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Task::class,
]);
}
}
渲染表单
现在表单已经创建,下一步是渲染它
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
// src/Controller/TaskController.php
namespace App\Controller;
use App\Entity\Task;
use App\Form\Type\TaskType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class TaskController extends AbstractController
{
public function new(Request $request): Response
{
$task = new Task();
// ...
$form = $this->createForm(TaskType::class, $task);
return $this->render('task/new.html.twig', [
'form' => $form,
]);
}
}
在内部,render()
方法调用 $form->createView()
将表单转换为表单视图实例。
然后,使用一些表单助手函数来渲染表单内容
1 2
{# templates/task/new.html.twig #}
{{ form(form) }}
就是这样!form() 函数渲染所有字段以及 <form>
开始和结束标签。默认情况下,表单方法是 POST
,目标 URL 是显示表单的相同 URL,但是你可以更改两者。
请注意,渲染的 task
输入字段具有来自 $task
对象的 task
属性的值(即“Write a blog post”)。这是表单的首要任务:从对象中获取数据并将其转换为适合在 HTML 表单中渲染的格式。
提示
表单系统足够智能,可以通过 Task
类上的 getTask()
和 setTask()
方法访问受保护的 task
属性的值。除非属性是公共的,否则它必须具有“getter”和“setter”方法,以便 Symfony 可以获取和放置数据到属性上。对于布尔属性,你可以使用“isser”或“hasser”方法(例如 isPublished()
或 hasReminder()
)而不是 getter(例如 getPublished()
或 getReminder()
)。
尽管这种渲染很简单,但它不是很灵活。通常,你需要更多地控制整个表单或某些字段的外观。例如,由于 Symfony 表单与 Bootstrap 5 集成,你可以设置此选项以生成与 Bootstrap 5 CSS 框架兼容的表单
1 2 3
# config/packages/twig.yaml
twig:
form_themes: ['bootstrap_5_layout.html.twig']
内置的 Symfony 表单主题包括 Bootstrap 3、4 和 5、Foundation 5 和 6,以及 Tailwind 2。你也可以创建自己的 Symfony 表单主题。
除了表单主题之外,Symfony 还允许你自定义字段的渲染方式,使用多个函数分别渲染每个字段部分(widgets、labels、errors、help messages 等)
处理表单
处理表单的推荐方法是使用单个操作来渲染表单和处理表单提交。你可以使用单独的操作,但是使用一个操作可以简化一切,同时保持代码简洁和可维护。
处理表单意味着将用户提交的数据转换回对象的属性。为了实现这一点,必须将用户提交的数据写入表单对象
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
// src/Controller/TaskController.php
// ...
use Symfony\Component\HttpFoundation\Request;
class TaskController extends AbstractController
{
public function new(Request $request): Response
{
// just set up a fresh $task object (remove the example data)
$task = new Task();
$form = $this->createForm(TaskType::class, $task);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// $form->getData() holds the submitted values
// but, the original `$task` variable has also been updated
$task = $form->getData();
// ... perform some action, such as saving the task to the database
return $this->redirectToRoute('task_success');
}
return $this->render('task/new.html.twig', [
'form' => $form,
]);
}
}
此控制器遵循处理表单的常见模式,并有三个可能的路径
- 当最初在浏览器中加载页面时,表单尚未提交,
$form->isSubmitted()
返回false
。因此,表单被创建和渲染; 当用户提交表单时,handleRequest() 会识别到这一点,并立即将提交的数据写回到
$task
对象的task
和dueDate
属性中。然后验证此对象(验证将在下一节中解释)。如果它无效,isValid() 返回false
,并且表单再次渲染,但现在带有验证错误。通过将
$form
传递给render()
方法(而不是$form->createView()
),响应代码会自动设置为 HTTP 422 Unprocessable Content。这确保了与依赖 HTTP 规范的工具(如 Symfony UX Turbo)的兼容性;- 当用户提交带有有效数据的表单时,提交的数据再次写入表单,但这次 isValid() 返回
true
。现在你有机会在使用$task
对象执行一些操作(例如,将其持久化到数据库)之后,将用户重定向到其他页面(例如,“谢谢”或“成功”页面);
注意
在成功提交表单后重定向用户是一种最佳实践,它可以防止用户点击浏览器上的“刷新”按钮并重新提交数据。
另请参阅
如果你需要更多地控制表单提交的确切时间或传递给表单的数据,你可以使用 submit() 方法来处理表单提交。
验证表单
在上一节中,你学习了如何提交带有有效或无效数据的表单。在 Symfony 中,问题不是“表单”是否有效,而是底层对象(本例中的 $task
)在表单应用提交的数据后是否有效。调用 $form->isValid()
是一个快捷方式,用于询问 $task
对象是否具有有效数据。
在使用验证之前,在你的应用程序中添加对它的支持
1
$ composer require symfony/validator
验证是通过向类添加一组规则(称为(验证)约束)来完成的。你可以将它们添加到实体类,也可以使用表单类型的constraints 选项。
为了查看第一种方法(将约束添加到实体)的实际效果,请添加验证约束,以便 task
字段不能为空,dueDate
字段不能为空,并且必须是有效的 DateTimeImmutable
对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
// src/Entity/Task.php
namespace App\Entity;
use Symfony\Component\Validator\Constraints as Assert;
class Task
{
#[Assert\NotBlank]
public string $task;
#[Assert\NotBlank]
#[Assert\Type(\DateTimeInterface::class)]
protected \DateTimeInterface $dueDate;
}
就是这样!如果你使用无效数据重新提交表单,你将看到与表单一起打印出的相应错误。
要查看第二种方法(将约束添加到表单),请参阅本节。两种方法可以一起使用。
其他常用表单功能
传递选项到表单
如果你在类中创建表单,当在控制器中构建表单时,你可以将自定义选项作为 createForm()
的第三个可选参数传递给它
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// src/Controller/TaskController.php
namespace App\Controller;
use App\Form\Type\TaskType;
// ...
class TaskController extends AbstractController
{
public function new(): Response
{
$task = new Task();
// use some PHP logic to decide if this form field is required or not
$dueDateIsRequired = ...;
$form = $this->createForm(TaskType::class, $task, [
'require_due_date' => $dueDateIsRequired,
]);
// ...
}
}
如果您现在尝试使用表单,您会看到一个错误消息:选项 "require_due_date" 不存在。 这是因为表单必须使用 configureOptions()
方法声明它们接受的所有选项
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
// src/Form/Type/TaskType.php
namespace App\Form\Type;
use Symfony\Component\OptionsResolver\OptionsResolver;
// ...
class TaskType extends AbstractType
{
// ...
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
// ...,
'require_due_date' => false,
]);
// you can also define the allowed types, allowed values and
// any other feature supported by the OptionsResolver component
$resolver->setAllowedTypes('require_due_date', 'bool');
}
}
现在您可以在 buildForm()
方法内部使用这个新的表单选项
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// src/Form/Type/TaskType.php
namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\FormBuilderInterface;
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
// ...
->add('dueDate', DateType::class, [
'required' => $options['require_due_date'],
])
;
}
// ...
}
表单类型选项
每个 表单类型 都有许多选项来配置它,如 Symfony 表单类型参考 中所述。 两个常用的选项是 required
和 label
。
required
选项
最常用的选项是 required
选项,它可以应用于任何字段。 默认情况下,此选项设置为 true
,这意味着支持 HTML5 的浏览器将要求您在提交表单之前填写所有字段。
如果您不想要这种行为,可以禁用整个表单的客户端验证,或者将一个或多个字段的 required
选项设置为 false
1 2 3
->add('dueDate', DateType::class, [
'required' => false,
])
required
选项不执行任何服务器端验证。 如果用户为字段提交一个空白值(例如,使用旧浏览器或 Web 服务),则它将被接受为有效值,除非您还使用 Symfony 的 NotBlank
或 NotNull
验证约束。
label
选项
默认情况下,表单字段的标签是属性名称的人性化版本(user
-> User
;postalAddress
-> Postal Address
)。 在字段上设置 label
选项以显式定义它们的标签
1 2 3 4
->add('dueDate', DateType::class, [
// set it to FALSE to not display the label for this field
'label' => 'To Be Completed Before',
])
提示
默认情况下,必填字段的 <label>
标签使用 required
CSS 类呈现,因此您可以通过应用 CSS 样式来显示星号
1 2 3
label.required:before {
content: "*";
}
更改 Action 和 HTTP 方法
默认情况下,<form>
标签使用 method="post"
属性呈现,并且没有 action
属性。 这意味着表单通过 HTTP POST 请求提交到呈现它的同一 URL 下。 构建表单时,使用 setAction()
和 setMethod()
方法来更改此设置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
// src/Controller/TaskController.php
namespace App\Controller;
// ...
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
class TaskController extends AbstractController
{
public function new(): Response
{
// ...
$form = $this->createFormBuilder($task)
->setAction($this->generateUrl('target_route'))
->setMethod('GET')
// ...
->getForm();
// ...
}
}
在类中构建表单时,将 action 和 method 作为表单选项传递
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// src/Controller/TaskController.php
namespace App\Controller;
use App\Form\TaskType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
// ...
class TaskController extends AbstractController
{
public function new(): Response
{
// ...
$form = $this->createForm(TaskType::class, $task, [
'action' => $this->generateUrl('target_route'),
'method' => 'GET',
]);
// ...
}
}
最后,您可以通过将 action 和 method 传递给 form()
或 form_start()
辅助函数来覆盖模板中的 action 和 method
1 2
{# templates/task/new.html.twig #}
{{ form_start(form, {'action': path('target_route'), 'method': 'GET'}) }}
注意
如果表单的 method 不是 GET
或 POST
,而是 PUT
、PATCH
或 DELETE
,Symfony 将插入一个名为 _method
的隐藏字段来存储此 method。 表单将以正常的 POST
请求提交,但 Symfony 的路由 能够检测到 _method
参数,并将其解释为 PUT
、PATCH
或 DELETE
请求。 必须启用 http_method_override 选项才能使其工作。
更改表单名称
如果您检查呈现的表单的 HTML 内容,您会看到 <form>
名称和字段名称是从类型类名称生成的(例如 <form name="task" ...>
和 <select name="task[dueDate][date][month]" ...>
)。
如果您想修改此设置,请使用 createNamed() 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// src/Controller/TaskController.php
namespace App\Controller;
use App\Form\TaskType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormFactoryInterface;
// ...
class TaskController extends AbstractController
{
public function new(FormFactoryInterface $formFactory): Response
{
$task = ...;
$form = $formFactory->createNamed('my_name', TaskType::class, $task);
// ...
}
}
您甚至可以通过将其设置为空字符串来完全禁止名称。
客户端 HTML 验证
由于 HTML5,许多浏览器可以在客户端本地强制执行某些验证约束。 最常见的验证是通过在必填字段上添加 required
属性来激活的。 对于支持 HTML5 的浏览器,如果用户尝试提交该字段为空的表单,这将导致显示本地浏览器消息。
生成的表单通过添加触发验证的合理 HTML 属性来充分利用这项新功能。 但是,可以通过将 novalidate
属性添加到 <form>
标签或将 formnovalidate
添加到 submit 标签来禁用客户端验证。 当您想测试服务器端验证约束,但被浏览器阻止(例如,提交空白字段)时,这尤其有用。
1 2 3 4
{# templates/task/new.html.twig #}
{{ form_start(form, {'attr': {'novalidate': 'novalidate'}}) }}
{{ form_widget(form) }}
{{ form_end(form) }}
表单类型猜测
如果表单处理的对象包含验证约束,Symfony 可以内省该元数据以猜测字段的类型。 在上面的示例中,Symfony 可以从验证规则中猜测 task
字段是一个普通的 TextType
字段,而 dueDate
字段是一个 DateType
字段。
要启用 Symfony 的“猜测机制”,请省略 add()
方法的第二个参数,或者将 null
传递给它
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
// src/Form/Type/TaskType.php
namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
// if you don't define field options, you can omit the second argument
->add('task')
// if you define field options, pass NULL as second argument
->add('dueDate', null, ['required' => false])
->add('save', SubmitType::class)
;
}
}
警告
当使用特定的 表单验证组 时,字段类型猜测器仍然会考虑所有验证约束来猜测您的字段类型(包括不属于正在使用的验证组的约束)。
表单类型选项猜测
当为某些字段启用猜测机制时,除了其表单类型外,以下选项也将被猜测
required
required
选项是根据验证规则(即字段是否为NotBlank
或NotNull
)或 Doctrine 元数据(即字段是否为nullable
)猜测的。 这非常有用,因为您的客户端验证将自动匹配您的验证规则。maxlength
- 如果字段是某种文本字段,则
maxlength
选项属性是从验证约束(如果使用Length
或Range
)或从 Doctrine 元数据(通过字段的长度)猜测的。
如果您想更改其中一个猜测值,请在 options 字段数组中覆盖它
1
->add('task', null, ['attr' => ['maxlength' => 4]])
另请参阅
除了猜测表单类型之外,如果您正在使用 Doctrine 实体,Symfony 还会猜测 验证约束。 阅读 数据库和 Doctrine ORM 指南以获取更多信息。
未映射字段
当通过表单编辑对象时,所有表单字段都被视为对象的属性。 表单上任何对象上不存在的字段都将导致抛出异常。
如果您需要在表单中添加不会存储在对象中的额外字段(例如,添加一个“我同意这些条款”复选框),请在这些字段中将 mapped
选项设置为 false
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// ...
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
class TaskType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('task')
->add('dueDate')
->add('agreeTerms', CheckboxType::class, ['mapped' => false])
->add('save', SubmitType::class)
;
}
}
这些“未映射字段”可以在控制器中使用以下方式设置和访问
1 2
$form->get('agreeTerms')->getData();
$form->get('agreeTerms')->setData(true);
此外,如果表单上有任何字段未包含在提交的数据中,则这些字段将被显式设置为 null
。
了解更多
构建表单时,请记住表单的首要目标是将数据从对象 (Task
) 转换为 HTML 表单,以便用户可以修改该数据。 表单的第二个目标是获取用户提交的数据并将其重新应用于对象。
Symfony 表单还有很多东西要学习,还有很多强大的技巧
参考
高级功能
表单主题和自定义
事件
验证
杂项