Do You PHP はてブロ

Do You PHPはてなからはてブロに移動しました

Symfony2のFormTypeでラジオボタンの属性を付ける

なんかタイトルの日本語がおかしい気もしますが。。。

忙しい人のための解決法超概要

デフォルトのフォームテーマ(form_div_layout.html.twig)では、choice_widgetブロック内でexpandedの場合に改めてform_widget関数を呼び出しているが属性情報を引き継いでいないため、ラジオボタンチェックボックスに属性を直接付けることができない。なので、このようなカスタムフォームテーマを作ってテンプレートに適用する必要がある。

発端


Symfony2のFormTypeで、radioボタンの属性ってどうやって付ければ良いんだろう?twig側のform_widget関数にattr属性を指定してもradioボタンを囲むdivタグにその属性が付いてしまう。。。 #symfony2

ツイートのとおり、ラジオボタン(choice)を含むFormTypeがあり、そのラジオボタンに任意の属性(foo属性)を付けようとしてたのですが、以下のようにinput[type=radio]な要素の外側に出力されるdiv要素やlabel要素に指定した属性が出力されてしまいました。

<div>
    <label foo="bar" class=" required">性別</label> <!-- ここには属性が付いている -->
    <div id="form_gender" foo="bar">                <!-- ここにも属性が付いている -->
        <!-- 肝心のラジオボタンには属性が付かない -->
        <input type="radio" id="form_gender_0" name="form[gender]" required="required" value="0" checked="checked" />
        <label for="form_gender_0" class=" required">女性</label>
        <input type="radio" id="form_gender_1" name="form[gender]" required="required" value="1" />
        <label for="form_gender_1" class=" required">男性</label>
    </div>
</div>

原因調査

フォームをレンダリングする際に使用されるテンプレートはテーマ(theme)と呼ばれ、デフォルトでは以下のファイルが使用されます。

vendor/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig

このform_div_layout.html.twigでは、フォームの各パーツ毎にblockが用意されており、ラジオボタンも以下の様なテンプレートになっています(radio_widgetブロック)。

{% block radio_widget %}
{% spaceless %}
    <input type="radio" {{ block('widget_attributes') }}{% if value is defined %} value="{{ value }}"{% endif %}{% if checked %} checked="checked"{% endif %} />
{% endspaceless %}
{% endblock radio_widget %}

これを見る限りラジオボタンの場合も問題なく属性が出力されるような感じがします。ここで、プルダウン・ラジオボタンチェックボックスの抽象型である"choice"タイプを出力するchoice_widgetブロックを見てみます。

{% block choice_widget %}
{% spaceless %}
    {% if expanded %}
        <div {{ block('widget_container_attributes') }}>
        {% for child in form %}
            {{ form_widget(child) }}
            {{ form_label(child) }}
        {% endfor %}
        </div>
    {% else %}
    <select {{ block('widget_attributes') }}{% if multiple %} multiple="multiple"{% endif %}>
        {% if empty_value is not none %}
            <option value="">{{ empty_value|trans }}</option>
        {% endif %}
        {% if preferred_choices|length > 0 %}
            {% set options = preferred_choices %}
            {{ block('widget_choice_options') }}
            {% if choices|length > 0 and separator is not none %}
                <option disabled="disabled">{{ separator }}</option>
            {% endif %}
        {% endif %}
        {% set options = choices %}
        {{ block('widget_choice_options') }}
    </select>
    {% endif %}
{% endspaceless %}
{% endblock choice_widget %}

プルダウンとそれ以外で処理が分かれており、ラジオボタンチェックボックスの場合には再度form_widget関数を呼び出していますが、よく見るとform_widget関数のオプションが指定されていない、つまり、属性情報を渡していないことが分かります。ここが原因です。

なお、フォームテーマの話はSymfony2日本語ドキュメントを参照してください。

解決策

choice_widgetブロックを上書きするカスタムフォームテーマを作り、バンドルのResources/views/Form以下に保存します。ソースはgithubに上げてあります

{% extends "form_div_layout.html.twig" %}

{% block choice_widget %}
{% spaceless %}
    {% if expanded %}
        {##
         # divタグにはid属性のみ出力
         #}
        <div id="{{ id }}">
        {% for child in form %}
            {##
             # 子タグに属性情報を引き継ぐ
             #}
            {{ form_widget(child, { 'attr': attr }) }}
            {{ form_label(child) }}
        {% endfor %}
        </div>
    {% else %}
    <select {{ block('widget_attributes') }}{% if multiple %} multiple="multiple"{% endif %}>
       :
    </select>
    {% endif %}
{% endspaceless %}
{% endblock choice_widget %}

また、バンドルのtwigテンプレートでform_themeタグを使用してカスタムフォームテーマを適用します。

{% form_theme form "AcmeFormThemeBundle:Form:form_div_layout.html.twig" %}
             :

これで意図するように属性が出力されるようになりました。

<div>
    <label foo="bar" class=" required">性別</label>
    <div id="form_gender">                          <!-- ここには属性が付かない -->
         <!-- ラジオボタンに属性が付いている -->
        <input type="radio" id="form_gender_0" name="form[gender]" required="required"    foo="bar" value="0" checked="checked" />
        <label for="form_gender_0" class=" required">女性</label>
        <input type="radio" id="form_gender_1" name="form[gender]" required="required"    foo="bar" value="1" />
        <label for="form_gender_1" class=" required">男性</label>
    </div>
</div>

まとめ

カスタムフォームテーマを利用したバンドルは結構公開されていますので解決策としてはありがちだった訳ですが、「テンプレートのHTML」ではなく「form_widget関数の引数が原因」だと気づくまでにちょっとかかりました。。。
また、これが唯一の解ではないので、block関数を駆使したりすればもっと簡単になるかも知れません。