2016/12/06

何故 Rails では View から Controller のインスタンス変数にアクセスできるのか

この記事は Okinawa.rb Advent Calendar 2016 6日目の記事です。


はじまり

むかしむかし、あるところでひとりのあっとんさんが Okinawa.rb Advent Calendar のネタをどうるか考えていました。
そこへ @CodeHex さんがあらわれ、ちょっと Rails について質問をしても良いですかと尋ねてきました。

CodeHex さん「Controller で @ を付けた変数は View からアクセスできるんですか?」
あっとんさん「Rails ではそうですね。ちなみに Ruby では @ が付いた変数はインスタンス変数なのです。」
CodeHex さん「なるほど。 Perl では @ があると配列なので違和感がありますね。」

さて、質問された時に Rails ではそういうものだという解答をしたのですがふと思いました。
「別にインスタンス変数だからと言って View から見えるのは関係が無いよね」
そこで調査班は実装を見付けるために Rails へと踏み込むことにしました。


View はどのようにインスタンス変数にアクセスしているのか

まずは View のクラスを確認してみます。
単に Controller を継承しているだけなのかもしれません。
それならこの記事は終わってしまいますがまず確認しないことには始まりません。

ということで Rails のプロジェクトを作ります
$ rails -v # 5.0.01
$ rails new advent2016
$ cd advent2016
$ rails generate scaffold user uid:string
ここで User#index に binding.pry を仕込んで self.class を確認してみます。
[2] pry(#<#<Class:0x007ffdce310d48>>)> self.class
=> #<Class:0x007ffdce310d48>
どうやら特異クラス。さて ancestors を確認してみると
となっていました。
継承しているものに Controller らしきものはパッと見は無いようですね。
調査は続行しそうです。

そこで self を確認していると
とそのまま @_assigns というインスタンス変数があることを発見。あやしいですね。
もちろんこの段階では @users は存在しているので
$ self.instance_eval{@_assigns= nil}
としても @users は変更されません。


@_assign を追いかける

@_assign はどこで変更されるのか。
調査隊はさらに Rails の奥へと踏み込むことを決意します。
$ git grep @_assigns
するとactionview/lib/action_view/base.rb の一つしかありません。
しかもやっていることは instance_variable_set なので怪しさしかありません。
assigns は引数なのでどこかで new されているはず。
次はそれを探します。


ActionView::Base.new はどこだ

まずは素直に git grep します。test もあったので grep -v で除外。
$ git grep ActionView::Base.new | grep -v test
actionpack/lib/action_controller/metal/helpers.rb が引っかかりました。
しかしこいつは module でこれが呼ばれるのを探すのは面倒。
また pry に戻って探してみることにします。

backtrace を覗く

また User#index の binding.pry に戻ります。
User#index の段階では view は用意されてるのでどこかで既に new されているはず。
なのでまず backtrace を眺めてみます。
pry-byebug を入れて pry-backtrace と打つと backtrace が見られます。
さて覗いてみると

多い。111もあります。
折角 byebug で repl も動くことなので up していきます。
途中で String を freeze しているころを見付けます。
噂の物体を副産物で見付けた気分ですね。
どんどん up していきます。
actionview/lib/action_view/renderer/template_renderer.rb の render_with_layout くらいから view という変数がちらほら出てきますね。あやしい。
view は context という名前になったりしながら連れ回されているようです。
actionview/lib/action_view/rendering.rb の _render_template までそれっぽいのがありますが、actionpack/lib/action_controller/metal/streaming.rb の _render_template あたりから出てこなくなります。
この辺りに真相が潜んでいそうです。
というか metal は見覚えがありますね。 ActionView::Base.new しているのも metal の helper でした。


結局どこでインスタンス変数を渡しているのか

最後に view っぽいのを扱っているのは actionview/lib/action_view/rendering.rb の _render_template でした。
context に view_context を代入しています。
view_context の定義に view_assigns という怪しさ満点のものを発見。
しかし rendering.rb に view_assigns は無い。
名前が分かればとりあえず git grep します。
actionpack/lib/abstract_controller/rendering.rb に定義を発見しました。
instance_variables を取ってきてhash にしています。
slice しているのは @ を取りのぞくためですね。
ここでおもしろいのは protected_vars を reject していること。
@_formats などは controller から渡さないようにしているようです。
view_assigns は AbstractController::Rendering に定義されています。
ということは ApplicationController が AbstractController::Rendering を継承していれば、ユーザ定義のコントローラは view_assigns を経由でインスタンス変数を取得して view を生成していそうですね。
となれば気になるのは UsersController の ancestors です。
と AbstractController::Rendering がばっちりありますね。56行目です。
という訳で「何故 View は Controller のインスタンス変数へとアクセスできるのか」を発見できました。
それは「Controller が View を Rendering する時に自分のインスタンス変数を渡してやっているから」です。


view_assigns を上書きするとどうなるか

実装が分かれば遊びたくなるのが常。
自分はきちんとコードが読めたのかも含めてちょっと遊んでみます。
内容は @users という変数を無理矢理作ってやるというもの。
この状態で user#index にアクセスすると DB の中身に関係無く、定義された内容が表示されました。
やはり view に渡すためのインスタンス変数を取得するメソッドは AbstractController::Rendering#view_assign で間違いない様子。
ということで無事 Rails の中からお目当てのものを見付けられました。
めでたしめでたし。


おわり

というおはなしなのでした。
読むのに pry-byebug が便利でした。
むしろ無かったら見付けられなかったかもしれない。
結論としては Controller で view_assigns を上書きすると大変なことになりそう、って感じです。


みなさま良い年末をー。

0 件のコメント:

コメントを投稿