この記事について
EC‑CUBE のカスタマイズ案件で Symfony の FormType を触っていると、毎回「あれ、オプション名なんだっけ?」とド忘れして公式ドキュメントを往復する羽目になります
ここでは 未来の自分が迷子にならないように、ポイントだけを抜き出してメモします。同じ悩みを抱える方の時短にもなれば幸いです。
困った時の参照用 を更新していけたらなと思います。
1. FormType クラスとは
Symfony の Form コンポーネント を司る 中心的クラス
AbstractType
を継承して作成し、下記を一括で担う
役割 | 概要 | 具体例 |
---|---|---|
フィールドツリーの構築 |
buildForm() 内で add() や create() を呼び、Input・Select など HTML 要素を ツリー状 に組み立てる |
TextType , ChoiceType , CollectionType … |
ビューの生成・調整 |
buildView() / finishView() でTwig に渡す変数や属性を上書きし、 テーマにフックさせる |
すべてのフィールドに data-hint を付与 など |
データ変換 & バリデーション設定 |
configureOptions() にdata_class , constraints , validation_groups 等を宣言し、Model ↔︎ View ↔︎ HTML の相互変換を制御 |
CallbackTransformer , NotBlank , Length
|
1‑1. どう呼び出す?
Controller からはたった 1 行:
$form = $this->formFactory->createBuilder(ArticleType::class, $article, []);
- 第 1 引数: 作成した FormType クラス名
-
第 2 引数: 初期データとして束ねるエンティティや DTO。
null
でも可 -
第 3 引数: (任意)フォーム全体に渡すオプション配列。例:
['csrf_protection' => false]
1‑2. FormType ⇄ FormBuilder ⇄ FormInterface の関係
- FormType … 設計図
- FormBuilder … パーツを付け外しする作業員
- FormInterface … 送信・バリデーションを実行する完成品
1‑3. EC‑CUBE でも必須
Symfony ベース であるため
受注・商品登録など管理画面フォームも すべて FormType で組まれている
独自入力欄を追加する場合も、標準の FormType を継承・拡張するのが王道
2. FormType メソッドと FormBuilder API
2-0. FormBuilderInterface ― 機能別メソッド早見表
カテゴリ | 代表メソッド | 用途 |
---|---|---|
フィールド操作 |
add() / create() / remove() / get() / has() / all()
|
フィールドを追加・生成・取得・削除 |
ビルダー生成 | getForm() |
FormInterface を確定生成 |
データ操作 |
setData() / getData()
|
初期データの注入・取得 |
イベント |
addEventListener() / addEventSubscriber()
|
FormEvents::* にリスナ/サブスクライバ登録 |
データ変換 |
addModelTransformer() / addViewTransformer() / resetViewTransformers() / getModelTransformers() / getViewTransformers()
|
エンティティ⇔フォーム値⇔表示値 の変換カスタム |
属性・HTML |
setAction() / setMethod() / setAttribute() / setAttributes() / getAttribute() / getAttributes()
|
フォームタグの action , method , 任意属性 |
マッピング制御 |
setDataMapper() / getDataMapper() / setCompound() / setMapped() / setPropertyPath() / setByReference() / setInheritData()
|
オブジェクト⇔フィールド間の同期方法指定 |
Tip : これらは すべてチェーン可能なので、
$builder->add(...)->addEventListener(...)->setMethod('PUT')
のように繋げて書ける
2-1. buildForm()
― フィールドとイベントの司令塔
public function buildForm(FormBuilderInterface $builder, array $options): void
{
// 1) フィールドを静的に追加
$builder
->add('title', TextType::class, ['label' => '記事タイトル'])
->add('body', TextareaType::class, ['attr' => ['rows' => 10]]);
// 2) イベントで動的にフィールドを操作
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event): void {
$article = $event->getData();
$form = $event->getForm();
// 新規作成時のみ「下書き保存」チェックボックスを追加
if (!$article || null === $article->getId()) {
$form->add('draft', CheckboxType::class, [
'mapped' => false,
'required' => false,
'label' => '下書きとして保存'
]);
}
});
// 3) モデル⇆ビュー変換を挟む
$builder->addModelTransformer(new CallbackTransformer(
fn (?string $v) => $v ?? '', // 変換: モデル→フォーム
fn (?string $submitted) => trim($submitted) // 逆変換: フォーム→モデル
));
}
-
イベント活用例
-
FormEvents::PRE_SET_DATA
: 初期データを基にフィールド構成を変更 -
FormEvents::PRE_SUBMIT
: リクエスト値に応じた動的バリデーション -
FormEvents::SUBMIT
: バインド直前にデータを正規化・補完
-
2-2. configureOptions()
― フォーム外から渡せるスイッチを定義
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Article::class,
'include_cover' => false, // ← 独自オプション
'csrf_protection' => true,
]);
$resolver->setAllowedTypes('include_cover', 'bool');
}
-
Controller 側
$form = $this->createForm(ArticleType::class, $article, [ 'include_cover' => true, // → buildForm() 側の分岐に利用できる ]);
2-3. buildView()
― Twig に追加変数を輸送
public function buildView(FormView $view, FormInterface $form, array $options): void
{
// 独自オプションをそのまま渡す
$view->vars['include_cover'] = $options['include_cover'];
// すべての入力に一括で CSS クラスを追加
$view->vars['attr']['class'] = trim(($view->vars['attr']['class'] ?? '').' card');
}
2-4. finishView()
― 子ビュー確定後の後処理
public function finishView(FormView $view, FormInterface $form, array $options): void
{
// 奇数行に z-stripe クラスを付与
$i = 0;
foreach ($view->children as $child) {
if (++$i % 2) {
$child->vars['row_attr']['class'] = 'z-stripe';
}
}
}
2-5. getBlockPrefix()
― Twig ブロック名を一括変更
public function getBlockPrefix(): string
{
// デフォルトは `article`
return 'blog_article';
}
Twig 側で blog_article_row
・blog_article_widget
などのブロックを上書きできる
2-6. getParent()
― 既存 FormType を薄くラップ
class PriorityChoiceType extends AbstractType
{
public function getParent(): string
{
return ChoiceType::class;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'choices' => ['High' => 'high', 'Medium' => 'medium', 'Low' => 'low'],
'placeholder' => '---',
]);
}
}
Controller では ->add('priority', PriorityChoiceType::class)
と書くだけで済み、設定漏れを防止
3. 組み込み FormType
Text 系
TextType
TextareaType
EmailType
IntegerType
MoneyType
NumberType
PasswordType
PercentType
SearchType
UrlType
RangeType
TelType
ColorType
Choice 系
ChoiceType
EnumType
EntityType
CountryType
LanguageType
LocaleType
TimezoneType
CurrencyType
Date/Time 系
DateType
DateIntervalType
DateTimeType
TimeType
BirthdayType
WeekType
その他フィールド
CheckboxType
FileType
RadioType
Symfony UX フィールド
CropperType
DropzoneType
UID フィールド
UuidType
UlidType
フィールドグループ
CollectionType
RepeatedType
Hidden フィールド
HiddenType
ボタン
ButtonType
ResetType
SubmitType
ベース
FormType
4. FormType が理解する 共通オプション
前提
- ここで挙げるオプションは すべての FormType が共通で受け付ける 基本パラメータ
- フィールド固有のもの(
choices
など)は 個別で要確認(後でまとめたい)
4-1. データ/マッピング系
オプション | 役割 & 典型値 | よくある使い方 | ||
---|---|---|---|---|
data |
初期値(フォームツリー確定前に直接セット) | createForm(MyType::class, null, ['data' => ['foo' => 'bar']]) |
||
data_class |
バインド対象エンティティ FQCN |
Article::class を指定→Article の setter/getter が使われる |
||
empty_data |
送信値が空 のときに入れる値 文字列 / クロージャ / 配列 |
empty_data => '' で必須文字列を空文字に、fn() => new User() で新オブジェクト生成 |
||
property_path |
エンティティのプロパティ名(ドット区切り可) |
user.address.street のようにネスト先へマッピング |
||
mapped |
false にすると 画面 ↔ エンティティ同期しない
|
検索フォーム・確認用 hidden などに便利 | ||
by_reference |
true (既定) = セッターを呼ばず配列/コレクションを参照渡し |
コレクション編集時に setter 経由で検証したい場合 false
|
||
inherit_data |
親のデータオブジェクトを子に共有 | サブフォームが別クラスを持たない場合に重宝 | ||
compound |
false にすると単一値扱い |
自作ウィジェットで値をまとめて投げるとき |
4-2. バリデーション/エラー制御
オプション | 役割 | 例 |
---|---|---|
constraints |
NotBlank , Length … Symfony\Validator 制約配列 |
['constraints'=>[new NotBlank(), new Length(max:255)]] |
validation_groups |
Default 以外のバリデーショングループを選択 |
'validation_groups' => ['Registration'] |
error_bubbling |
子のエラーを親に表示 (true ) |
チェックボックスグループなどでまとめてエラー表示 |
error_mapping |
エラーを別フィールドに付け替え | ['password' => 'plainPassword'] |
invalid_message(_parameters) |
変換失敗時のメッセージと置換変数 | 日付パース失敗などで独自文言を出す |
extra_fields_message |
送信に「未知フィールド」があった場合のメッセージ | API などで厳格にしたいとき |
post_max_size_message |
PHP アップロードサイズ超過エラー文言 |
upload_max_filesize 超過時に表示 |
4-3. 表示/ラベル/HTML 属性
オプション | 効果 | サンプル |
---|---|---|
label / label_format
|
ラベル文言 or プレースホルダで自動生成 | label_format => 'form.%name%' |
label_attr / row_attr
|
<label> / 行 <div> への任意属性 |
['row_attr'=>['class'=>'mb-3']] |
label_html / help_html
|
ラベル・ヘルプを HTML 解釈するか | Bootstrap アイコンを埋め込みたいとき true
|
help , help_attr , help_translation_parameters
|
ヘルプテキスト&属性&翻訳変数 | |
attr , attr_translation_parameters
|
ウィジェット直付け属性 | attr => ['placeholder'=>'検索…'] |
block_name , block_prefix , priority
|
Twig ブロック名と描画順制御 | 高度なテーマカスタムで使用 |
translation_domain / label_translation_parameters
|
翻訳ドメイン切替 & プレースホルダ | translation_domain=>'admin' |
4-4. 動作/リクエスト関連
オプション | 目的 | サンプル |
---|---|---|
required |
HTML5 required 属性生成 + バリデーション必須化 |
required=>false で任意入力 |
disabled |
入力不可 (<input disabled> ) |
編集不可フィールドに |
trim |
文字列トリム | API 用フォームで false にする例も |
allow_extra_fields |
送信に余計なキーがあっても無視 | Webhook 受信フォームなど |
action , method
|
<form> の送信先 URL と HTTP メソッド |
$builder->setAction('/search')->setMethod('GET') |
csrf_protection |
CSRF トークン生成有無 | API 専用なら false
|
4-5. コールバック/HTML5 カスタム
オプション | 用途 | 一言 |
---|---|---|
getter / setter
|
Entity プロパティ名をオーバーライド | MagicAccessor を使う場合 |
form_attr |
親 <form> タグに付ける属性 |
form_attr=>['novalidate'=>'novalidate'] |
4-6. 代表的な組み合わせ例
$builder
->add('cover', FileType::class, [
'label' => false,
'mapped' => false, // エンティティに自動保存しない
'required' => false,
'help' => 'jpeg / png, 最大2MB',
'help_html' => true,
'constraints' => [
new File(maxSize: '2M', mimeTypes: ['image/jpeg','image/png']),
],
'attr' => ['accept'=>'image/*'],
])
->setAction($options['upload_action']) // configureOptions 側で注入
->setMethod('POST');
-
ポイント
-
mapped:false
でファイルストレージ処理を自前でハンドリング -
help_html:true
によりヘルプ文に<br>
や<i class="bi-info">
など挿入可能 -
setAction()
/setMethod()
は FormBuilder のメソッド(オプション指定でも可)
-
困った時の参照用
# | 困りごと / シナリオ | 対応 | ➕ ヒント |
---|---|---|---|
1 | エンティティに保存せず画面だけで使いたいフィールド |
mapped:false / property_path:false
|
検索フォーム・確認チェック用 hidden に最適 |
2 | 必須⇆任意を動的に切り替えたい | a. $form->get('foo')->setRequired(false) b. イベントで required オプション変更 |
PRE_SET_DATA で初期値を見て分岐 |
3 | CSRF で弾かれる API / Webhook 受信用 | ルートフォーム csrf_protection:false
|
併せて allow_extra_fields:true で未知キー無視 |
4 | エラーを 1 か所(フォーム上部)に集約したい | ルートに error_bubbling:true
|
個別配置なら error_mapping
|
5 | i18n:ラベルを翻訳キーで書きたい |
label_format:'form.%name%' + translation_domain:'admin'
|
%name% がフィールド名に置換 |
6 | CollectionType の add/remove が効かない | 子 Type:entry_options['label'=>false] + prototype:true /親:by_reference:false
|
setter 無い時は allow_add , allow_delete
|
7 | アップロード 2 MB 超過時のメッセージ変更 | ルート post_max_size_message:'ファイルが大きすぎます'
|
FileType の constraints も要追加 |
8 | PUT/DELETE など HTML 非対応メソッド送信 |
setMethod('POST') + hidden _method
|
EC-CUBE でも同じ(メソッドスプーフ) |
9 | 環境ごとに送信先 URL を動的変更 | controller で $form->setAction($url)
|
setAction() は Builder でも可 |
10 | 状態別にバリデーショングループ切替 | validation_groups => fn(FormInterface $f) => $f->getData()->isDraft() ? ['Draft'] : ['Default'] |
Closure で柔軟指定 |
11 | row / label へ属性を一括追加 |
row_attr / label_attr または $builder->setAttribute()
|
テーマが属性を出力しているか確認 |
12 | 特殊 getter/setter を使いたい |
getter:'isPublished' , setter:'markPublished'
|
DTO でも活用可 |
13 | 空文字を null にしたくない |
empty_data:'' + trim:false
|
API で “空文字も値” の時 |
14 | フォーム複数描画で ID 衝突が怖い | それぞれで getBlockPrefix() を上書き |
Twig ブロック名も衝突回避 |
15 | 動的フィールド追加時にバリデーションが効かない | 追加後に setValidationGroups() /POST_SUBMIT で再評価 |
Choice 依存入力で発生しがち |
16 | フォーム全体を multipart にしたい | $builder->setAttribute('enctype','multipart/form-data') |
FileType があれば自動設定されるが念のため |
17 | ArrayCollection の追加が永続化されない | 親エンティティ addXxx() 実装 + by_reference:false
|
Doctrine の cascade も確認 |
18 | Ajax で部分バリデーションしたい | サブフォーム validation_groups:['Partial']
|
JS で fetch → JSON エラー返却 |
19 | ラベル/ヘルプを HTML 混在にしたい |
label_html:true , help_html:true
|
アイコンタグや <br> も使える |
20 | HTML5 のブラウザ自動検証を無効化したい | ルート form_attr:['novalidate'=>'novalidate']
|
JS で独自検証を行うケース |
21 | 複数フィールドをグループ化 (fieldset) したい |
add('group', FormType::class,['label'=>false]) 内に子フィールド |
compound:true でネスト可能 |
22 | ChoiceType の表示順を厳密保持したい |
choice_loader:null + 配列順に choices 指定 |
or preferred_choices で先頭固定 |
23 | ChoiceType にプレースホルダーが欲しい | placeholder:'---- 選択 ----' |
required:false にしないと選択不可 |
24 | DateType の年範囲を限定したい | years: range(date('Y'), date('Y')+5) |
期間外日付はバリデーション NG |
25 | EntityType でソフトデリートを除外したい |
query_builder で WHERE e.deletedAt IS NULL
|
リポジトリで共通メソッドを切ると◎ |
26 | エラーメッセージを別翻訳ドメインにまとめたい |
translation_domain:'errors' + 各 invalid_message はキーで指定 |
バリデーションメッセージにも適用 |
27 | FileType を非活性表示 (値保持) したい | disabled:true |
編集不可モード用 |
28 | 2 フィールドを結合して 1 プロパティへ保存 | setDataMapper(new CustomMapper()) |
例:電話番号 (市外 + 市内) |
29 | HTML id をカスタムしたい | attr:{id:'my_custom_id'} |
CSS/JS でピンポイント操作 |
30 | DB レコード数に応じてフィールドを動的生成 | addEventSubscriber(new DynamicFieldSubscriber($repo)) |
PRE_SET_DATA でループ追加 |