Rails を使っている時 Controller で params を呼び出すと GET リクエストだったらクエリパラメータは取得できるし POST であれば Content-Type が multipart/form-data や application/json とかに関係なく値を取得できるようになっている。
便利!!!
Rails ガイドだとこのあたりに書いてある
railsguides.jp
ふとこの params ってどうやって生成されてるのかなーって気になったので Rails のコードを読みながら追ってみたので、そのメモをこの記事として残します。
※ もし間違っていたら教えてください 🙏
今回は Rails 6.0.3.4 を使用しています。
Controller の params は何?
Controller で params
を使う時は Rails の StrongParameters を使っていると思います。
params は下記の StrongParameters モジュール内の params メソッドから呼び出されているようです。
処理をみると request.parameters
を引数にとって ActionController::Parameters
のインスタンスを生成しています。
def params @_params ||= Parameters.new(request.parameters) end
この ActionController::Parameters
が Controller で params を使う時に呼び出す require
や permit
などが定義されています。
どうやらこの request.parameters にほしい情報が入っているようです。
request.parameters
まず request
は ActionDispatch::Request
です。
これは Controller からもアクセスできるので馴染みがあるかもですね。
request.parameters
は ActionDispatch::Http::Parameters モジュール内に parameters
メソッドとして定義されています。
https://github.com/rails/rails/blob/fe76a95b0d252a2d7c25e69498b720c96b243ea2/actionpack/lib/action_dispatch/http/parameters.rb#L49-L63
一部を抜粋したのが下記になります。
params = begin request_parameters.merge(query_parameters) rescue EOFError query_parameters.dup end params.merge!(path_parameters)
request_parameters
, query_parameters
, path_parameters
というそれっぽいものが使用されています。
それぞれ何なのかを見ていきます。
path_parameters
値としては {:controller=>"users", :action=>"create"}
のような値が入っています。
リクエストされた URL に対してルーティングから選ばれた Controller とそのアクション名が入っているようです。
この path_parameters は下記で定義されていました。
処理としては get_header(PARAMETERS_KEY) || set_header(PARAMETERS_KEY, {})
だけでした。
https://github.com/rails/rails/blob/fe76a95b0d252a2d7c25e69498b720c96b243ea2/actionpack/lib/action_dispatch/http/parameters.rb#L83-L85
値の格納は下記のメソッドで行われているようです。
https://github.com/rails/rails/blob/fe76a95b0d252a2d7c25e69498b720c96b243ea2/actionpack/lib/action_dispatch/http/parameters.rb#L66-L77
実際の値は ActionDispatch::Journey::Router
内の下記メソッドで格納しています。
この ActionDispatch::Journey
は Rails のルーティングを支えているライブラリのようです。
https://github.com/rails/rails/blob/fe76a95b0d252a2d7c25e69498b720c96b243ea2/actionpack/lib/action_dispatch/journey/router.rb#L47-L47
query_parameters
次は query_parameters
ですが、名前からもわかるように値としては例えば ?foo=bar
のような URL でアクセスすると {"foo"=>"bar"}
が入ってきます。
この query_parameters
は下記で GET
メソッドの alias として定義されています。
https://github.com/rails/rails/blob/fe76a95b0d252a2d7c25e69498b720c96b243ea2/actionpack/lib/action_dispatch/http/request.rb#L374-L384
fetch_header("action_dispatch.request.query_parameters") do |k| rack_query_params = super || {} # Check for non UTF-8 parameter values, which would cause errors later Request::Utils.check_param_encoding(rack_query_params) set_header k, Request::Utils.normalize_encode_params(rack_query_params) end
fetch_header
は rack gem に定義されているメソッドで下記に定義されています。
https://github.com/rack/rack/blob/1741c580d71cfca8e541e96cc372305c8892ee74/lib/rack/request.rb#L68-L70
中身は Hash の fetch メソッドを読んでいるだけで、今回は block を定義しているので、キーが見つからなかった時にブロック内のコードが実行されます。
ここで重要なのでは rack_query_params = super || {}
でここで super
を呼び出すと rack に定義されている下記の GET メソッドが呼び出されます。
https://github.com/rack/rack/blob/1741c580d71cfca8e541e96cc372305c8892ee74/lib/rack/request.rb#L426-L434
rack の GET メソッド内の query_hash = parse_query(query_string, '&;')
で、query_string には "foo=bar"
のような文字列が入っているのでこれがパースされて {"foo"=>"bar"}
のような Hash になるようです。
そしてこの Hash が query_parameters の値として使用されます。
request_parameters
最後は request_parameters です。
path_parameters には Controller とアクション名が入っていて、query_parametersパラメータにはクエリパラメータの値が入っていました。
となると残りの request_parameters には POST 時の Body 情報が入ってそうです。
この request_parametes は下記に POST メソッドの alias として定義されています。
https://github.com/rails/rails/blob/fe76a95b0d252a2d7c25e69498b720c96b243ea2/actionpack/lib/action_dispatch/http/request.rb#L387-L397
一部を抜粋したのが下記のコードです。
pr = parse_formatted_parameters(params_parsers) do |params| super || {} end
まず params_parsers
は下記で定義されているメソッドで中身としては ActionDispatch::Request.parameter_parsers
を呼び出します。
https://github.com/rails/rails/blob/fe76a95b0d252a2d7c25e69498b720c96b243ea2/actionpack/lib/action_dispatch/http/parameters.rb#L130-L132
この ActionDispatch::Request.parameter_parsers
は通常下記の DEFAULT_PARSERS
が使われるようです。
デフォルトで使用される parsers は :json
の場合しか定義されておらず、 :json の場合は raw_post という値を ActiveSupport::JSON.decode して Hash に変換しています。
raw_post は body の値を String として取り出したものを使うようです。
https://github.com/rails/rails/blob/fe76a95b0d252a2d7c25e69498b720c96b243ea2/actionpack/lib/action_dispatch/http/request.rb#L319-L326
DEFAULT_PARSERS = { Mime[:json].symbol => -> (raw_post) { data = ActiveSupport::JSON.decode(raw_post) data.is_a?(Hash) ? data : { _json: data } } }
parser を見たので次は parse_formatted_parameters
の処理を見ていきます。
これは呼び出す際にブロックも指定しています。
pr = parse_formatted_parameters(params_parsers) do |params| super || {} end
関数は下記で定義されています。
いくつか気になる処理がありますが、まず先ほどみた Hash の parsers から content_mime_type.symbol
で値を取り出して、あれば後続の処理でその parser に raw_post (bodyをstringにしたもの) を渡して実行、なければ実行時に定義した block を実行しています。
def parse_formatted_parameters(parsers) return yield if content_length.zero? || content_mime_type.nil? strategy = parsers.fetch(content_mime_type.symbol) { return yield } begin strategy.call(raw_post) rescue # JSON or Ruby code block errors. log_parse_error_once raise ParseError end end
まずは parsers の key として指定している content_mime_type.symbol
が何のか見ていきます。
これは下記で定義されており、CONTENT_TYPEをみつつ値を返しています。
https://github.com/rails/rails/blob/fe76a95b0d252a2d7c25e69498b720c96b243ea2/actionpack/lib/action_dispatch/http/mime_negotiation.rb#L20-L29
マッピングは下記で定義されていて、例えば application/json
なら :json, multipart/form-data
なら :multipart_form です。
https://github.com/rails/rails/blob/fe76a95b0d252a2d7c25e69498b720c96b243ea2/actionpack/lib/action_dispatch/http/mime_types.rb
先ほどのparse_formatted_parametersの処理に戻ります。
parsers には :json しか定義されていないので application/json
の場合は先ほどDEFAULT_PARSERSで定義されていた :json の値を呼び出して ActiveSupport::JSON.decode
した Hash を返してそうです。
そして multipart/form-data
の場合は parser がないのでブロック内の処理が実行されます。
ブロック内は super || {}
だけなのでこの super が Hash を返してそうです。
この super は rack の POST メソッドを呼び出します。
https://github.com/rack/rack/blob/1741c580d71cfca8e541e96cc372305c8892ee74/lib/rack/request.rb#L440-L463
この処理ではいくつかの処理をしているようですが、今回は multipart/form-data
の時の処理を追っていくので、下記の処理に注目します。
elsif form_data? || parseable_data? unless set_header(RACK_REQUEST_FORM_HASH, parse_multipart) ~~~ end set_header RACK_REQUEST_FORM_INPUT, get_header(RACK_INPUT) get_header RACK_REQUEST_FORM_HASH else
中でも set_header(RACK_REQUEST_FORM_HASH, parse_multipart)
が重要で parse_multipart
が multipart/form-data
の時の body を下記のparserを使って Hash にして返しています。
https://github.com/rack/rack/blob/1741c580d71cfca8e541e96cc372305c8892ee74/lib/rack/multipart/parser.rb
まとめ
最後にもう一度 params を呼び出した時の request.parameters の処理をみてみます。
params = begin request_parameters.merge(query_parameters) rescue EOFError query_parameters.dup end params.merge!(path_parameters)
Bodyの情報が入った request_parameters
とクエリパラメータの入った query_parameters
がマージされてその後に Controller やアクション名の入った path_parameters がマージされます。
最後に
今回は Controller の params がどうやって生成されるかを見ていきました。