§アクションの合成
この章では、汎用的なアクション機能を定義するいくつかの方法について説明します。
§カスタムアクションビルダー
以前に見たように、アクションを宣言するには複数の方法があります。リクエストパラメーターを使用する場合と使用しない場合、ボディパーサーを使用する場合などです。非同期プログラミングに関する章で説明するように、実際には他にも多くの方法があります。
これらのアクション構築メソッドは、実際にはすべてActionBuilder
というトレイトによって定義されており、アクションを宣言するために使用するAction
オブジェクトはこのトレイトのインスタンスにすぎません。独自のActionBuilder
を実装することで、再利用可能なアクションスタックを宣言し、それを使用してアクションを構築できます。
簡単な例として、ログデコレーターから始めましょう。このアクションへの各呼び出しをログに記録したいと思います。
最初の方法は、ActionBuilder
によって構築されたすべてのアクションに対して呼び出されるinvokeBlock
メソッドにこの機能を実装することです。
import play.api.mvc._
class LoggingAction @Inject() (parser: BodyParsers.Default)(implicit ec: ExecutionContext)
extends ActionBuilderImpl(parser)
with Logging {
override def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = {
logger.info("Calling action")
block(request)
}
}
これで、コントローラーで依存性注入を使用してLoggingAction
のインスタンスを取得し、Action
を使用する場合と同じ方法で使用できます。
class MyController @Inject() (loggingAction: LoggingAction, cc: ControllerComponents)
extends AbstractController(cc) {
def index = loggingAction {
Ok("Hello World")
}
}
ActionBuilder
はアクションを構築するさまざまなメソッドを提供するため、これはたとえば、カスタムボディパーサーの宣言にも機能します。
def submit: Action[String] = 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] with play.api.Logging {
def apply(request: Request[A]): Future[Result] = {
logger.info("Calling action")
action(request)
}
override def parser = action.parser
override def executionContext = action.executionContext
}
独自の action クラスを定義せずに、Action
アクションビルダーを使用してアクションを構築することもできます。
import play.api.mvc._
def logging[A](action: Action[A]) = Action.async(action.parser) { request =>
logger.info("Calling action")
action(request)
}
composeAction
メソッドを使用して、アクションをアクションビルダーにミックスインできます。
class LoggingAction @Inject() (parser: BodyParsers.Default)(implicit ec: ExecutionContext)
extends ActionBuilderImpl(parser) {
override def invokeBlock[A](request: Request[A], block: (Request[A]) => Future[Result]) = {
block(request)
}
override def composeAction[A](action: Action[A]): Logging[A] = new Logging(action)
}
これで、ビルダーを以前と同じように使用できます。
def index = loggingAction {
Ok("Hello World")
}
アクションビルダーを使用せずに、ラップアクションをミックスインすることもできます。
def index = Logging {
Action {
Ok("Hello World")
}
}
§より複雑なアクション
これまで、リクエストにまったく影響を与えないアクションのみを示してきました。もちろん、着信リクエストオブジェクトを読み取り、変更することもできます。
import play.api.mvc._
import play.api.mvc.request.RemoteConnection
def xForwardedFor[A](action: Action[A]) = Action.async(action.parser) { request =>
val newRequest = request.headers.get("X-Forwarded-For") match {
case None => request
case Some(xff) =>
val xffConnection = RemoteConnection(xff, request.connection.secure, None)
request.withConnection(xffConnection)
}
action(newRequest)
}
注: Play は既に
X-Forwarded-For
ヘッダーをサポートしています。
リクエストをブロックすることもできます。
import play.api.mvc._
import play.api.mvc.Results._
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._
def addUaHeader[A](action: Action[A]) = Action.async(action.parser) { request =>
action(request).map(_.withHeaders("X-UA-Compatible" -> "Chrome=1"))
}
§異なるリクエストタイプ
アクションの合成により、HTTP リクエストとレスポンスレベルで追加の処理を実行できますが、多くの場合、リクエスト自体にコンテキストを追加したり、リクエスト自体を検証したりするデータ変換のパイプラインを構築したい場合があります。ActionFunction
は、入力リクエストタイプと次のレイヤーに渡される出力タイプの両方でパラメーター化された、リクエスト上の関数と考えることができます。各アクション関数は、認証、オブジェクトのデータベース検索、権限チェック、またはアクション全体で合成および再利用する他の操作などのモジュール式処理を表す場合があります。
さまざまな種類の処理に役立つ、ActionFunction
を実装するいくつかの事前に定義されたトレイトがあります。
ActionTransformer
は、追加の情報を追加するなどして、リクエストを変更できます。ActionFilter
は、リクエスト値を変更せずにエラーを生成するなどして、リクエストを選択的にインターセプトできます。ActionRefiner
は、上記の両方の一般的なケースです。ActionBuilder
は、入力をRequest
として取る関数の特殊なケースであり、したがってアクションを構築できます。
invokeBlock
メソッドを実装することで、独自の任意のActionFunction
を定義することもできます。多くの場合、入力と出力のタイプをRequest
のインスタンス(WrappedRequest
を使用)にする方が便利です。ただし、これは厳密に必要ではありません。
§認証
アクション関数の最も一般的なユースケースの1つは認証です。元のリクエストからユーザーを判別し、新しいUserRequest
に追加する独自の認証アクショントランスフォーマーを簡単に実装できます。これは、単純なRequest
を入力として取るため、ActionBuilder
でもあります。
import play.api.mvc._
class UserRequest[A](val username: Option[String], request: Request[A]) extends WrappedRequest[A](request)
class UserAction @Inject() (val parser: BodyParsers.Default)(implicit val executionContext: ExecutionContext)
extends ActionBuilder[UserRequest, AnyContent]
with ActionTransformer[Request, UserRequest] {
def transform[A](request: Request[A]) = Future.successful {
new UserRequest(request.session.get("username"), request)
}
}
Play は、組み込みの認証アクションビルダーも提供しています。これに関する情報とその使用方法については、こちらを参照してください。
注: 組み込みの認証アクションビルダーは、単純なケースの認証に必要なコードを最小限に抑えるための便利なヘルパーにすぎません。その実装は上記の例と非常に似ています。
独自の認証ヘルパーを簡単に記述できるため、組み込みのヘルパーがニーズに合わない場合は、独自のヘルパーを記述することをお勧めします。
§リクエストへの情報の追加
では、タイプItem
のオブジェクトを操作する REST API を検討してみましょう。/item/:itemId
パスには多くのルートがあり、これらのルートごとにアイテムを検索する必要があります。この場合、このロジックをアクション関数に入れると便利です。
まず、UserRequest
にItem
を追加するリクエストオブジェクトを作成します。
import play.api.mvc._
class ItemRequest[A](val item: Item, request: UserRequest[A]) extends WrappedRequest[A](request) {
def username = request.username
}
次に、そのアイテムを検索して、エラー(Left
)または新しいItemRequest
(Right
)を返すアクションリファイナーを作成します。このアクションリファイナーは、アイテムのIDを受け取るメソッド内で定義されていることに注意してください。
def ItemAction(itemId: String)(implicit ec: ExecutionContext) = new ActionRefiner[UserRequest, ItemRequest] {
def executionContext = ec
def refine[A](input: UserRequest[A]): Future[Either[Status, ItemRequest[A]]] = Future.successful {
ItemDao
.findById(itemId)
.map(new ItemRequest(_, input))
.toRight(NotFound)
}
}
§リクエストの検証
最後に、リクエストを続行するかどうかを検証するアクション関数が必要になる場合があります。たとえば、UserAction
のユーザーがItemAction
のアイテムにアクセスできる権限を持っているかどうかを確認し、権限がない場合はエラーを返す必要があるかもしれません。
def PermissionCheckAction(implicit ec: ExecutionContext) = new ActionFilter[ItemRequest] {
def executionContext = ec
def filter[A](input: ItemRequest[A]) = Future.successful {
if (!input.item.accessibleByUser(input.username))
Some(Forbidden)
else
None
}
}
§すべてを組み合わせる
これで、andThen
を使用してこれらのアクション関数を連結し(ActionBuilder
から開始)、アクションを作成できます。
def tagItem(itemId: String, tag: String)(implicit ec: ExecutionContext): Action[AnyContent] =
userAction.andThen(ItemAction(itemId)).andThen(PermissionCheckAction) { request =>
request.item.addTag(tag)
Ok("User " + request.username + " tagged " + request.item.id)
}
Play はグローバルフィルターAPIも提供しており、グローバルなクロスカットに関する懸念事項に役立ちます。
§ボディパーシングとの相互作用におけるアクションの合成
デフォルトでは、ボディパーシングはアクションの合成が行われる前に実行されるため、request.body()
を介して、既に解析されたリクエストボディにすべてのアクション内でアクセスできます。ただし、ボディパーシングをいくつかの(またはすべて)アクションの合成を介して定義されたアクションが処理された*後*に延期する方が理にかなうユースケースがあります。たとえば
- リクエスト属性を介してリクエスト固有の情報をボディパーサーに渡す場合。たとえば、ユーザーに依存する最大ファイルアップロードサイズ、またはボディパーサーがアップロードをリダイレクトする必要があるWebサービスまたはオブジェクトストレージのユーザーに依存する資格情報などです。
- (粒度の細かい)承認にアクション合成を使用する場合、権限チェックが失敗した場合、リクエストボディを解析せず、リクエストを早期にキャンセルする場合があります。
もちろん、ボディパーシングを延期する場合、ボディパーシングが行われる前に実行されるアクション内ではリクエストボディはまだ解析されないため、request.body()
はnull
を返します。
conf/application.conf
でグローバルに遅延ボディパーシングを有効にできます。
play.server.deferBodyParsing = true
すべてのplay.server.*
設定キーと同様に、この設定はDEVモードではPlayによって取得されず、PRODモードでのみ取得されることに注意してください。DEVモードでこの設定を設定するには、build.sbt
で設定する必要があります。
PlayKeys.devSettings += "play.server.deferBodyParsing" -> "true"
グローバルに遅延ボディパースを有効にする代わりに、routes修飾子のdeferBodyParsing
を使用して、特定のルートに対してのみ有効にすることができます。
+ deferBodyParsing
POST / controllers.HomeController.uploadFileToS3
逆もまた同様です。グローバルに遅延ボディパースを有効にしている場合、routes修飾子のdontDeferBodyParsing
を使用して、特定のルートに対して無効にすることができます。
+ dontDeferBodyParsing
POST / controllers.HomeController.processUpload
ボディは、play.api.mvc.BodyParser.parseBody
を呼び出すことで解析できるようになりました。
def home(): Action[AnyContent] = Action.async(parse.default) { implicit request: Request[AnyContent] =>
{
// When body parsing was deferred, the body is not parsed here yet, so following will be true:
// - request.body == null
// - request.attrs.contains(play.api.mvc.request.RequestAttrKey.DeferredBodyParsing)
// Do NOT rely on request.hasBody because it has nothing to do if a body was parsed or not!
BodyParser.parseBody(
parse.default,
request,
(req: Request[AnyContent]) => {
// The body is parsed here now, therefore:
// - request.body has a value now
// - request.attrs does not contain RequestAttrKey.DeferredBodyParsing anymore
Future.successful(Ok)
}
)
}
}
次へ: コンテンツネゴシエーション
このドキュメントに誤りを見つけましたか? このページのソースコードはこちらにあります。ドキュメントガイドラインを読んだ後、プルリクエストを自由に投稿してください。質問やアドバイスがありますか? コミュニティフォーラムでコミュニティとの会話を開始してください。