Because We Love Happy Coding

フリーライターからエンジニアへ。発信力だけあり余ってる感じ

ECCUBE3 のフォーム

フォーム

フォームの扱いは、私のようにフレームワークに不慣れな人間からするとだいぶこみ入っている。

ただ、きちんと把握すれば、データベースからフロントエンドまできちんと繋がり、ハンドリングにも手間がない、はずだ。ただECCUBE3ではかなりフォームやEntityの構成が複雑なので把握しづらい。

ER図とかを熟読した方が早いのかもしんない。

目次

Requirements

全体像を俯瞰する

Symfony2 のフォームは、以下の要素が繋がっている。

  • データベース
  • Entity
  • FormType, FormTypeExtension
  • FormBuilder
  • Form
  • FormView
  • Twigテンプレート

データベースやコアコードに手を加えずに、プラグインから干渉しようとする場合、新規ならForm、修正ならFormTypeExtensionより下の要素が対象になる。

フォーム生成の基本

Forms (Symfony Docs)

// FormFactoryからFormTypeを指定してFormBuilderを生成する
$builder = $app['form.factory']->createBuilder('some_form_type');

//FormBuilderにさらにフォームを追加する
$builder
->add('','',array(
  'required' => true,
  'attr' => array(
      'placeholder' => 'type something here'
    ),
  'mapped' => true,
  'label' => 'some_label',
  'constraints' => array(
        new Assert\Length(array('max' => 20)),
    ),
  )
)//セミコロンなしで次に続ける
->add('', '', array(
    'required' => false,
    ),
));//最後はセミコロン必須

$form = $builder->getForm();
$view = $form->createView();

//FormViewをtwigテンプレートに渡す
return $this->render('something.twig', [
            'form' => $view,
        ]);

FormFactoryからFormBuilderを生成する。

FormBuilderで、FormTypeにフィールドを追加したり加工したりする

add()で追加・上書き修正。remove()で削除。

FormBuilderからFormを生成する

Formは3形態のデータを持っている。model data、normalized data、そしてview dataだ。Symfony\Component\Form\Form | Symfony APIにこの3つが存在する理由について説明がある。

To implement your own form fields, you need to have a thorough understanding of the data flow within a form. A form stores its data in three different representations: (1) the "model" format required by the form's object (2) the "normalized" format for internal processing (3) the "view" format used for display

FormからFormViewを生成する

FormViewをrender()でtwigに渡すことでform表示が可能になる。

フォームからのデータ受け取り

public function index(Request $request)
{
    $builder = $app['form.factory']->createBuilder('some_form_type');

    $form = $builder->getForm();

    //ここからがデータ受け取りの処理
    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {

        $data = $form->getData();

//Entity に紐付けされていればここで一時データ更新
$app->persist($SomeEntity);
//データベース書き込み
$app->flush();
    }

}

Form のaction属性で指定されたrouteに向けてPOSTされたデータは、routeの処理(Eccube/ControllerProviderなど)を通って特定のControllerで処理される。ECCUBE3の場合、自分自身にPOSTしてその中で受け取っている場合が多い。Symfony2のドキュメントが挙げているサンプルでもそんな構成になっているようだ。

How to Use a Form without a Data Class (Symfony Docs) 

$form->handleRequest($request)でrequestに含まれるデータを処理する。

Inspects the given request and calls {@link submit()} if the form was submitted. Internally, the request is forwarded to the configured {@link RequestHandlerInterface} instance, which determines whether to submit the form or not.

if ($form->isSubmitted() && $form->isValid())でフォームの整合性を確認。getData()でPOSTされたデータを取得する。

Entityとのひもづけ

Forms (Symfony 2.7 Docs)

上記のページの「Setting the data class」の囲みに、OptionsResolverを使って、Entityとの紐付けを行う方法が紹介されている。

この場合、Entityの値とformの内容が一致しないといけないので、Controllerなどで後から余計なフォームを追加すると例外を投げられてしまう。

これを避けるにはadd()のオプションで array('mapped' => false)を指定する。'mapped' => falseになっている場合、$form->get('some_form_name')->setData()を用いて、値を変更することもできる。

