Hama Blog

Hama Blog

主にtech関連の記録

Railsのform_withのオプションでmethodを指定しない場合にHTTPメソッドがどのように決められるのか調べてみた

概要

Rails の form_with ヘルパーでは、method オプション(任意オプション)を指定すると form_with によって生成される html の form タグに対して HTTP メソッドを任意に決めることができる。

未指定の場合はデフォルトで POST になるのかと思っていた(ここに書いてあったのでそう思っていた)が、そうでもなさそうだったので未指定の場合の挙動について今回調べてみた。

追記

たしかに form タグ自体の method のデフォルトは POST になる。

記事執筆時の環境

結論

例えば、以下のように form_with を指定したとき( method オプションが未指定のとき)、@model が保存済み ( persisted? ) かどうかで HTTP メソッドが決まる。

/ erb ではなく slim で記述している
= form_with model: @model, local: true do |f|
~省略~
  • 保存済みの場合: POST

      <form action="/models/:id" accept-charset="UTF-8" method="post">
        <input type="hidden" name="authenticity_token" value="~省略~" autocomplete="off">
    
  • 未保存の場合: PATCH

    name="_method" の input タグが追加される。

      <form action="/models/:id" accept-charset="UTF-8" method="post">
        <input type="hidden" name="_method" value="patch" autocomplete="off">
        <input type="hidden" name="authenticity_token" value="~省略~" autocomplete="off">
    

詳しく見てみる

結論にも書いたが、以下のような記述で form_with を使ってみる。

(新規作成時 / 編集時 の両方の画面で使えるようなかたちにしてみた)

/ erb ではなく slim で記述している
= form_with model: @model, local: true do |f|
~省略~

ここからコードを探っていく。

まず、form_with ヘルパーは以下で定義されている。

https://github.com/rails/rails/blob/8015c2c2cf5c8718449677570f372ceb01318a32/actionview/lib/action_view/helpers/form_helper.rb#L754-L777

def form_with(model: nil, scope: nil, url: nil, format: nil, **options, &block)
  options = { allow_method_names_outside_object: true, skip_default_ids: !form_with_generates_ids }.merge!(options)

  if model
    if url != false
      url ||= polymorphic_path(model, format: format)
    end

    model   = _object_for_form_builder(model)
    scope ||= model_name_from_record_or_class(model).param_key
  end

  if block_given?
    builder = instantiate_builder(scope, model, options)
    output  = capture(builder, &block)
    options[:multipart] ||= builder.multipart?

    html_options = html_options_for_form_with(url, model, **options)
    form_tag_with_body(html_options, output)
  else
    html_options = html_options_for_form_with(url, model, **options)
    form_tag_html(html_options)
  end
end

今回のサンプルコードの場合だと、ブロックを渡しているので if block_given? の条件分岐は true になる。

(ただ、今回の調査内容でいうと block_given? かどうかは調査結果に関係ない)

どちらにせよ、form_tag_with_body もしくは form_tag_html で html の form タグが作られそうである。

ひとまず、その前の html_options_for_form_with の中身を見てみると、html_options[:method] ||= :patch if model.respond_to?(:persisted?) && model.persisted? という記述があり、ここでとりあえず model が保存済みなら PATCH になることがわかる。

https://github.com/rails/rails/blob/8015c2c2cf5c8718449677570f372ceb01318a32/actionview/lib/action_view/helpers/form_helper.rb#L1578-L1591

def html_options_for_form_with(url_for_options = nil, model = nil, html: {}, local: !form_with_generates_remote_forms,
  skip_enforcing_utf8: nil, **options)
  html_options = options.slice(:id, :class, :multipart, :method, :data, :authenticity_token).merge!(html)
  html_options[:remote] = html.delete(:remote) || !local
  html_options[:method] ||= :patch if model.respond_to?(:persisted?) && model.persisted?
  if skip_enforcing_utf8.nil?
    if options.key?(:enforce_utf8)
      html_options[:enforce_utf8] = options[:enforce_utf8]
    end
  else
    html_options[:enforce_utf8] = !skip_enforcing_utf8
  end
  html_options_for_form(url_for_options.nil? ? {} : url_for_options, html_options)
end

form_with メソッドに戻って、form_tag_with_bodyform_tag_html を見ると、form_tag_with_bodyform_tag_html を呼び出している。

どちらも途中で extra_tags_for_form というメソッドを呼んでおり、ここでいかにも method を決定していそうなことがわかった。

https://github.com/rails/rails/blob/8015c2c2cf5c8718449677570f372ceb01318a32/actionview/lib/action_view/helpers/form_tag_helper.rb#L950-L978

def extra_tags_for_form(html_options)
  authenticity_token = html_options.delete("authenticity_token")
  method = html_options.delete("method").to_s.downcase

  method_tag = \
    case method
    when "get"
      html_options["method"] = "get"
      ""
    when "post", ""
      html_options["method"] = "post"
      token_tag(authenticity_token, form_options: {
        action: html_options["action"],
        method: "post"
      })
    else
      html_options["method"] = "post"
      method_tag(method) + token_tag(authenticity_token, form_options: {
        action: html_options["action"],
        method: method
      })
    end

  if html_options.delete("enforce_utf8") { default_enforce_utf8 }
    utf8_enforcer_tag + method_tag
  else
    method_tag
  end
end

今回は model オプションに指定した @model が保存済みかどうかで html_options_for_form_with のところで html_options[:method]:patchnil が入るようになっていた。

なので、method_tag に代入している switch 文では elsewhen "post", "" 内の処理が実行されることになる。

( html_options[:method]nil の場合は extra_tags_for_formmethod = html_options.delete("method").to_s.downcase により "" になるので when "post", "" 内に入る)

まとめると、

  • まずは @model が保存済みかどうか関係なく、html_options["method"] = "post" が設定される。

    これは <form action="/models/:id" accept-charset="UTF-8" method="post"> の method の部分。

  • @model が保存済みなら、method_tag(method) ( method は :patch) が実行され、<input type="hidden" name="_method" value="patch" autocomplete="off"> の部分が作られる。