【Rails】 FormHelperのソースコードを読んでみた
はじめに
以前、当ブログにてRailsのcheck_boxのソースコードを調べてみました。
存在するはずなのにドキュメントには載っていないmultipleオプションが気になったからでした。
peitetsu.hatenablog.com
↑の記事にて、1箇所だけ疑問が残り、あまり時間が無かったので後日調査することにしました。
check_boxメソッドの処理はたった1行
`@template.check_box(@object_name, method, objectify_options(options), checked_value, unchecked_value)`↑のメソッドはFormBuilderクラスに定義されている。
@templateはview_contextで、つまるところFormHelperの同名メソッドを呼ぶことになっているらしい。
参考:https://blog.freedom-man.com/rails-formhelper-codereading/# TODO: この辺りの正確な理解ができなかったので、後日view_contextについて調べる。
今日は、FormHelperのソースコードを読み、上記箇所の疑問を解消できたので、まとめます。
課題
FormBuilderクラスのメソッドでは、下記のように@templateの同名メソッドを呼んでいます。
def check_box(method, options = {}, checked_value = "1", unchecked_value = "0") @template.check_box(@object_name, method, objectify_options(options), checked_value, unchecked_value) end
この@templateが最終的にFormHelperのインスタンスになっているらしい。
参考: Railsのform helper周りのコードリーディング
どのような流れでFormHelperに繋がっているのか、疑問を解消したい、というのが本記事の趣旨です。
処理の流れを追いかけて見る
フォームが作られる際の処理の流れを整理しながら、真相に迫って行きたいと思います。
まず、一般にRailsでフォームを実装する際はViewで form_with タグなどを呼ぶと思います。
(Rails5.1より前のバージョンだと、form_forやform_tagでした。)
form_withはFormHelperクラスに定義されています。
def form_with(model: nil, scope: nil, url: nil, format: nil, **options) options[:allow_method_names_outside_object] = true options[:skip_default_ids] = !form_with_generates_ids if model url ||= polymorphic_path(model, format: format) model = model.last if model.is_a?(Array) scope ||= model_name_from_record_or_class(model).param_key end if block_given? builder = instantiate_builder(scope, model, options) output = capture(builder, &Proc.new) 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?` の分岐の中で、フォーム全体を作っているような処理が書かれています。
(form_withにブロックを渡さない使い方なんて有るの?)
instantiate_builder が怪しいですね。ビルダーをセットしていそうです。
定義を見てみましょう。
def instantiate_builder(record_name, record_object, options) case record_name when String, Symbol object = record_object object_name = record_name else object = record_name object_name = model_name_from_record_or_class(object).param_key if object end builder = options[:builder] || default_form_builder_class builder.new(object_name, object, self, options) end
どうやら↓でビルダークラスをセットして初期化していますね。
builder = options[:builder] || default_form_builder_class builder.new(object_name, object, self, options)
`options[:builder]`はアプリケーションのViewでform_withに渡された値ですね。
では、`default_form_builder_class`はどう定義されているのでしょうか?
上記、`instantiate_builder`メソッドの直後に有りました。
def default_form_builder_class builder = default_form_builder || ActionView::Base.default_form_builder builder.respond_to?(:constantize) ? builder.constantize : builder end
`default_form_builder`か`ActionView::Base.default_form_builder`が、
FormBuilderクラスに繋がっていそうです。
これはゴール間近だと確信しました。
が、、、
再び混乱
`default_form_builder`の定義を探してみたが、それっぽいのが下記ぐらいしか見当たらない。
attr_internal :default_form_builder
https://github.com/rails/rails/blob/master/actionview/lib/action_view/helpers/form_helper.rb#L119
`attr_internal`は、内部変数として`@_defalut_form_builder`が定義される、というもの。
`@_default_form_builder`が使われている様子は無く、途方に暮れる。
困ったので試しにblameして見ると、それらしきコミットがヒットしました。
Override default form builder for a controller · rails/rails@2b8acdc · GitHub
コントローラ側で下記のように書いて、コントローラ毎にFormBuilderを設定できる、という仕様らしい。
class AdminController < ApplicationController default_form_builder AdminFormBuilder end
どのような処理がされているのか一目では分からなかったが、今回の主題から逸れるため割愛。
ようやく答えに
さて、`default_form_builder`が無いときにbuilderにセットされる`ActionView::Base.default_form_builder`ですが、
これはform_helper.rbファイルの最後に書かれていました。
ActiveSupport.on_load(:action_view) do cattr_accessor :default_form_builder, instance_writer: false, instance_reader: false, default: ::ActionView::Helpers::FormBuilder end
action_viewが読み込まれた際に、`ActionView::Base`のクラス変数`default_form_builder`に値をセットするというもの。
そしてそのデフォルト値が`::ActionView::Helpers::FormBuilder`となっています。
少し話を戻して、`instantiate_builder`メソッドでは下記の処理をしていました。
builder = options[:builder] || default_form_builder_class builder.new(object_name, object, self, options)
つまり、builderには`::ActionView::Helpers::FormBuilder`がセットされ、次の行でインスタンス化されています。
本題
さて、本記事で知りたいのは、「FormBuilderクラスのメソッドで使われている@templateの中身は何になっているのか」
というものでした。
FormBulderクラスの初期化メソッドを見てみると、
def initialize(object_name, object, template, options) @nested_child_index = {} @object_name, @object, @template, @options = object_name, object, template, options @default_options = @options ? @options.slice(:index, :namespace, :skip_default_ids, :allow_method_names_outside_object) : {} @default_html_options = @default_options.except(:skip_default_ids, :allow_method_names_outside_object) ==省略== end
https://github.com/rails/rails/blob/master/actionview/lib/action_view/helpers/form_helper.rb#L1660-L1678
第三引数が`@template`にセットされています。
`instantiate_builder`メソッドで第三引数として渡されているのは、`self`です。
ここでの`self`は、つまりFormHelperのインスタンスですね。
ということで、`@template`とは、FormHelperのインスタンスで有り、
FormBuilder内のほとんどのメソッドでは、FormHelperの同名メソッドをそのまま呼ぶだけ、という作りになっていました。
まとめ
フォームを作るときに使う`form_with`メソッドは中でビルダーをセットしている箇所が有ります。(`instantiate_builder`)
ビルダーのデフォルトには、`::ActionView::Helpers::FormBuilder`がセットされ、
その初期化時に、@templateにFormHelperのインスタンスがセットされています。
したがって、例えば`text_field`や`check_box`は最終的にFormHelperで定義されたものが使われるようになっています。