跳到内容

表单

编辑此页

视频教程

你喜欢视频教程吗?查看 Symfony 表单视频教程系列

创建和处理 HTML 表单既困难又重复。你需要处理渲染 HTML 表单字段、验证提交的数据、将表单数据映射到对象等等。Symfony 包含强大的表单功能,提供了所有这些功能以及更多针对真正复杂场景的功能。

安装

在使用 Symfony Flex 的应用中,运行此命令以安装表单功能,然后再使用它

1
$ composer require symfony/form

用法

使用 Symfony 表单时,推荐的工作流程如下

  1. 在 Symfony 控制器中或使用专用的表单类构建表单;
  2. 在模板中渲染表单,以便用户可以编辑和提交它;
  3. 处理表单以验证提交的数据,将其转换为 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() 方法。

在这个例子中,你向表单添加了两个字段 - taskdueDate - 对应于 Task 类的 task 和 dueDate 属性。你还为每个字段分配了一个表单类型(例如 TextTypeDateType),由其完全限定的类名表示。最后,你添加了一个带有自定义标签的提交按钮,用于将表单提交到服务器。

创建表单类

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:formmake: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,
        ]);
    }
}

此控制器遵循处理表单的常见模式,并有三个可能的路径

  1. 当最初在浏览器中加载页面时,表单尚未提交,$form->isSubmitted() 返回 false。因此,表单被创建和渲染;
  2. 当用户提交表单时,handleRequest() 会识别到这一点,并立即将提交的数据写回到 $task 对象的 taskdueDate 属性中。然后验证此对象(验证将在下一节中解释)。如果它无效,isValid() 返回 false,并且表单再次渲染,但现在带有验证错误。

    通过将 $form 传递给 render() 方法(而不是 $form->createView()),响应代码会自动设置为 HTTP 422 Unprocessable Content。这确保了与依赖 HTTP 规范的工具(如 Symfony UX Turbo)的兼容性;

  3. 当用户提交带有有效数据的表单时,提交的数据再次写入表单,但这次 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 表单类型参考 中所述。 两个常用的选项是 requiredlabel

required 选项

最常用的选项是 required 选项,它可以应用于任何字段。 默认情况下,此选项设置为 true,这意味着支持 HTML5 的浏览器将要求您在提交表单之前填写所有字段。

如果您不想要这种行为,可以禁用整个表单的客户端验证,或者将一个或多个字段的 required 选项设置为 false

1
2
3
->add('dueDate', DateType::class, [
    'required' => false,
])

required 选项不执行任何服务器端验证。 如果用户为字段提交一个空白值(例如,使用旧浏览器或 Web 服务),则它将被接受为有效值,除非您还使用 Symfony 的 NotBlankNotNull 验证约束。

label 选项

默认情况下,表单字段的标签是属性名称的人性化版本(user -> UserpostalAddress -> 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 不是 GETPOST,而是 PUTPATCHDELETE,Symfony 将插入一个名为 _method 的隐藏字段来存储此 method。 表单将以正常的 POST 请求提交,但 Symfony 的路由 能够检测到 _method 参数,并将其解释为 PUTPATCHDELETE 请求。 必须启用 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 选项是根据验证规则(即字段是否为 NotBlankNotNull)或 Doctrine 元数据(即字段是否为 nullable)猜测的。 这非常有用,因为您的客户端验证将自动匹配您的验证规则。
maxlength
如果字段是某种文本字段,则 maxlength 选项属性是从验证约束(如果使用 LengthRange)或从 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

本作品,包括代码示例,根据 Creative Commons BY-SA 3.0 许可协议获得许可。
目录
    版本