跳到內容

如何建立自訂表單欄位類型

編輯此頁面

Symfony 提供了數十種表單類型(在其他專案中稱為「表單欄位」),可在您的應用程式中直接使用。然而,建立自訂表單類型以解決專案中的特定目的也很常見。

基於 Symfony 內建類型建立表單類型

建立表單類型最簡單的方法是基於現有的表單類型之一。假設您的專案將「運送選項」列表顯示為 <select> HTML 元素。這可以使用 ChoiceType 來實作,其中 choices 選項設定為可用的運送選項列表。

但是,如果您在多個表單中使用相同的表單類型,則每次使用時都重複 choices 列表很快就會變得乏味。在本範例中,更好的解決方案是建立基於 ChoiceType 的自訂表單類型。自訂類型看起來和行為都像 ChoiceType,但選項列表已預先填入運送選項,因此您無需自行定義它們。

表單類型是實作 FormTypeInterface 的 PHP 類別,但您應該改為繼承 AbstractType,它已經實作了該介面並提供了一些實用工具。依照慣例,它們儲存在 src/Form/Type/ 目錄中

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/Form/Type/ShippingType.php
namespace App\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\OptionsResolver\OptionsResolver;

class ShippingType extends AbstractType
{
    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'choices' => [
                'Standard Shipping' => 'standard',
                'Expedited Shipping' => 'expedited',
                'Priority Shipping' => 'priority',
            ],
        ]);
    }

    public function getParent(): string
    {
        return ChoiceType::class;
    }
}

getParent() 告訴 Symfony 以 ChoiceType 作為起點,然後 configureOptions() 覆寫其某些選項。(本文稍後將詳細說明 FormTypeInterface 的所有方法。)產生的表單類型是具有預定義選項的選擇欄位。

現在,您可以在建立 Symfony 表單時新增此表單類型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/Form/Type/OrderType.php
namespace App\Form\Type;

use App\Form\Type\ShippingType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;

class OrderType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            // ...
            ->add('shipping', ShippingType::class)
        ;
    }

    // ...
}

就這樣。 shipping 表單欄位將在任何範本中正確呈現,因為它重複使用了其父類型 ChoiceType 定義的範本邏輯。如果您願意,您也可以為您的自訂類型定義範本,如本文稍後所述。

從頭開始建立表單類型

有些表單類型對於您的專案來說非常特殊,以至於它們無法基於任何現有的表單類型,因為它們差異太大。考慮一個應用程式,該應用程式希望在不同的表單中重複使用以下欄位集作為「郵寄地址」

如上所述,表單類型是實作 FormTypeInterface 的 PHP 類別,儘管從 AbstractType 繼承會更方便

1
2
3
4
5
6
7
8
9
10
11
// src/Form/Type/PostalAddressType.php
namespace App\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\OptionsResolver\OptionsResolver;

class PostalAddressType extends AbstractType
{
    // ...
}

以下是表單類型類別可以定義的最重要方法

getParent()

如果您的自訂類型基於另一種類型(即它們共用某些功能),請新增此方法以傳回原始類型的完整類別名稱。請勿對此使用 PHP 繼承。Symfony 將在呼叫自訂類型中定義的方法之前,先呼叫父項的所有表單類型方法(buildForm()buildView() 等)和類型擴充功能。

否則,如果您的自訂類型是從頭開始建立的,則可以省略 getParent()

預設情況下,AbstractType 類別會傳回通用的 FormType 類型,它是 Form 元件中所有表單類型的根父項。

configureOptions()
它定義了使用表單類型時可設定的選項,這些選項也是可以在以下方法中使用的選項。選項繼承自父類型和父類型擴充功能,但您可以建立所需的任何自訂選項。
buildForm()
它設定目前的表單,並且可以新增巢狀欄位。它與在類別中建立 Symfony 表單時使用的方法相同。
buildView()
它設定在表單主題範本中呈現欄位時需要的任何額外變數。
finishView()
buildView() 相同。僅當您的表單類型包含許多欄位時(即由許多單選按鈕或核取方塊組成的 ChoiceType)才有用,因為此方法將允許使用 $view['child_name'] 存取子視圖。對於任何其他用例,建議改用 buildView()

