Symfony Form Eventsの利用

はじめに

こんにちは。イメージ・マジック廣田です。
この頃は猛暑の上に、勉強することが多くて知恵熱が…(-д-;)
冗談ですが。

さて、業務でフォームを扱うことが多く、symfonyフォームの使い方を知ったので備忘がてら書いていきます。

Symfony Form について

PHPフレームワーク、SymfonyではHTMLフォームを扱いやすくするためにFormフォームが用意されています。

フォームを使う場合、入力された情報はDBに保存される場合が多いですよね。
例えば会員登録のフォームに入力したユーザ名やメールアドレス、パスワードなどはDBのUser
テーブルに保存される、といったような場合です。
SymfonyのFormバンドルでは、そういった仕組みのためにフォームをエンティティと結び付けてくれます。
基本的な使い方はSymfonyの公式ドキュメントをご参考に。

基本のその先

フォームに入力してそのままDB保存処理という流れの場合、
フォーム用意して…

・フォーム用意
Public function buildForm(FormBuilderInterface $builder, array $options)
{
	$builder->add(‘email’, TextType::class)
		->add(‘userName’, TextType::class)
		->add(‘password’, PasswordType::class)
	        ...
}

コントローラで表示処理とか保存処理とか…

・コントローラ
$user = $isNew ? new User() : $UserRepository->find($id);  //エンティティ用意
$form = $this->createForm(UserType::class, $user); //第二引数でエンティティを指定してフォームと連携
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) { 
        // submit後の処理
        return $this->redirectToRoute('task_success');
    }

templateに表示…

・Twigでの表示
<div>
 <label>メールアドレス</label>
 {{ form_widget(form.email) }}
</div>
<div>
<label>ユーザ名</label>
{{ form_widget(form.userName) }}
</div>
……

という流れになると思います。

しかしながら、ユーザビリティを考慮して初期値を設定したり、submit後に入力値を調整したりしたい場合があります。

例えば、ユーザ登録フォームの場合。
メアドだけ変更したいけど、パスワード欄はpasswordTypeが設定されているため編集画面では常に空欄になってしまい、いちいちパスワード入力しないといけなくて面倒…。
パスワード欄を空欄のままにしても、送信後に自動的に既定値がセットされるようにしてバリデートエラーを避けたいなぁ…というようなシーンがあったとします。

そんな時に、コントローラ内で以下のようにすれば良いと思いました。

$form = $this->createForm(UsrMstEditType::class, $u);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
 送信後の処理
}
//以下でformのpassword欄の入力値を変更したい
$form->get("password")->setData($password);

しかし、これだと以下のエラーが出ます。
You cannot change the data of a submitted form.
送信されたデータを変えることはできないですよ、とのこと。

symfonyドキュメントを見直すと、こんなときはForm Eventsというのを使うと良いらしいです。

Form 送信フロー


こちらの送信フローを念頭にForm Eventsの使用例を書いていきます。

Form Eventsの利用

以下Form Eventを利用することで、Symfonyのフォームをより柔軟に動かすことができます。

FormEvents Constant Event’s Data
FormEvents::PRE_SET_DATA Model data
FormEvents::POST_SET_DATA Model data
FormEvents::PRE_SUBMIT Request data
FormEvents::SUBMIT Normalized data
FormEvents::POST_SUBMIT View data

1. Model Dataの修正

フォームの整形をする場合にはPOST_SET_DATAを利用します。

createFormの第二引数にEntityを入れるとそのEntityのフィールドの値が入りますが、フォームにそのEntityのフィールド以外の項目があった場合、そのままだと該当フォームは当然空欄のままで表示されます。
そこに初期値を設定したい時に利用したのがこのイベントです。

例えば以下のようなフォームを考えます。

$builder->add(‘email’, TextType::class)
		->add(‘userName’, TextType::class)
		->add(‘password’, PasswordType::class);
		->add(‘company’, EntityType::class, [
			‘Class’ => Company::class,
			'mapped' => false,
			'multiple' => true,
			'query_builder' => function (EntityRepository $er) {
				return $er->createQueryBuilder('c')
					->addOrderBy('c.id');}
		]);

companyの項目はUserエンティティのフィールドに存在しないものとします。
そこに以下の指定で初期値を設定します。

$entity = $builder->getData();//Userエンティティ取得
if($entity->getId()) {
	$builder->addEventListener(
		FormEvents::POST_SET_DATA,
		function(FormEvent $event) use ($entity) {
			$relations = $this->em->getRepository(UserCompany::class)->findCompaniesByUser($entity);
			$col = new ArrayCollection();
			foreach($relations as $r) {
				$col->add($r->getCompany());
			}
			$form = $event->getForm();
			$form->get(‘company’)->setData($col)
		});
}

これで初期値を設定できました!

2. Request dataの修正

リクエストデータの修正にはPRE_SUBMITイベントを利用します。

空欄のパスワード欄に規定値をセットする場合に利用したのがこのイベントでした。
実際のコードは以下のようになりました。

public function buildForm(FormBuilderInterface $builder, array $options)
{
	$builder->add(‘email’, TextType::class)
		->add(‘userName’, TextType::class)
		->add(‘password’, PasswordType::class);
	
	//パスワードが空欄でも既存値をFormにセットする処理
	$builder->addEventListener(FormEvents::PRE_SUBMIT, function(FormEvent $event) {
		$data = $event->getData(); //入力したデータ取得
		$form = $event->getForm(); //フォームを取得
		if(is_array($data)) {
		  if(!$data[‘password’]) {
			$data[‘password’] = $form->getData()->getPassword();
		  }
		} else {
 		  if(!$data->getPassword()){
			$data->setPassword($form->getData()->getPassword());
		  }
		}
		$event->setData($data);
        });
      }

addEventListener以下の処理をすこし解説すると、
・$event->getData()でフォームに実際に入力されたデータを取得
・$event->getForm()でフォームを取得。
・if(!$data[‘password’]) 以下でパスワード欄の入力値の有無を判断し、空欄の場合にすでにエンティティが持っているパスワードをリクエストデータにセットしています。

$event->getForm()->getData()でcreateFormの第二引数に指定されたエンティティが取得できるので
$data[‘password’] = $form->getData()->getPassword();
とすればリクエストデータに既存値をセットできますよね。

※if (is_array($data))でリクエストデータが配列かどうか判定しているのは、送信ボタンを押すタイミングによって配列の時もあればオブジェクトの時もあったためです。原因よくわかってません。この辺、原因がわかれば追記したいと思います^^;

さいごに

他にもPRE_SET_DATAは初期値を設定したり、SUBMITはデータのvalidateなどができるようです。
使い方がわかると便利だな~としみじみしました。