2012年5月17日木曜日

Play framework 2.0のフォームの制御部分意訳

Play framework2.0のHandling form submissionを意訳しました。

ざっと概要を言うと、ブラウザから送られてくるフォームのデータを、どうやってScalaの値にマッピングするのか、ということです。
マッピングの前に、どうやって値を取得するんだ?という疑問もありますが、あまり深く考えずに取得できるようになっています。
言い方を変えると、マッピングする部分を書けば、後はPlayがうまいこと値をrequestから引っ張ってきてくれるということです。

ただ、Play frameworkらしからぬ感じなのですが、具体的なサンプルが少なくて、ちょっとマニュアルが分かりづらかったです。(私の諸々のスキルが低いというのもありますが)
なので、途中でサンプルソースを載せました。(背景が紫色の部分)

例のごとく、もし私の訳が間違っていても責任は負えませんのであしからず・・・
正確なものを読みたい方は原典をどうぞ。

http://www.playframework.org/documentation/2.0.1/ScalaForms

なお、日本Playframeworkユーザー会さんの方で翻訳作業が進行中のようですので、そちらを待たれるという手もあります。

それではどうぞ!

フォームの定義

play.api.dataパッケージは、HTTPフォームのデータを扱い、バリデーションも扱うヘルパーが含まれています。
フォームを扱う最も簡単な方法は、play.api.data.Form構造を定義することです。

import play.api.data._
import play.api.data.Forms._

val loginForm = Form(
  tuple(
    "email" -> text,
    "password" -> text
  )
)

以下のフォームは、結果としてMap[String,String]から、(String, String)の値を生成します。

val anyData = Map("email" -> "bob@gmail.com", "password" -> "secret")
val (user, password) = loginForm.bind(anyData).get

もし、requestスコープの中のソースをコーディングしている場合であれば、直接requestの中身をバインドすることが出来ます。

val (user, password) = loginForm.bindFromRequest.get

複雑なオブジェクトの構築

formは値を構築・分解する機能を使用することが出来ます。
例えば、既存のケースクラスにラップすることが出来ます。

import play.api.data._
import play.api.data.Forms._

case class User(name: String, age: Int)

val userForm = Form(
  mapping(
    "name" -> text,
    "age" -> number
  )(User.apply)(User.unapply)
)

val anyData = Map("name" -> "bob", "age" -> "18")
val user: User = userForm.bind(anyData).get

Note:tupleとmappingを使用することによる違いは、tupleを使用する場合、constructionとdeconstructionを定義する必要が無いということです。
(だって私たちはtupleの構築と分解方法を知っていますよね?)

mapping関数は、あなたに、あなたのカスタマイズされた関数を定義させます。
ケースクラスを構築したり分解したりしたい場合はデフォルトのapplyメソッドとunapplyメソッドを使用するだけです。

※注意(公式にない私的メモ)
ここの訳は良く分からない。
construction and deconstruction functionsをいったいなんと訳せばいいのか・・・

もちろん、Formシグネチャがケースクラスに正確に一致しないことも良くあります。
例として、利用規約に同意するために使われるチェックボックスを含んだformを使ってみましょう。
Userに値を与える必要はありません。
これはフォームのバリデーションに一度使用されますが、役に立つ情報を教えてくれないダミーのフィールドです。
私たちは自分自身の構築・分解のための機能を定義できるので、これを扱うのはとても簡単です。

