ましめも

技術系メモ

play frameworkでPUT, DELETE, PATCH等のリクエストを<form>から受け取る

RESTfulなパスにしたいとき、<form>タグでmethod=PUT, DELETE等を送れない問題*1にぶち当たることがある。
全部POSTで送ればいいのだが、どうしてもPUTやDELETEを使いたいRESTful脳な人のための解決方法。

解決方法

Railsはこの願いを叶えるために_methodというパラメータを渡した場合はそのメソッドとしてリクエストを解釈するという仕様がある。(formヘルパーは、勝手に<input type="hidden" name="_method" value="put">というhiddenパラメータをつけてくれる *2 )play frameworkでもこれとほぼ同様の手段で解決できるよう、@helper.formの代わりになるヘルパーと、HTTPリクエストの処理の仕方を変えるようにした。

app/views/helper/formExtended.scala.html

GET or POSTでない場合のみGETパラメータに"_method=METHOD"をつけるヘルパー。@helper.formの代わりに@helper.formExtendedとしてあげれば良い。

@(action: Call, args: (Symbol, String)*)(body: => Html)

@availableFormMethod(method: String) = @{
  method.toUpperCase match {
    case "GET" | "POST" | "" => true
    case _ => false
  }
}
@appendMethod(action: Call) = @{
  val (url, method) = (action.url, action.method)
  if(availableFormMethod(method)) {
    url
  } else {
    url + (if(url.contains('?')) "&" else "?") + "_method=" + method
  }
}

<form action="@appendMethod(action)" method="@if(availableFormMethod(action.method)) {@action.method} else {POST}" @toHtmlArgs(args.toMap)>
  @body
</form>

(大抵のケースだとCSRF対策でtokenを送っていると思うので、送るロジックをついでにここに書いちゃえばいい)

app/Global.scala

あとはGlobal.scalaでリクエストのメソッドを書き換えてあげれば完成

object Global extends GlobalSettings with Results {
....
  override def onRouteRequest(request: RequestHeader): Option[Handler] = {
    val requestRewrited = request.method.toUpperCase match {
      case "POST" =>
        request.copy(method = request.getQueryString("_method").getOrElse("POST"))
      case _ => request
    }
    super.onRouteRequest(requestRewrited)
  }
}


(追記) @formExtended というヘルパーを作るより、単純にCallを書き換えてくれるやつ作ったほうが良いかも

*1:HTML4だとGET, POSTしか定義されていない。2014/02/02時点のChrome, Firefoxもこれら以外のmethodで送れないように思える…。要検証。

*2:http://guides.rubyonrails.org/form_helpers.html