§アクション合成
この章では、アクションの一部機能を汎用的な形で切り出して定義する方法を紹介していきます。
§独自のアクションビルダー
以前のページ で、 リクエストパラメータを使用する、リクエストパラメータを使用しない、ボディパーサーを使用するなど アクションを宣言する複数の方法を見てきました。 実際のところ、非同期プログラミング の章にもあるとおり、ほかにも方法があります。
アクションを構築するためのこれらのメソッドは、実際は ActionBuilder
と呼ばれるトレイトに全て定義されています。 また、 これまでに定義してきた Action
オブジェクトはこのトレイトの単なるインスタンスです。 自身の ActionBuilder
を実装することにより、 アクションを構築するために使用可能な、再利用可能なアクションスタックを宣言できます。
アクションの呼び出しをロギングする、簡単なロギングデコレーターの例から始めてみましょう。
1つめの方法は、 ActionBuilder
から構築された全てのアクションから呼び出される invokeBlock
メソッドに、この機能を実装することです。
import play.api.mvc._
object LoggingAction extends ActionBuilder[Request] {
def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[SimpleResult]) = {
Logger.info("Calling action")
block(request)
}
}
ここで実装したものは、 Action
を使用するのと同じ方法で使用することができます。
def index = LoggingAction {
Ok("Hello World")
}
ActionBuilder
はアクションを構築するための全種類のメソッドを提供するので、例えば、独自のボディパーサーを宣言しても実行できます。
def submit = LoggingAction(parse.text) { request =>
Ok("Got a body " + request.body.length + " bytes long")
}
§アクション合成
ほとんどのアプリケーションでは、 様々な種類の認証を行いたい、様々な種類の一般的な機能を提供したいなど、複数のアクションビルダーが必要となるでしょう。 このような場合、それぞれの種類に応じたアクションビルダーに対するロギングアクションのコードを書き直すのではなく、再利用が可能な方法で定義したくなります。
アクションをラッピングして実装することで、再利用が可能なアクションのコードを書くことができます。
import play.api.mvc._
case class Logging[A](action: Action[A]) extends Action[A] {
def apply(request: Request[A]): Future[SimpleResult] = {
Logger.info("Calling action")
action(request)
}
lazy val parser = action.parser
}
また、アクションクラスを定義しないでアクションを構成するのに、 Action
をアクションビルダーとして使用することもできます。
import play.api.mvc._
def logging[A](action: Action[A])= Action.async(action.parser) { request =>
Logger.info("Calling action")
action(request)
}
アクションは composeAction
メソッドを使ってアクションビルダーに合成されます。
object LoggingAction extends ActionBuilder[Request] {
def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[SimpleResult]) = {
block(request)
}
override def composeAction[A](action: Action[A]) = new Logging(action)
}
これで、ビルダーを先ほどと同じ方法で使用することができます。
def index = LoggingAction {
Ok("Hello World")
}
また、アクションビルダーを用いず、アクションにラッピングすることでも合成が可能です。
def index = Logging {
Action {
Ok("Hello World")
}
}
§より複雑なアクション
ここまで見てきたアクションは、リクエストについて全く影響を与えていません。 もちろん、入力されてくるリクエストオブジェクトに対して、読み込みと変更も可能です。
import play.api.mvc._
def xForwardedFor[A](action: Action[A]) = Action.async(action.parser) { request =>
val newRequest = request.headers.get("X-Forwarded-For").map { xff =>
new WrappedRequest[A](request) {
override def remoteAddress = xff
}
} getOrElse request
action(newRequest)
}
Note: Play は X-Forwarded-For ヘッダを組み込みですでにサポートしています。
リクエストをブロックすることも可能です。
import play.api.mvc._
def onlyHttps[A](action: Action[A]) = Action.async(action.parser) { request =>
request.headers.get("X-Forwarded-Proto").collect {
case "https" => action(request)
} getOrElse {
Future.successful(Forbidden("Only HTTPS requests allowed"))
}
}
そして最後に、 戻り値の結果を変更することも可能です。
import play.api.mvc._
import play.api.libs.concurrent.Execution.Implicits._
def addUaHeader[A](action: Action[A]) = Action.async(action.parser) { request =>
action(request).map(_.withHeaders("X-UA-Compatible" -> "Chrome=1"))
}
§様々なリクエスト型
ActionBuilder
トレイトは、様々なリクエスト型を用いてアクションを構築することをできるようにするために、パラメータ化されています。 invokeBlock
メソッドは入力リクエストを、自身が望む型に変換することができます。 これは多くの点で有用で、 例えば、認証においてユーザーオブジェクトを現在のリクエストに紐付ける、 データベースからオブジェクトを読込むロジックを共有するなどが考えられます。
Item
型というオブジェクトについて動作する REST API について考えてみましょう。 /item/:itemId
というパスの下に多くのルートがあり、それらのルートでは Item を見つける必要があるとします。 また、同じ認可属性を共有します。 このような場合、 このロジックをアクションビルダーの中に入れてしまうと便利です。
まずはじめに、 Item
を追加したリクエストオブジェクトを作成します。
import play.api.mvc._
class RequestWithItem[A](val item: Item, request: Request[A]) extends WrappedRequest[A](request)
そして、 リクエストが作られたときに Item を見つけるアクションビルダーを作成します。 このアクションビルダーが Item の id を引数に取るメソッドの内部に定義されていることに注意してください。
def ItemAction(itemId: String) = new ActionBuilder[RequestWithItem] {
def invokeBlock[A](request: Request[A], block: (RequestWithItem[A]) => Future[SimpleResult]) = {
ItemDao.findById(itemId).map { item =>
block(new RequestWithItem(item, request))
} getOrElse {
Future.successful(NotFound)
}
}
}
これで、このアクションビルダーを各 Item に対して使えるようになりました。
def tagItem(itemId: String, tag: String) = ItemAction(itemId) { request =>
request.item.addTag(tag)
Ok
}
§認証
アクション合成のユースケースの中で最も一般的なもののひとつは、認証です。 認証を行うアクションビルダーは以下のようにして簡単に実装できます。
import play.api.mvc._
class AuthenticatedRequest[A](val username: String, request: Request[A]) extends WrappedRequest[A](request)
object Authenticated extends ActionBuilder[AuthenticatedRequest] {
def invokeBlock[A](request: Request[A], block: (AuthenticatedRequest[A]) => Future[SimpleResult]) = {
request.session.get("username").map { username =>
block(new AuthenticatedRequest(username, request))
} getOrElse {
Future.successful(Forbidden)
}
}
}
def currentUser = Authenticated { request =>
Ok("The current user is " + request.username)
}
Play はまた、組み込みの認証アクションビルダーを提供しています。 詳細と使用方法については こちら を参照してください。
Note: 組み込みの認証アクションビルダーは、単純な場合の認証に対して必要なコード量を少なくするための便利なヘルパーで、その実装は上記の例に非常に良く似ています。
もし 組み込みの認証アクションビルダーで適応できることよりもさらに複雑な要求がある場合は、 簡潔ではなくりますが自分で実装することを推奨します。
Play はまた、 グローバルな横断的関心事に対して有用な グローバルフィルター API を提供しています。
次ページ: コンテンツネゴシエーション