ドキュメント

§アクションの合成

この章では、汎用的なアクション機能を定義するいくつかの方法について説明します。

§カスタムアクションビルダー

以前に見たように、アクションを宣言するには複数の方法があります。リクエストパラメーターを使用する場合と使用しない場合、ボディパーサーを使用する場合などです。非同期プログラミングに関する章で説明するように、実際には他にも多くの方法があります。

これらのアクション構築メソッドは、実際にはすべて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を実装するいくつかの事前に定義されたトレイトがあります。

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パスには多くのルートがあり、これらのルートごとにアイテムを検索する必要があります。この場合、このロジックをアクション関数に入れると便利です。

まず、UserRequestItemを追加するリクエストオブジェクトを作成します。

import play.api.mvc._

class ItemRequest[A](val item: Item, request: UserRequest[A]) extends WrappedRequest[A](request) {
  def username = request.username
}

次に、そのアイテムを検索して、エラー(Left)または新しいItemRequestRight)を返すアクションリファイナーを作成します。このアクションリファイナーは、アイテムの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()を介して、既に解析されたリクエストボディにすべてのアクション内でアクセスできます。ただし、ボディパーシングをいくつかの(またはすべて)アクションの合成を介して定義されたアクションが処理された*後*に延期する方が理にかなうユースケースがあります。たとえば

もちろん、ボディパーシングを延期する場合、ボディパーシングが行われる前に実行されるアクション内ではリクエストボディはまだ解析されないため、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)
      }
    )
  }
}

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


このドキュメントに誤りを見つけましたか? このページのソースコードはこちらにあります。ドキュメントガイドラインを読んだ後、プルリクエストを自由に投稿してください。質問やアドバイスがありますか? コミュニティフォーラムでコミュニティとの会話を開始してください。