PlayFrameworkには、デフォルトでフォームを作る機能があります。
フォームについて、html部分とそれ以外について、説明してみます。
前説:PlayFrameworkのHTML描画について
まずは前提で、PlayFrameworkではどうやってHTMLを描画するのか、触れておきます。
一般的なMVCパターンと同様、Controllerがリクエストを受け取って、必要なModelを構築し、それをViewに渡す、という方式です。
Modelはなんでもよいですが、PythonのDjangoのように既定の扱いやすいModelがあるわけではないです。なのでそこの詳細は今回は割愛します。
Viewのテンプレートエンジンは、既定ではTwirl
と呼ばれるものを使っています。ASP.NET Razor
からインスパイアされたそうです*1。基本はcontrollerから描画用のクラス(Model)を受け取って、それをテンプレート上で参照しながら描画します。
フォームについて
HTMLのFormがどういう形式で送信されるのかは、MDNに説明があります。
要するに、単なる文字列です。型付の言語上のプログラムでフォームデータをよしなに扱うには、この単なる文字列を型付のクラスに変換する必要があります。フォームを作る側としては、定義を書いたら変換してくれるのが望ましいです。
生成
フォームの生成は、こういう書き方ができます。データ型は適当です。
import play.api.data._ import play.api.data.Forms._ case class InquiryData( name: String, tel: String, email: String, reason: Int, ) object InquiryData { val inquiryForm = Form( mapping( "name" -> nonEmptyText, "tel" -> nonEmptyText, "email" -> email, "reason" -> number )(InquiryData.apply)(InquiryData.unapply) ) }
これだけ書いておけば、あとはController側で変換メソッドなどを呼び出せばおおむねそのまま使えます。
フォームデータの取り出し
POST
なりGET
リクエストなりでフォーム入力データをControllerで受け取ったものとすると、form.bindFromRequest
メソッドを使うと、フォームデータを変換した後の結果を取得できます。
フォーム関連のクラスについて
どういうクラスが使われているか書きます。
フォームと紐づくクラス
自分で定義したクラスと、フォームを紐づけられます。この定義はいろいろありますが、上記の例では単なるcase classにしています。case classが良い理由は後述します。
case class InquiryData( name: String, tel: String, email: String, reason: Int, )
フォームで入力してほしいデータと一対一で紐づくのが、最も楽な定義です。
Form[T]
フォームは、オブジェクトとして定義します。そのフォームをControllerからViewに渡して、テンプレートで描画できます。それらを楽にできるように、PlayFrameworkにはForm
クラスが用意されています。play.api.data.Form
にあります。
T
は、フォームのデータと紐づけたいクラスを指定します。
case class Form[T](mapping: Mapping[T], data: Map[String, String], errors: Seq[FormError], value: Option[T]) { // }
上はインスタンス生成のコンストラクタで、コンパニオンオブジェクトに用意されている生成はもっと簡単です。
* * @param mapping the form mapping * @return a form definition */ def apply[T](mapping: Mapping[T]): Form[T] = Form(mapping, Map.empty, Nil, None)
第一引数のmapping
でフォームの入力要素と、実際どのクラスにマッピングされるのか定義します。
前者のcase classは、POSTリクエストなどで受け付けたbodyを解釈した結果を、PlayFramework側で解釈した結果を詰めるとき*2に、ライブラリが使用します。後者は定義だけ決めたフォームを生成するのに使います。普段は後者だけ気にしておけばよいです。
Mapping[T]
Mapping[T]
というのは、フォームの各入力項目の定義です。入力項目ごとに名前と制約を指定します。生成は、play.api.data.Forms
にあるmapping
メソッドを使います。22個ほど定義があります。引数の数が22個までという制約なので、対応メソッドが一つ一つ書かれています。
実体はObjectMapping1~22
というクラスになっています。
playframework/ObjectMappings.scala at master · playframework/playframework · GitHub
圧巻です。ObjectMapping
の各数値すべてMapping[T]
をextendsしています。これで画一的に扱えるようになっています。
Mapping[T]
の定義の面白いところは、Mapping自体をネストさせられる点だと思います。Formを生成する際の
val inquiryForm = Form( mapping( "name" -> nonEmptyText, "tel" -> nonEmptyText, "email" -> email, "reason" -> number )(InquiryData.apply)(InquiryData.unapply) )
というようなソースのMapping[T]
生成部分
mapping( "name" -> nonEmptyText, // 以下省略
ですが、name
の右辺nonEmptyText
自体もMapping[T]
です。正確にはこの場合Mapping[String]
となっています。インスタンス生成後の型イメージを書くと、
Form[InquiryData] - Mapping[InquiryData] - "name" -> Mapping[String] - "tel" -> Mapping[String] - "email" -> Mapping[String] - "reason" -> Mapping[Int]
となっています。
もうちょっと追ってみます。Mapping[String]
とはなんでしょうか?まずはnonEmptyText
の定義から。
val nonEmptyText: Mapping[String] = text.verifying(Constraints.nonEmpty)
先ほど書いた通り、Mapping[String]
を返します。text
は
val text: Mapping[String] = of[String]
であり、of[String]
は
def of[T](implicit binder: Formatter[T]): FieldMapping[T] = FieldMapping[T]()(binder)
です。Mapping[T]
は、具象クラスとしてObjectMapping
とFieldMapping
がある、ということが分かります。要するにCompositeパターン
です。
Formatter[T]
of[T]
のimplicit parameterであるbinder: Formatter[T]
ですが、見るからにフィールド型とMapping[T]
の変換用クラスを挿入しろ、という感じです。お手本のように綺麗なStrategyパターン
ですね*3。
もちろん、デフォルト変換は定義されています。
playframework/Format.scala at master · playframework/playframework · GitHub
ここに無いもの、例えば独自定義Enum
のようなものを変換したい場合は、フォームから参照できる場所にimplicitな関数を定義すれば挿入されます。また、デフォルトのimplicitを別のものに差し替えたい場合も同様に、独自定義すればよいです。
Form[T]やMapping[T]のT
型パラメータのT
ですが、これはcase class
を使っておくのが楽ですし、サンプルも大抵そうなっています。これはmapping
の最も簡単な定義が
def mapping[R, A1](a1: (String, Mapping[A1]))(apply: Function1[A1, R])(unapply: Function1[R, Option[(A1)]]): Mapping[R] = { new ObjectMapping1(apply, unapply, a1) }
となっており、case classで自動的に付与されるapply
とunapply
をそのまま渡せるためです。
フォームで入力するデータとControllerで使う型のフィールド定義をどうしても別にしたかったら、apply
とunapply
を独自定義して渡すこともできます。そしてメソッド名は何でもよいです。型が一致していればよく、case classである必要もありません。
Function1[A1, R]
:型A1
を引数で受け取って型R
を返す関数
Function1[R, Option[(A1)]]
:型R
を引数で受け取って型Option[(A1)]
を返す関数(Optionの中身はTuple)
apply
は、入力データ(Map
)を結果クラスに変換(クラスに束縛する、のほうがしっくりくる)する際に使用されます。unapply
は、クラスからMapへ変換(解放)する際に使用されます。
余談ですが、bind
の戻り値はEither[Seq[FormError], R]
です。束縛に失敗した場合、エラー原因を返すための定義です。unbind
の戻り値はMap[String, String]
で、unapply
がNone
を返した場合は空のMap
を返します。
おわりに
急に終わる感じですが…
本当は、テンプレート側でFormFieldを描画する部分も書きたかったのですが、分量が多くなりすぎるので止めました。フォームデータ変換部分、単純に書けるようによく考えられていました。すごい。
次回
この続きで、テンプレート側でFormFieldをどうやって描画しているのか書きたいと思っています。