SymfonyのCollectionTypeの日本語記事がなかったので書いてみます。
バージョンはEC-CUBE3.nで利用されているはSymfony3.4です。
参考
SymfonyのCollectionTypeとは
1対多の関係にあるフォームを動的に作成するときに使うFormType。
CollectionTypeの基本的な使い方
この章はこちらの公式ドキュメントの例を私の言葉で解説したものです。
正確な情報は公式ドキュメントを参照してください。
CollectionTypeの表示
例えばE-mailの配列の入力フォームを表示させるには以下のようなFormTypeを作成します。
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
// ...
$builder->add('emails', CollectionType::class, array(
// それぞれのフィールドはEmailTypeで作成する
'entry_type' => EmailType::class,
// entry_typeで指定したFormTypeにわたすオプションを指定する
'entry_options' => array(
'attr' => array('class' => 'email-box'),
),
));
twig側で form_row
を記述すると配列のすべてのフォームを表示可能です。
{{ form_row(form.emails) }}
フォーマットを指定して出力させたい場合はループと form_widget
を使って以下のように書けます。
{{ form_label(form.emails) }}
{{ form_errors(form.emails) }}
<ul>
{% for emailField in form.emails %}
<li>
{{ form_errors(emailField) }}
{{ form_widget(emailField) }}
</li>
{% endfor %}
</ul>
form_errors(form.emails)
ではCollectionTypeのエラーが出力され、 form_widget(emailField)
では各フォームごとのエラーが出力されます。
項目の追加
CollectionTypeの allow_add
オプションを有効にしておくと項目の追加が可能になります。
さて、ややこしいのはここからです。
項目を動的に追加するには少し工夫が必要です。
具体的には、
- 項目を追加するためのテンプレート(以下、プロトタイプと呼びます)を準備しておき
- JavaScriptで採番処理をしてから追加してやる
必要があります。
例えば配列に2つのEmailアドレスがある場合には以下のように表示されるとします。
<input type="email" id="form_emails_0" name="form[emails][0]" value="foo@foo.com" />
<input type="email" id="form_emails_1" name="form[emails][1]" value="bar@bar.com" />
ここに新しくフォームを追加するために、まずは以下のようなプロトタイプを準備します。
<input type="email"
id="form_emails___name__"
name="form[emails][__name__]"
value=""
/>
上記の例のフォームの連番の部分が __name__
に置き換わっている事がわかります。
この __name__
をJavaScriptで数字に置き換えて項目を追加してやることになります。
上記のプロトタイプは prototype
オプションを有効にしておき {{ form_widget(form.emails.vars.prototype)|e }}
で出力が可能です。
(ただし |e
はエスケープ処理をするためのフィルタです。)
twigを以下のように拡張します。 data-prototype
にプロトタイプを持たせておきます。
{{ form_start(form) }}
{# ... #}
{# data-prototypeにプロトタイプをセットしておく #}
<ul id="email-fields-list"
data-prototype="{{ form_widget(form.emails.vars.prototype)|e }}"
data-widget-tags="{{ '<li></li>'|e }}">
{% for emailField in form.emails %}
<li>
{{ form_errors(emailField) }}
{{ form_widget(emailField) }}
</li>
{% endfor %}
</ul>
<button type="button"
class="add-another-collection-widget"
data-list="#email-fields-list">Add another email</button>
{# ... #}
{{ form_end(form) }}
以下はJavaScriptで項目を追加する実装例です。
jQuery(document).ready(function () {
// 「Add another email」ボタンがクリックされた場合に実行
jQuery('.add-another-collection-widget').click(function (e) {
// 項目を追加するリスト
var list = jQuery(jQuery(this).attr('data-list'));
// 'widget-counter' から項目番号を取得
var counter = list.data('widget-counter') | list.children().length;
// できない場合はリストの個数を項目番号として使用
if (!counter) { counter = list.children().length; }
// 'data-prototype' からプロトタイプを取得
var newWidget = list.attr('data-prototype');
// "__name__" を項目番号で置換
newWidget = newWidget.replace(/__name__/g, counter);
// 項目番号をインクリメント
counter++;
// 項目が削除されたときのために項目番号を保存しておく
list.data(' widget-counter', counter);
// テンプレートを作成してリストに追加
var newElem = jQuery(list.attr('data-widget-tags')).html(newWidget);
newElem.appendTo(list);
});
});
以上がCollectionTypeの基本的な使い方となります。
よく使うオプション
-
entry_type
: 各項目のFormTypeを指定 -
prototype
: プロトタイプの出力できるようにする -
allow_add
: 項目の追加を許可する(登録されていない項目が送信された場合に新規追加する) -
allow_delete
: 項目の削除を許可する(送信されなかった項目は削除する)
何か処理をしてから項目の削除をしたい時
allow_delete
を指定すると送信されなかった項目は削除されるのですが、削除される前に何か処理をしたい場合があります。
その場合はあらかじめ削除される項目のクローンを作成しておき、処理をしてから消すという手順を踏む必要があります。
詳しくはまた追記します。
EC-CUBEでの応用
EC-CUBEでは以下のようなところで利用されています。
- 商品に対する商品画像やタグ、商品規格
- 受注に対する商品や配送先
- 配送方法に対する配送時間や配送料金
- マスタデータ
配送方法と配送時間の実装例
EC-CUBEの最新ブランチからソースコードから一部抜粋
DeliveryTypeでCollectionTypeを利用しています。
entry_typeオプションでDeliveryTimeTypeを指定しています。
allow_add、allow_delete、prototypeのオプションをそれぞれtrueに設定しています。
/src/Eccube/Form/Type/Admin/DeliveryType.php
$builder
->add('delivery_times', CollectionType::class, [
'label' => 'delivery.label.delivery_time',
'required' => false,
'entry_type' => DeliveryTimeType::class,
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
;
DeliveryTimeTypeではTextTypeのdelivery_timeとHiddenTypeのsort_noを追加しています。
/src/Eccube/Form/Type/Admin/DeliveryTimeType.php
class DeliveryTimeType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('delivery_time', TextType::class, [
'label' => false,
'attr' => [
'placeholder' => 'admin.setting.shop.delivery_edit.delivery_times_placeholder',
],
'constraints' => [
new Assert\NotBlank(),
],
])
->add('sort_no', HiddenType::class, [
'label' => false,
'constraints' => [
new Assert\NotBlank(),
],
])
;
}
}
フォームを動的に追加するJavaScriptは以下のようになっています。
/src/Eccube/Resource/template/admin/Setting/Shop/delivery_edit.twig
var $collectionHolder = $('#delivery-time-group');
var index = $collectionHolder.find('.delivery-time-item').length;
// お届け時間設定の新規作成ボタンでお届け時間項目を追加する
$('#add-delivery-time-button').on('click', function (event) {
// 追加するお届け時間名を取得
var deliveryTimeName = $('#add-delivery-time-value').val();
if (deliveryTimeName == '') {
return;
}
var prototype = $collectionHolder.data('prototype');
var newForm = prototype.replace(/__name__/g, index);
var newForm = newForm.replace(/__value__/g, deliveryTimeName);
// 要素を追加
var $lastRow = $('#delivery-time-group > li:last');
$lastRow.after(newForm);
// お届け時間名を入れる
var inputId = '#delivery_delivery_times_' + index + '_delivery_time';
$(inputId).val(deliveryTimeName);
// 入力欄を初期化
$('#add-delivery-time-value').val('');
// 要素数をインクリメント
index++;
});
新規追加ボタン押下で新規項目を追加します。
項目のテンプレートは delivery_time_prototype.twig
ファイルに切り出しています。
プロトタイプの場合は form.delivery_times.vars.prototype
を引数として渡して delivery_time_prototype.twig
をincludeするようにしています。
/src/Eccube/Resource/template/admin/Setting/Shop/delivery_edit.twig
{# ... #}
<ul id="delivery-time-group" class="list-group list-group-flush sortable-container"
data-prototype="{% filter escape %}{{ include('@admin/Setting/Shop/delivery_time_prototype.twig', {'form': form.delivery_times.vars.prototype}) }}{% endfilter %}">
<li class="list-group-item">
<div class="form-row">
<div class="col-auto d-flex align-items-center">
<input id="add-delivery-time-value" class="form-control" type="text">
</div>
<div class="col-auto d-flex align-items-center">
<button id="add-delivery-time-button" class="btn btn-ec-regular" type="button">新規追加</button>
</div>
</div>
</li>
{% for child in form.delivery_times %}
{{ include('@admin/Setting/Shop/delivery_time_prototype.twig', {'form': child}) }}
{% endfor %}
</ul>
{# ... #}
prototypeのtwigファイルは以下です、
/src/Eccube/Resource/template/admin/Setting/Shop/delivery_time_prototype.twig
<li class="list-group-item delivery-time-item sortable-item">
<div class="row justify-content-around mode-view">
<div class="col-auto d-flex align-items-center">
<i class="fa fa-bars text-ec-gray"></i>
</div>
<div class="col d-flex align-items-center">
<a class="display-label">{% if form.vars.value == '' %}__value__{% else %}{{ form.vars.value }}{% endif %}</a>
</div>
{# ... #}
</div>
<div class="row justify-content-around mode-edit d-none">
<div class="col d-flex align-items-center">
<div class="form-row">
<div class="col-auto d-flex align-items-center">
{{ form_widget(form.delivery_time, {'attr': {'data-origin-value': form.vars.value }}) }}
</div>
{# ... #}
{{ form_errors(form.delivery_time) }}
{{ form_widget(form.sort_no, {'attr': {'class': "sort-no" }}) }}
{{ form_errors(form.sort_no) }}
</div>
</div>
</div>
</li>
少し複雑になっていますがやっていることは前半で説明したことと同じです。
この記事およびコードはCreative Commons BY-SA 3.0ライセンスに従います。