フォームのオプション

FormBuilder にadd()する際一緒に指定できるオプションが、いろいろ用意されている。

'required',
'attr' => array(
    'placeholder' => 'Name01',
),
'empty_value'

一覧はこちら。

FormType Field (Symfony Docs)

よく使うものについては、以下で一言解説。

constraints

さまざまな基準で入力値をチェックすることができる。

Validation (Symfony Docs)

オプションのrequiredとは別にNotBlankのconstraintsがかかっていると、結局requiredにしているのと大差ないので、既存のformを変更する場合にはconstraintsもチェックした方がよい。

data

「オブジェクトに関連付けられたデータと無関係に、別のデータを表示したい時に指定する。」というようなことがSymfony2のドキュメントにある。

empty_data

選択式のinput要素に対して、選択前の状態を指定する。

mapped

Entityオブジェクトに関連付けられたformの場合、無関係のformを勝手に追加すると「こんなプロパティはない」と怒られる。mappedをfalseにしておくことで、Entityオブジェクトとの関連付けに例外を設けることができる。

required

入力必須項目に指定する。

attr

Twig テンプレートがフォームのHTMLを生成する際に属性を追加できる。HTMLの属性を参照。

ただしrequiredなどにattrの外に別途指定できるようになっているものもあるので混同しないよう。attrの中に入れてもたぶんレンダリングの結果は同じになるかもしれない。

classを指定する時に一番使うかも。

label

form_label() に表示する文字列を指定する。

repeatedグループ

確認のために2回入力をさせるような場合に使う。これが仕組みがわかるまではコントロールできず弱った。

