> Symfony中文手册 > 如何创建一个表单类型扩展

如何创建一个表单类型扩展

表单类型扩展(Form Type Extension)不可思议地强大:它们允许你在整个系统之内去 调整 任何一个既存的表单字段类型。

有两个主要使用场景:

  1. 你想添加一个 特定功能到某个表单类型 (诸如添加一个“下载”功能到 FileType 字段类型中);

  2. 你想添加一个 通用功能到多个类型中(如,在所有“input text/文本输入”的类型中添加一个“help/帮助”信息)。

假设你有一个 Media entity,每个媒体关联一个文件(file)。这个 Media 使用的是一个file字段类型,但是在编辑这个entity时,你希望看到它的图片自动显示在文件输入框的旁边。

表单类型扩展: Form Type Extension。

表单字段类型: Form Field Type。

Tip

老版文档提示1:自定义表单字段类型并控制其输出,可以实现同样目的。但使用表单类型扩展更清爽(它限制了模板中的业务逻辑)、更灵活(可以给某个表单类型添加多个扩展)。

Tip

老版文档提示2:表单类型扩展可以实现大多数表单字段类型所能达到的事,可是,字段类型只是对自己进行自定义,而扩展则可以打入到所有已知类型之中。

Tip

老版文档提示3:无论如何你可以通过自定义“该字段如何渲染到模板”来实现(译注:即自定义表单字段类型的方式)。但是表单类型扩展允许你使用一种更加DRY(省事)的方式来完成。

定义表单类型扩展 ¶

首先,创建表单类型扩展类:

Note

根据标准,表单扩展通常应保存在你bundle的 Form\Extension 目录中.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/AppBundle/Form/Extension/ImageTypeExtension.PHP
namespace AppBundle\Form\Extension;
 
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\FileType;
 
class ImageTypeExtension extends AbstractTypeExtension
{
    /**
     * Returns the name of the type being extended.
     * 返回待扩展的类型之名称
     * @return string The name of the type being extended 
     */
    public function getExtendedType()
    {
        // use FormType::class to modify (nearly) every field in the system
        // 若使用 FormType::class 即可修改系统中(几乎)所有的字段(类型)
        return FileType::class;
    }
}

唯一的一个你必须要实现的方法是 getExtendedType。它用于配置你要修改的是 哪个 表单字段或字段类型。

Tip

getExtendedType 方法返回的值对应你想要扩展的表单类型之完整名称。

除了 getExtendedType 函数,你可能还希望覆写以下方法:

  • buildForm()
  • buildView()
  • configureOptions()
  • finishView()

关于这些方法的更多信息,参考 如何创建一个自定义的表单字段类型。

把你的表单类型注册成一个服务 ¶

下一步是让Symfony了知你的扩展。通过用 form.type_extension 标签来注册你的服务即可实现:

1
2
3
4
5
6
services:
    # ...

    AppBundle\Form\Extension\ImageTypeExtension:
        tags:
            - { name: form.type_extension, extended_type: Symfony\Component\Form\Extension\Core\Type\FileType }
1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="Http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">
 
    <services>
        <service id="AppBundle\Form\Extension\ImageTypeExtension">
            <tag name="form.type_extension" extended-type="Symfony\Component\Form\Extension\Core\Type\FileType" />
        </service>
    </services>
</container>
1
2
3
4
5
6
7
8
use AppBundle\Form\Extension\ImageTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\FileType;
 
$container->autowire(ImageTypeExtension::class)
    ->addTag('form.type_extension', array(
        'extended_type' => FileType::class
    ))
;

打标签时的 extended_type 必须要匹配你从 extended_type 方法中返回的类。—旦 你做完此事,你所覆写的任何方法(如 buildForm()),都会在给定的类型(FileType)所对应的 任何一个 字段在构建时被调用。我们可通过下例查看。

Symfony 3.3新增: Symfony 3.3之前,你需要把类型扩展的服务给定义成public。从Symfony 3.3开始,你也可以将其定义成private。

Tip

tag中有一个可选的属性叫做 priority,默认值是0,控制的是表单类型扩展被加载时的顺序(愈高的优先级,该扩展愈先被加载)。这在你需要确保某个扩展要提前加载,或是在其他扩展之后加载时,十分有用。

Symfony 3.2新增: priority 属性自Symfony 3.2起被引入。

为扩展添加业务逻辑 ¶

你的扩展的目标,就是在文件的输入框附近展示一张漂亮的图片(当底层的模型包含图片时)。为此,假设你的实现方式类似于 如何使用 Doctrine 处理文件上传 中所描述的:你有一个Media模型,内含一个path属性,用于将数图片路径保存在数据库中:

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/AppBundle/Entity/Media.php
namespace AppBundle\Entity;
 
use Symfony\Component\Validator\Constraints as Assert;
 
class Media
{
    // ...
 
    /**
     * @var string The path - typically stored in the database / 一般存于数据库中
     */
    private $path;
 