定義表單類型

首先新增 buildForm() 方法,以設定郵寄地址中包含的所有類型。目前,所有欄位的類型都是 TextType

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/Form/Type/PostalAddressType.php
namespace App\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;

class PostalAddressType extends AbstractType
{
    // ...

    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('addressLine1', TextType::class, [
                'help' => 'Street address, P.O. box, company name',
            ])
            ->add('addressLine2', TextType::class, [
                'help' => 'Apartment, suite, unit, building, floor',
            ])
            ->add('city', TextType::class)
            ->add('state', TextType::class, [
                'label' => 'State',
            ])
            ->add('zipCode', TextType::class, [
                'label' => 'ZIP Code',
            ])
        ;
    }
}

提示

執行以下命令以驗證表單類型是否已成功在應用程式中註冊

1
$ php bin/console debug:form

此表單類型已準備好在其他表單內使用,並且其所有欄位都將在任何範本中正確呈現

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// src/Form/Type/OrderType.php
namespace App\Form\Type;

use App\Form\Type\PostalAddressType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;

class OrderType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            // ...
            ->add('address', PostalAddressType::class)
        ;
    }

    // ...
}

但是,自訂表單類型的真正威力是透過自訂表單選項(使其更具彈性)和自訂範本(使其外觀更好)來實現的。

為表單類型新增設定選項

假設您的專案需要以兩種方式設定 PostalAddressType

  • 除了「地址行 1」和「地址行 2」之外,某些地址應允許顯示「地址行 3」以儲存擴充的地址資訊;
  • 某些地址不應顯示自由文字輸入,而應能夠將可能的州/省份限制為給定的列表。

這可以透過「表單類型選項」來解決,這些選項允許設定表單類型的行為。選項在 configureOptions() 方法中定義,您可以使用所有OptionsResolver 元件功能來定義、驗證和處理其值

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
// src/Form/Type/PostalAddressType.php
namespace App\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;

class PostalAddressType extends AbstractType
{
    // ...

    public function configureOptions(OptionsResolver $resolver): void
    {
        // this defines the available options and their default values when
        // they are not configured explicitly when using the form type
        $resolver->setDefaults([
            'allowed_states' => null,
            'is_extended_address' => false,
        ]);

        // optionally you can also restrict the options type or types (to get
        // automatic type validation and useful error messages for end users)
        $resolver->setAllowedTypes('allowed_states', ['null', 'string', 'array']);
        $resolver->setAllowedTypes('is_extended_address', 'bool');

        // optionally you can transform the given values for the options to
        // simplify the further processing of those options
        $resolver->setNormalizer('allowed_states', static function (Options $options, $states): ?array
        {
            if (null === $states) {
                return $states;
            }

            if (is_string($states)) {
                $states = (array) $states;
            }

            return array_combine(array_values($states), array_values($states));
        });
    }
}

現在,您可以在使用表單類型時設定這些選項

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/OrderType.php
namespace App\Form\Type;

// ...

class OrderType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            // ...
            ->add('address', PostalAddressType::class, [
                'is_extended_address' => true,
                'allowed_states' => ['CA', 'FL', 'TX'],
                // in this example, this config would also be valid:
                // 'allowed_states' => 'CA',
            ])
        ;
    }

    // ...
}

最後一步是在建立表單時使用這些選項

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/Form/Type/PostalAddressType.php
namespace App\Form\Type;

// ...

class PostalAddressType extends AbstractType
{
    // ...

    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        // ...

        if (true === $options['is_extended_address']) {
            $builder->add('addressLine3', TextType::class, [
                'help' => 'Extended address info',
            ]);
        }

        if (null !== $options['allowed_states']) {
            $builder->add('state', ChoiceType::class, [
                'choices' => $options['allowed_states'],
            ]);
        } else {
            $builder->add('state', TextType::class, [
                'label' => 'State/Province/Region',
            ]);
        }
    }
}

