大昔のRailsでは、before_filterでfalseを返すとそこでchainが終わる、とやっていた気がしますが、今はそういうコード見ないですよね。
Rails 4だとこんなノリでbefore_actionでredirectして はいおしまい、ってやりますよね。
class UsersController < ApplicationController
before_action :my_authenticate_admin
def my_authenticate_admin
unless current_user.admin?
redirect_to root_path
end
end
end
当然、以後のbefore_actionやactionは実行されないことを期待するわけです。
これで全然問題ないのですが、なぜredirect_toするとそこで止まってアクションが実行されないのか、せっかくなので読んでみましょう。
before_actionを探す
まず、before_actionのコードを探します。
actionpack-4.0.0/lib/abstract_controller/callbacks.rb
[:before, :after, :around].each do |callback|
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
# Append a before, after or around callback. See _insert_callbacks
# for details on the allowed parameters.
def #{callback}_action(*names, &blk)
_insert_callbacks(names, blk) do |name, options|
set_callback(:process_action, :#{callback}, name, options)
end
end
AbstractController::Callbacksにありました。before_action, after_action, around_actionを定義しています。
AbstractController::Callbacksは、includedでこんな処理もしています。
included do
define_callbacks :process_action, :terminator => "response_body", :skip_after_callbacks_if_terminated => true
end
あと、controllerのactionを実行するべきprocess_actionが、callbackの中に組み込まれていますね。
callbacksを読む
ActiveSupportのcallbacksを呼び出しているので、そちらを読んでみましょう。
activesupport-4.0.0/lib/active_support/callbacks.rb
def define_callbacks(*callbacks)
config = callbacks.last.is_a?(Hash) ? callbacks.pop : {}
callbacks.each do |callback|
class_attribute "_#{callback}_callbacks"
send("_#{callback}_callbacks=", CallbackChain.new(callback, config))
end
end
actionpackのほうで呼び出していたdefine_callbacksはここにありました。
CallbackChainはArrayにいくつかメソッドが付いただけのものです。"_process_action_callbacks"という配列ができるようです。
set_callbackはこんな感じです。
def set_callback(name, *filter_list, &block)
mapped = nil
__update_callbacks(name, filter_list, block) do |target, chain, type, filters, options|
mapped ||= filters.map do |filter|
Callback.new(chain, filter, type, options.dup, self)
end
options[:prepend] ? chain.prepend(*mapped) : chain.append(*mapped)
target.send("_#{name}_callbacks=", chain)
end
end
配列に追加してるだけですね。
CallbackChainはArrayですが、1つcompile
という重要そうなメソッドがあります。
def compile
method = ["value = nil", "halted = false"]
callbacks = "value = !halted && (!block_given? || yield)"
reverse_each do |callback|
callbacks = callback.apply(callbacks)
end
method << callbacks
method << "value"
method.join("\n")
end
お次はapplyです。
def apply(code)
case @kind
when :before
<<-RUBY_EVAL
if !halted && #{@compiled_options}
# This double assignment is to prevent warnings in 1.9.3 as
# the `result` variable is not always used except if the
# terminator code refers to it.
result = result = #{@filter}
halted = (#{chain.config[:terminator]})
if halted
halted_callback_hook(#{@raw_filter.inspect.inspect})
end
end
#{code}
RUBY_EVAL
いかにもなif文が出てきました。haltedがtrueになると、それ以降callbackを実行しないようになっています。
chain.config[:terminator]を評価した結果がhaltedになっていますが、これは最初に見たAbstractController::Callbacksで"response_body"にセットされていました。
つまり、response_bodyがnilやfalseでないときに、そこでcallback chainが止まるわけですね。
response_bodyを探す
actionpack-4.0.0/lib/abstract_controller/base.rb
class Base
attr_internal :response_body
単なるattributeです。
renderやrespond_toを読んでみると、確かにresponse_bodyに書き込んでいます。
actionpack-4.0.0/lib/abstract_controller/metal/redirecting.rb
def redirect_to(options = {}, response_status = {}) #:doc:
raise ActionControllerError.new("Cannot redirect to nil!") unless options
raise AbstractController::DoubleRenderError if response_body
self.status = _extract_redirect_to_status(options, response_status)
self.location = _compute_redirect_to_location(options)
self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.h(location)}\">redirected</a>.</body></html>"
end
ということで、before_actionでrenderやredirect_toをすると、response_bodyが空で無くなるため、そこでcallback chainが止まり、以降のbefore_actionやactionが実行されなくなるということが分かりました。
めでたしめでたし。