Play FrameworkのFormの値のカスタムMappingを作る
nextbeatのエンジニアの國見です。このエントリではPlay FrameworkのFormに関する話をしようと思います。
Play Frameworkには、入力フォームを抽象化したFormというクラスがあります。これは基本的にはHTMLのフォームとドメインモデルをマッピングするために使われます。また、HTMLのフォームだけでなく、POSTで受け取るjsonやGETで受け取るURLパラメータも対応しており、同じFormというクラスで抽象化して扱うことができます。
Formの詳細については本家のドキュメントを参照するのが良いと思います。ここで結構詳しく説明が行われているんですが、1つ個人的にやりたいけど説明されていないものがありました。
それはtext
やnumber
のような、オブジェクトのマッピングではなく値のマッピングを自前で作るやり方です。
言葉だけでは伝わりにくいと思うので、例を書きますと、例えばモデルのIDを直接Int
型ではなく、UserId
のような専用のクラスを作るやり方は一般的に行われていると思いますが、問題はこのUserId
のマッピングを作りたい時です。おそらく、ドキュメントに書いてある情報を用いてUserId
を受け取る実装を書くと以下のようになるんじゃないでしょうか。
import play.api.data._ import play.api.data.Forms._ import play.api.mvc.Action case class UserId(value: Int) def hoge = Action { implicit request => val params = single("id" -> number) Form(params).fold( _ => BadRequest, id => { val userId = UserId(id) // ここがめんどくさい // 必要な処理 Ok } ) }
ここで問題なのは、id
の値をInt
として受け取って、手動でUserId
に変換しているところです。おそらくUserId
を受け取る処理は何度も書くことになるでしょう。毎回Int
をUserId
に変換する処理を書くのは面倒です。
そこで、直接これをUserId
に変換するマッピングがかけないでしょうか。つまり以下のようなコーディングがしたいわけです。
import play.api.data._ import play.api.data.Forms._ import play.api.mvc.Action case class UserId(value: Int) def hoge = Action { implicit request => val params = single("id" -> userIdMapping) // 直接UserIdに変換するこのuserIdMappingが欲しい。 Form(params).fold( _ => BadRequest, userId => { // 必要な処理 Ok } ) }
このエントリではこのuserIdMapping
を作る方法を紹介します。
では、それの実現方法を調査するために、デフォルトで用意されているtext
の実装を見てみましょう。
/** * Constructs a simple mapping for a text field. * * For example: * {{{ * Form("username" -> text) * }}} */ val text: Mapping[String] = of[String]
非常に短いですね、このof
とは何でしょう。
/** * Creates a Mapping of type `T`. * * For example: * {{{ * Form("email" -> of[String]) * }}} * * @tparam T the mapping type * @return a mapping for a simple field */ def of[T](implicit binder: Formatter[T]): FieldMapping[T] = FieldMapping[T]()(binder)
implicitなFormatter[T]
(今回の場合はT
=String
)を受け取って処理を行っているようです。つまり、userIdMapping
を作るためにはFormatter[UserId]
を作り、val userIdMapping = of[UserId]
とすれば作れるようです。
次にFormatterの実装を見てみましょう。
/** * Default formatter for the `String` type. */ implicit def stringFormat: Formatter[String] = new Formatter[String] { def bind(key: String, data: Map[String, String]) = data.get(key).toRight(Seq(FormError(key, "error.required", Nil))) def unbind(key: String, value: String) = Map(key -> value) }
どうやらこのbind
とunbind
を実装してFormatter[UserId]
を作れば良いようです。
Formatter
のインタフェースを見てみると
trait Formatter[T] { /** * The expected format of `Any`. */ val format: Option[(String, Seq[Any])] = None /** * Binds this field, i.e. constructs a concrete value from submitted data. * * @param key the field key * @param data the submitted data * @return Either a concrete value of type T or a set of error if the binding failed. */ def bind(key: String, data: Map[String, String]): Either[Seq[FormError], T] /** * Unbinds this field, i.e. transforms a concrete value to plain data. * * @param key the field ke * @param value the value to unbind * @return either the plain data or a set of errors if unbinding failed */ def unbind(key: String, value: T): Map[String, String] }
このbind
とunbind
を実装すればいいわけですね。
unbind
はForm
を作ってhtmlを返すときに、Form
とhtmlのフォームのマッピングを行うときに用いられるもので、要はその値の文字列表現を返せば良いです。
bind
が受け取ったデータを変換する肝になるメソッドで、key
がこれから取得する値のキー、data
が送られてきたデータになります。それを受け取りEither[Seq[FormError], UserId]
を返すようにすれば、Formatter[UserId]
が出来上がるわけです。
では実装して見ましょう。
import play.api.data.format.{ Formats, Formatter } implicit val userIdFormatter = new Formatter[UserId] { def bind(key: String, data: Map[String, String]): Either[Seq[FormError], UserId] = Formats.intFormat.bind(key, data).right.map(UserId.apply) def unbind(key: String, value: UserId): Map[String, String] = Map(key -> value.value.toString) }
内容を説明すると、送られてきたデータから一旦Int
で値を取り出し、それをUserId
に変換しています。
これで準備が整ったので、最初のコントローラのメソッドを実装して見ましょう。
import play.api.data._ import play.api.data.Forms._ import play.api.mvc.Action import play.api.data.format.{ Formats, Formatter } case class UserId(value: Int) implicit val userIdFormatter = new Formatter[UserId] { def bind(key: String, data: Map[String, String]): Either[Seq[FormError], UserId] = Formats.intFormat.bind(key, data).right.map(UserId.apply) def unbind(key: String, value: UserId): Map[String, String] = Map(key -> value.value.toString) } val userIdMapping = of[UserId] def hoge = Action { implicit request => val params = single("id" -> userIdMapping) // 動く! Form(params).fold( _ => BadRequest, userId => { // 必要な処理 Ok } ) }
実際のコーディングではuserIdFormatter
とuserIdMapping
をベースのコントローラtraitなどの適切な場所に移動する必要があるでしょう。それで完了です。
このエントリではPlay FrameworkでFormの値のMappingを作る方法を紹介しました。ぐぐってもすぐやり方が見つからなかったので、次に同じようなことをやろうとした誰かの助けになれば幸いです。 nextbeatではドキュメントにないものでもソースコードを読んで自分の要件を実装できるエンジニアを募集しています。