val userForm = Form(
  mapping(
    "name" -> text,
    "age" -> number,
    "accept" -> checked("Please accept the terms and conditions")
  )((name, age, _) => User(name, age))
   ((user: User) => Some((user.name, user.age, false))
)

Note:formに既存のUserを代入したとき、deconstructionが使用されています。
これは、Userをデータベースからロードして、それを更新するためにフォームを用意する場合に便利です。

制約の定義

それぞれのmappingで、バインディングの過程で値をチェックする、追加のバリデーションの制約を定義することが出来ます。

import play.api.data._
import play.api.data.Forms._
import play.api.data.validation.Constraints._

case class User(name: String, age: Int)

val userForm = Form(
  mapping(
    "name" -> text.verifying(required),
    "age" -> number.verifying(min(0), max(100))
  )(User.apply)(User.unapply)
)

Note:こういう書き方もできます

mapping(
  "name" -> nonEmptyText,
  "age" -> number(min=0, max=100)
)

このconstructsは上の例と全く同じ制約になります。

また、フィールドにアドホックな制約を定義することもできます。

val loginForm = Form(
  tuple(
    "email" -> nonEmptyText,
    "password" -> text
  ) verifying("Invalid user name or password", fields => fields match { 
      case (e, p) => User.authenticate(e,p).isDefined 
  })
)

バインディングのエラーを制御する

制約を定義した場合には、バインディング時のエラーを制御できるようにする必要があります。
以下のように、foldを使うことが出来ます。

loginForm.bindFromRequest.fold(
  formWithErrors => // binding failure, you retrieve the form containing errors,
  value => // binding success, you get the actual value 
)

※注意(公式にない私的メモ)
実際のサンプルはこんな感じ

def debug = Action { implicit request =>
    // ************************************************************
    // フォームの定義
    // ************************************************************
    // nonEmptyTextやnumberといったプロパティ(とメソッド)はplay.api.data.Formsクラスの中
    val myForm = Form(
        tuple(
            "val1" -> nonEmptyText,
            "val2" -> number
        )
    )
    
    
    // ************************************************************
    // request(POSTされたデータ)を取得する(2パターン)
    // ************************************************************
    /**
     * 最もシンプルなフォームの値の取得方法
     * フォームの定義で 制約(constraints)を指定している値で、実際のフォームに値が
     * 入力されていない場合、この取得方法だと以下のエラーが発生する
     * [NoSuchElementException: None.get]
     * フォームの定義で制約を付けている場合は、必ずfoldを使ってエラーハンドリングをすること!
     * 値が入力されている場合は正常に動いてしまうので、正常系のテストでは問題なく
     * 動作してしまう為、注意すること。
     */
    val(val1, val2) = myForm.bindFromRequest.get
    Ok("bindFormRequest.get" + " -> " + (val1 + 1) + " : " +  (val2 + 1))
    
    
    /**
     * フォームの定義に制約を指定している場合は、foldを使って必ずエラーハンドリングするように!
     * 問題ない場合はタプルとして値が取得でき、型の制約も特に問題なく動作する
     */
    myForm.bindFromRequest.fold(
        errors => Ok("NOOOOO"),
        value => Ok("bindFormRequest.fold" + " -> " + (value._1 + 1) + " : " + (value._2 + 1))
    )
}


formをデフォルト値で初期化する

時々データを編集するために、formに既存のデータを代入したいこともあるでしょう。その場合は以下のようにします。

val filledForm = userForm.fill(User("Bob", 18))

ネストされた値

formのmappingでは、ネストされた値を定義することもできます。

case class User(name: String, address: Address)
case class Address(street: String, city: String)

val userForm = Form(
  mapping(
    "name" -> text,
    "address" -> mapping(
        "street" -> text,
        "city" -> text
    )(Address.apply)(Address.unapply)
  )(User.apply, User.unapply)
)

この方法でネストされたデータを使用する場合、ブラウザから送られるフォームの値は「address.street, address.city, etc」の様な命名にしなければなりません。

繰り返される値

formのmappingでは、繰り返される値(同名の複数の値)を定義することもできます。

case class User(name: String, emails: List[String])

val userForm = Form(
  mapping(
    "name" -> text,
    "emails" -> list(text)
  )(User.apply, User.unapply)
)

このような繰り返される値を使用する場合、ブラウザから送られるフォームの値は「emails[0], emails[1], emails[2], etc」のような命名にしなければなりません。

Optionな値

formのmappingでは、Optionな値を定義することもできます。

case class User(name: String, email: Option[String])

val userForm = Form(
  mapping(
    "name" -> text,
    "email" -> optional(text)
  )(User.apply, User.unapply)
)

Note:emailフィールドは、requestの中に存在しない場合と、ブランク値が入っている場合には、Noneがセットされて無視されます。

無視する値

送られてきた値を無視して、フィールドに静的な値を持たせたい場合は以下のようにします。

case class User(id: Long, name: String, email: Option[String])

val userForm = Form(
  mapping(
    "id" -> ignored(1234),
    "name" -> text,
    "email" -> optional(text)
  )(User.apply, User.unapply)
)


あなたは今、Optionな値、ネストされた値、同名の複数の値を混在させて、複雑なformを作成することが出来ます。


以上で終了です。

0 件のコメント: