Documentation

You are viewing the documentation for the 2.1.5 release in the 2.1.x series of releases. The latest stable release series is 2.4.x.

§アクション合成

この章では、アクションの一部機能を汎用的な形で切り出して定義する方法を紹介していきます。

§アクション合成の基本

アクションの呼び出しをロギングする、簡単なロギングデコーレータの例から始めてみましょう。

一つめの方法は、アクション自体を定義する代わりに、アクションを生成するためのヘルパーメソッドを提供することです。

def LoggingAction(f: Request[AnyContent] => Result): Action[AnyContent] = {
  Action { request =>
    Logger.info("Calling action")
    f(request)
  }
}

このヘルパーメソッドは次のように利用できます。

def index = LoggingAction { request =>
  Ok("Hello World")
}

この方法はとても簡単ですが、ボディパーサーを外部から指定する方法がないため、デフォルトの parse.anyContent ボディパーサーしか利用できません。そこで、次のようなヘルパーメソッドも定義してみます。

def LoggingAction[A](bp: BodyParser[A])(f: Request[A] => Result): Action[A] = {
  Action(bp) { request =>
    Logger.info("Calling action")
    f(request)
  }
}

これは次のように利用できます。

def index = LoggingAction(parse.text) { request =>
  Ok("Hello World")
}

§既存のアクションをラップする

その他に、LoggingActionAction のラッパーとして定義する方法もあります。

case class Logging[A](action: Action[A]) extends Action[A] {
  
  def apply(request: Request[A]): Result = {
    Logger.info("Calling action")
    action(request)
  }
  
  lazy val parser = action.parser
}

これを利用すると、他のアクション値をラップすることができます。

def index = Logging { 
  Action { 
    Ok("Hello World")
  }
}

この方法ではラップされたアクションのボディパーサーがそのまま再利用されるため、次のような記述が可能です。

def index = Logging { 
  Action(parse.text) { 
    Ok("Hello World")
  }
}

次のように、Logging クラスを定義せずに全く同じ機能を持つラッパーを記述することもできます。

def Logging[A](action: Action[A]): Action[A] = {
  Action(action.parser) { request =>
    Logger.info("Calling action")
    action(request)
  }
} 

以下は既存のアクションをラップしてセッション変数を追加する例です。

def addSessionVar[A](action: Action[A]) = Action(action.parser) { request =>
  action(request).withSession("foo" -> "bar")
}

§さらに複雑な例

次は、認証を伴うアクションという、より複雑ですが一般的な例を見てみましょう。ここでの問題は、認証されたユーザだけをラップしたアクションへ通すことです。

def Authenticated(action: User => EssentialAction): EssentialAction = {
  
  // Let's define a helper function to retrieve a User
  def getUser(request: RequestHeader): Option[User] = {
    request.session.get("user").flatMap(u => User.find(u))
  }
  
  // Now let's define the new Action
  EssentialAction { request =>
    getUser(request).map(u => action(u)(request)).getOrElse {
      Done(Unauthorized)
    }
  }
  
}

このヘルパーメソッドは次のように利用することができます。

def index = Authenticated { user =>
  Action { request =>
    Ok("Hello " + user.name)
  }
}

注: play.api.mvc.Security.Authenticated にはここで説明した例よりもっと汎用的な Authenticated アクションの実装が用意されています。

前回のセクション では Action[A]Request[A] => Result の関数だとされていましたが、完全には正しくありません。実際には Action[A] トレイトは以下のように定義されています。

trait EssentialAction extends (RequestHeader => Iteratee[Array[Byte], Result])

trait Action[A] extends EssentialAction {
  def parser: BodyParser[A]
  def apply(request: Request[A]): Result
  def apply(headers: RequestHeader): Iteratee[Array[Byte], Result] = …
}

EssentialAction は、リクエストヘッダを受け取り、最終的にリクエストボディをパースして HTTP の結果を生成する Iteratee を返す関数です。 Action[A]EssentialAction を次のように実装します: ボディパーサを使ってリクエストボディをパースし、生成された Request[A] オブジェクトをアクションのコードに渡し、アクションのコードの結果を返します。Action[A]apply(request: Request[A]): Result メソッドを持っているため、依然として Request[A] => Result の関数だと考えることができます。

EssentialAction トレイトは、リクエストボディをパースする前にリクエストヘッダ情報の取得を行う必要があるコードを、アクションと合成する時に有効です。

上記の Authenticated の実装は、リクエストセッションからユーザ ID を探し、見つかった場合はラップされたアクションをこのユーザーと共に呼び出し、そうでない場合は 401 UNAUTHORIZED ステータスをリクエストボディをパースせずに返しています。

§認証されたアクションの別の実装方法

次は、先ほどの例を、アクションを丸ごとラップせずに記述してみましょう。

def Authenticated(f: (User, Request[AnyContent]) => Result) = {
  Action { request =>
    val result = for {
      id <- request.session.get("user")
      user <- User.find(id)
    } yield f(user, request)
    result getOrElse Unauthorized
  }
}

これは次のように利用します。

def index = Authenticated { (user, request) =>
   Ok("Hello " + user.name)
}

ここでの問題は、もう request という引数を implicit 指定することはできない、ということです。これはカリー化を使うと解決できます。

def Authenticated(f: User => Request[AnyContent] => Result) = {
  Action { request =>
    val result = for {
      id <- request.session.get("user")
      user <- User.find(id)
    } yield f(user)(request)
    result getOrElse Unauthorized
  }
}

これは次のように利用することができます。

def index = Authenticated { user => implicit request =>
   Ok("Hello " + user.name)
}

別の (たぶんより簡単な) 方法は、Request のサブクラス AuthenticatedRequest を定義することです (二つの引数をひとつにまとめる、とも言い換えられます) 。

case class AuthenticatedRequest(
  user: User, private val request: Request[AnyContent]
) extends WrappedRequest(request)

def Authenticated(f: AuthenticatedRequest => Result) = {
  Action { request =>
    val result = for {
      id <- request.session.get("user")
      user <- User.find(id)
    } yield f(AuthenticatedRequest(user, request))
    result getOrElse Unauthorized
  }
}

これを利用すると、次のような記述ができます。

def index = Authenticated { implicit request =>
   Ok("Hello " + request.user.name)
}

ボディパーサーを指定できるようにすると、この実装はもっと一般的な形に拡張することができます。

case class AuthenticatedRequest[A](
  user: User, private val request: Request[A]
) extends WrappedRequest(request)

def Authenticated[A](p: BodyParser[A])(f: AuthenticatedRequest[A] => Result) = {
  Action(p) { request =>
    val result = for {
      id <- request.session.get("user")
      user <- User.find(id)
    } yield f(Authenticated(user, request))
    result getOrElse Unauthorized
  }
}

// Overloaded method to use the default body parser
import play.api.mvc.BodyParsers._
def Authenticated(f: AuthenticatedRequest[AnyContent] => Result): Action[AnyContent]  = {
  Authenticated(parse.anyContent)(f)
}

Play は、アプリケーション全体に渡る横断的な関心事を扱うのに便利な グローバルフィルター API も提供しています。

次ページ: コンテンツネゴシエーション