建立表單類型範本

預設情況下,自訂表單類型將使用應用程式中設定的表單主題來呈現。但是,對於某些類型,您可能更喜歡建立自訂範本,以便自訂其外觀或 HTML 結構。

首先,在應用程式中的任何位置建立新的 Twig 範本,以儲存用於呈現類型的片段

1
2
3
{# templates/form/custom_types.html.twig #}

{# ... here you will add the Twig code ... #}

然後,更新form_themes 選項以在此列表的末尾新增此新範本(每個主題都會覆寫所有先前的範本)

1
2
3
4
5
# config/packages/twig.yaml
twig:
    form_themes:
        - '...'
        - 'form/custom_types.html.twig'

最後一步是建立將呈現類型的實際 Twig 範本。範本內容取決於應用程式中使用的 HTML、CSS 和 JavaScript 框架和程式庫

1
2
3
4
5
6
7
8
9
10
11
{# templates/form/custom_types.html.twig #}
{% block postal_address_row %}
    {% for child in form.children|filter(child => not child.rendered) %}
        <div class="form-group">
            {{ form_label(child) }}
            {{ form_widget(child) }}
            {{ form_help(child) }}
            {{ form_errors(child) }}
        </div>
    {% endfor %}
{% endblock %}

Twig 區塊名稱的第一部分(例如 postal_address)來自類別名稱(PostalAddressType -> postal_address)。這可以透過覆寫 PostalAddressType 中的 getBlockPrefix() 方法來控制。Twig 區塊名稱的第二部分(例如 _row)定義了正在呈現的表單類型部分(row、widget、help、errors 等)

關於表單主題的文章詳細說明了表單片段命名規則。以下是郵寄地址類型的一些 Twig 區塊名稱範例

postal_address_row
完整的表單類型區塊。
postal_address_addressLine1_help
第一個地址行下方的說明訊息區塊。
postal_address_state_widget
州/省份欄位的文字輸入小工具。
postal_address_zipCode_label
郵遞區號欄位的標籤區塊。

警告

當您的表單類別名稱與任何內建欄位類型相符時,您的表單可能無法正確呈現。名為 App\Form\PasswordType 的表單類型將與內建 PasswordType 具有相同的區塊名稱,並且無法正確呈現。覆寫 getBlockPrefix() 方法以傳回唯一的區塊前置詞(例如 app_password),以避免衝突。

將變數傳遞到表單類型範本

Symfony 將一系列變數傳遞到用於呈現表單類型的範本。您也可以傳遞自己的變數,這些變數可以基於表單定義的選項,也可以完全獨立

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/Form/Type/PostalAddressType.php
namespace App\Form\Type;

use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
// ...

class PostalAddressType extends AbstractType
{
    public function __construct(
        private EntityManagerInterface $entityManager,
    ) {
    }

    // ...

    public function buildView(FormView $view, FormInterface $form, array $options): void
    {
        // pass the form type option directly to the template
        $view->vars['isExtendedAddress'] = $options['is_extended_address'];

        // make a database query to find possible notifications related to postal addresses (e.g. to
        // display dynamic messages such as 'Delivery to XX and YY states will be added next week!')
        $view->vars['notification'] = $this->entityManager->find('...');
    }
}

如果您使用的是預設 services.yaml 設定,則此範例將已可運作!否則,請為此表單類別建立服務,並使用 form.type 標記它

buildView() 中新增的變數在表單類型範本中可用,就像任何其他常規 Twig 變數一樣

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{# templates/form/custom_types.html.twig #}
{% block postal_address_row %}
    {# ... #}

    {% if isExtendedAddress %}
        {# ... #}
    {% endif %}

    {% if notification is not empty %}
        <div class="alert alert-primary" role="alert">
            {{ notification }}
        </div>
    {% endif %}
{% endblock %}
這項作品,包括程式碼範例,已根據 Creative Commons BY-SA 3.0 授權條款授權。
目錄
    版本