Instead of code with {{ form_widget(form.plainPassword }} Just change by {{ form_widget(form.plainPassword.first }} {{ form_widget(form.plainPassword.second }\} php - How to not display label from password field in Twig - Stack Overflow

という書き方もあるし、こういう書き方もあるみたい。ECCUBE3のテンプレートにあった。

{% for emailField in form.email %}
    <div class="form-group {% if emailField.vars.errors is not empty %}has-error{% endif %}">
    {{ form_widget(emailField,{'attr':{'class':"foobar"}}) }}
    {{ form_errors(emailField) }}
</div>
{% endfor %}

つまり、Repeatedグループのform.emailの中にはemailFieldが2つ含まれているので、forで回して処理をしていく、ということらしい。うっかりEmailfield.firstとやってみたら「そんなプロパティはない」とエラーになる。それはそうだ。email.first(=emailの中の最初の要素)は指定できるが、Emailfield.firstはできない。

{% for emailField in form.email %}
    <div class="form-group {% if emailField.vars.errors is not empty %}has-error{% endif %}">
    {% if loop.index == 1 %}
        {{ form_widget(emailField,{'attr':{'class':"foobar1"}}) }}
    {% elseif loop.index == 2 %}
        {{ form_widget(emailField,{'attr':{'class':"foobar2"}}) }}
    {% else %}
        
    {% endif %}
    {{ form_errors(emailField) }}
</div>
{% endfor %}

choice_attr

選択肢をオプションで決める場合に。

choice Field Type (select drop-downs, radio buttons & checkboxes) (Symfony 2.7 Docs)

expanded, multiple

この2つのオプションによって、HTMLにレンダリングされた時にselectになるか、ラジオボタンになるか、など要素名が決まる模様。

Twigでのフォームオプション指定

form_widget()内で指定する。

{{ form_widget(emailField,{'attr':{'class':"someClass thatClass", 'placeholder':"E-mail"}}) }}

実際、PHP側でコントロールするのが分かりづらい部分もあるので、Twigで制御するとだいたい確実ではある。

name 属性の指定が効かない

通常は、attrオプションによってHTMLにレンダリングされた際のformの属性値を変更することができる。class属性やid属性はこれでいい。

だがname属性は注意が必要。以下のコードを書いたとする。

$builder = $app['form.factory']->createBuilder(); 
$form = $builder ->add('ShopID', 'text', array('attr' => array('name'=>'ShopID'))); 
return $app ->render($app->render('index.twig', array(
'form'=>$form,
)); 

attrにnameを指定したのだからレンダリングされたフォームはそのname属性を持っているはずなのだが、結果、name属性はform[ShopID]となっていた。勝手にform[]が付加されて外せなかった。

確認したわけではないが、renderに渡した時の名前がここに入ってくるのかもしれない。どうやらSynfony2の仕様らしい。(それともform_themeとかいうやつ?)

たしかにECCUBE3の中で処理する限り、共通の変数配列の中に置いておけばハンドリングしやすい。それはわかるが、外部のAPIなどに値をPostしたいときにこれでは困る。

Synfony2の情報を探ったところ、やや裏技っぽいのがあった。createNamedBuildernullで呼ぶという。

Change input name of form fields in Symfony2

$builder = $app['form.factory']->createNamedBuilder(null); 
$form = $builder ->add('ShopID', 'text', array('attr' => array('name'=>'ShopID'))); 
return $app ->render($app->render('index.twig', array(
'form'=>$form,
)); 

この場合、name属性はShopIDとなる。

拡張したのにformに追加されない

デバッグモードのSynfonyProfilerにフォームの項目があるので、ここを見るとフォームが追加されたかどうかよくわかる。

serviceProvider に登録し忘れている

FormExtensionServiceProviderに登録する必要がある。忘れていると追加されない。

住所のaddr01だけ保存されない

これは手強かった。なぜかaddr01だけが保存されず、addr02は保存されていた。

結論だけ言うと、FormTypeExtensionで拡張したaddr01と、イベントフックで拡張したaddr01が上書きしあっていた。

わかってしまえば単純なミスだが、挙動からこれを見つけるのはけっこう難しい。addr01 フォームから受けたデータはFormTypeExtensionが受け取っていたが、その後にイベントフックしたaddr01が上書きされてnullになってしまっていた。

当然、イベントフックで拡張した方を削除すれば無事反映されるようになった。

既存のFormTypeを編集したり、要素を追加、削除したりする

ECCUBE3 の初心者が戸惑う箇所の一つは、既存のFormTypeを編集する方法ではないかと思う。私はこのやり方を把握するまでに時間がかかった。

EC-CUBE本体で定義されているFormTypeを拡張するには、フックポイントを利用する方法と、Form Extensionを利用する方法の2種類があります。 プラグインによるフォームの追加、変更 | EC-CUBE 開発ドキュメント

FormExtension

EC-CUBE3.0.8まではFormExtensionというイベントを使ってFormに対して拡張を行えましたが、3.0.9からは新たなイベントが用意されたため、そのイベントでフォームの拡張を行います。 開発ドキュメント(https://doc.ec-cube.net/plugin_bp_form)

この日本語の書き方だと「3.0.9以降は非標準的」という位置づけに読めるが、既存のフォームに何か追加、削除、上書きするならFormTypeExtensionをまず、一番に検討すべきだ。ECCUBE3のfax項目(TelTypeの亜種)のような複雑なものでなければ、FormTypeExtensionで対応できる。

作成手順

function buildForm()に追加や削除を指定する。

function buildForm(){
$builder->remove('foo');
$builder->add('bar', 'text', array());
}

getExtendedType() に、拡張したいformの名前(該当するFormTypegetName()がreturnしてくる値)を指定する。OrderType を拡張したいなら'order'。

function getExtendedType(){
return 'order'; 
}

拡張したformの値をデータベースに保存するには

$this->app['orm.em']->persist($some_entity);
$this->app['orm.em']->flush($some_entity);

する必要がある。逆にこれをやらないと値が保存されないので注意。

イベントフックで拡張する

局所的に追加する場合は、イベントフックで拡張する。

FormEventで変更する

ものによっては、FormEventをフックして変更する必要がある。

FormEvents::POST_SET_DATAで入れ替えるのが妥当なようです。 EC-CUBE 開発コミュニティ - フォーラム

他のメソッド

buildForm : createBuilder が呼ばれたタイミングで実行される buildView : createView が呼ばれたタイミングで実行される
【EC-CUBE】プラグインでDBの値をtwigに渡す方法 - Qiita

参考記事