如何建立自訂表單欄位類型
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 %}