    // ...
 
    public function getWebPath()
    {
        // ... $webPath being the full image URL, to be used in templates
        // ... $webPath 应是完整的图片链接,将用于模板中
 
        return $webPath;
    }
}

为了扩展 FileType::class 表单类型,你的表单类型扩展类,需要做两件事:

  1. 覆写 configureOptions 方法,以便任何一个(使用了)FileType 的字段,都能拥有一个 image_path 选项;

  2. 重写 buildFormbuildView 方法,来将图片的URL地址传递到view(视图层)。

Tip

老版文档提示4: 此处的逻辑如下:当添加一个 FileType::class 类型的表单字段时,你是能够设置一个新的选项的:image_path。这个配置将告诉file字段如何去获取图片的真实URL并把它显示在视图层中。

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
41
42
43
44
// src/AppBundle/Form/Extension/ImageTypeExtension.php
namespace AppBundle\Form\Extension;
 
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\FileType;
 
class ImageTypeExtension extends AbstractTypeExtension
{
    public function getExtendedType()
    {
        return FileType::class;
    }
 
    public function configureOptions(OptionsResolver $resolver)
    {
        // makes it legal for FileType fields to have an image_property option
        // 应确保对于FileType字段来说,拥有一个image_property选项是合法的
        $resolver->setDefined(array('image_property'));
    }
 
    public function buildView(FormView $view, FormInterface $form, array $options)
    {
        if (isset($options['image_property'])) {
            // this will be whatever class/entity is bound to your form (e.g. Media)
            // 这即是你的表单所绑定的任何一个entity或class(此处是Media)
            $parentData = $form->getParent()->getData();
 
            $imageUrl = null;
            if (null !== $parentData) {
                $accessor = PropertyAccess::createPropertyAccessor();
                $imageUrl = $accessor->getValue($parentData, $options['image_property']);
            }
 
            // set an "image_url" variable that will be available when rendering this field
            // 设置一个能当字段被输出时,够使用在模板中的 "image_url" 变量
            $view->vars['image_url'] = $imageUrl;
        }
    }
 
}

覆写文件控件(File Widget)的模板码段 ¶

每个字段类型,都是通过一个模板码段来呈现的。这些模板码段是可以被覆写的,为的是对表单的输出进行自定义。更多信息请参考 什么是表单主题?。

在你的扩展类中,你已经添加了一个变量(image_url),但是你仍然需要在模板中来利用这个新变量。特别是,你需要重写 file_widget 区块:

1
2
3
4
5
6
7
8
9
10
11
12
{% extends 'form_div_layout.HTML.twig' %}
 
{% block file_widget %}
    {% spaceless %}
 
    {{ block('form_widget') }}
    {% if image_url is not null %}
        <img src="{{ asset(image_url) }}"/>
    {% endif %}
 
    {% endspaceless %}
{% endblock %}
1
2
3
4
5
<!-- app/Resources/file_widget.html.php -->
<?php echo $view['form']->widget($form) ?>
<?php if (null !== $image_url): ?>
    <img src="<?php echo $view['assets']->getUrl($image_url) ?>"/>
<?php endif ?>

Tip

老版文档提示5: 你需要改变配置文件,或显式地指定你想要如何给表单增加主题,这都是为了让 Symfony 使用你覆写了的模板代码区块(block)。参考 什么是表单主题? 以了解更多。

确保 配置了此一表单主题的模板 以便系统能够发现它。

使用表单类型扩展 ¶

从现在开始,当你在表单中添加一个 FileType::class 类型的字段时,你可以指定一个 image_path 选项,这个选项将用来在file字段旁边展示图片。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/AppBundle/Form/Type/MediaType.php
namespace AppBundle\Form\Type;
 
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
 
class MediaType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name', TextType::class)
            ->add('file', FileType::class, array('image_property' => 'webPath'));
    }
}

当显示表单时,如果底层模型(underlying model)已经关联到一张图片,你就会看到它显示在文件输入框的旁边。

通用型表单类型扩展 ¶

你可以一次性地修改若干表单类型,通过指定它们的通用父类型来实现(见Form Types参考)。例如,某些表单类型是从 TextType 表单类型继承过来的(如,EmailType, SearchType, UrlType 等)。一个应用到 TextType 的表单类型扩展(即,getExtendedType 方法返回的是 TextType::class)将会同时应用到所有这些(继承者们的)表单类型中去。

利用这一方式,由于 大多数 Symfony原生表单类型都是继承自 FormType 这个表单类型,所以,一个应用到 FormType 的表单类型扩展,将同时应用到所有这些(继承者们的)表单类型中去(但有个重大例外是 ButtonType 这个表单类型)。另外注意,如果你创建或使用了一个自定义的表单类型,它是可以不去继承 FormType 的,因此你的表单类型扩展也可能作用不到它的身上。

Tip

老版文档提示6: 你自定义的表单类型属于 basetypeButtonType 请参考 ButtonType Field。