1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

PlayFramework の Form で Custom Mapping を作る2つのパターン

Posted at

久しぶりに PlayFramework の Controller を実装するなどしてリクエストボディを Form を使ってパースするなどした。CustomMapping を作るパターンを2つ紹介する。

その1. Formatter を実装する

ブログでよく見かけるやつ。
リクエストボディが下記のようになっていたとする。

{ "name": "something string" }

これを下記にマッピングしたい。

case class Whoami(name: Name)
case class Name(value: String)

play.api.data.format.Formatter の型パラメータに
対象の ValueObject の型を指定し bindunbind を実装して、
暗黙の関数として定義する。

trait CustomFormatter {

  implicit def userFormat: Formatter[Name] = new Formatter[Name] {

    override def bind(key: String, data: Map[String, String]): Either[Seq[FormError], Name] = {
      Formats.stringFormat.bind(key, data).right.map(Name)
    }

    override def unbind(key: String, value: Name): Map[String, String] =
      Map(key -> value.value)
  }
}

使うときは Forms.of に型パラメータを与えることでよしなにやってくれる。
今回は↑で作った trait を継承して使う。

object SampleMapper extends CustomFormatter {

  val sampleForm = Form(
    mapping(
      "name" -> Forms.of[Name]
    )(Whoami.apply)(Whoami.unapply))
}

作成した FormController で使う。
SampleMapperimport して bindFromRequest() を呼び出す。

import com.google.inject.Inject
import javax.inject.Singleton
import play.api.data.Forms.mapping
import play.api.data.format.{Formats, Formatter}
import play.api.data.{Form, FormError, Forms}
import play.api.mvc.{AbstractController, ControllerComponents}

@Singleton
class FormSample @Inject()(cc: ControllerComponents) extends AbstractController(cc) {

  import SampleMapper._

  def post() = Action { implicit request =>
    sampleForm.bindFromRequest().fold(
      e => BadRequest(e.errors.toString), whoami => Ok(whoami.toString))
  }
}

基本的な使い方はこんな感じ。

その2. Form#transform を使う

Formatter の実装と同様に ValueObject をマッピングするだけならこっちのが楽ちん。先程の Form の定義を下記のようにするだけ。

型の指定は必須。

Form(
  mapping(
    "name" -> text.transform[Name](Name, _.value)
  )(Whoami.apply)(Whoami.unapply)
)

配列を受け取る場合

例えば下記のように、メールアドレスの配列を受け取ったとする。

{
  "name": "something string",
  "emails": [
    "a@qiita.com",
    "b@qiita.com",
    "c@qiita.com"
  ]
}

この場合は組み込みのコレクションマッパーのコンストラクタに指定。

val sampleForm = Form(
  mapping(
    "name" -> text.transform[Name](Name, _.value),
    "emails" -> seq(text.transform[Email](Email, _.value))
  )(Whoami.apply)(Whoami.unapply))

case class Whoami(name: Name, emails: Seq[Email])
case class Name(value: String)
case class Email(value: String)

更に emails を ファーストクラスコレクションとする場合は下記のようになる。

val sampleForm = Form(
  mapping(
    "name" -> text.transform[Name](Name, _.value),
    "emails" -> seq(text.transform[Email](Email, _.value))
      .transform[Emails](Emails, _.values)
  )(Whoami.apply)(Whoami.unapply))

case class Whoami(name: Name, emails: Emails)
case class Name(value: String)
case class Email(value: String)
case class Emails(values: Seq[Email])

所感

CustomMapping は transform の利用でほぼこと足りた。
Formatter を実装するほうが嬉しいときってどんなときだろ?

1
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?