railsのbefore_actionはどうやってうごいているか

[Ateam Lifestyle x cyma Advent Calendar 2018](https://qiita.com/advent-calendar/2018/ateam-lifestyle-cyma) の15日目は、株式会社エイチームライフスタイルのWebエンジニア @kytiken が担当します。

before_actionと言っているのは、controllerに `before_action :メソッド名` と書いたら、アクションが実行される直前に指定したメソッドを実行してくれるアレのことです。
今回の記事では `before_action` がどうやって動いているのかを、railsのソースコードを読むことによって、理解を深めようという趣旨で書いていきます。

## バージョン

rails5.2

## コードリーディング

実際にbefore_actionをメソッドを定義している場所を見てみます。

```ruby
       [:before, :after, :around].each do |callback|
         define_method "#{callback}_action" do |*names, &blk|
           _insert_callbacks(names, blk) do |name, options|
             set_callback(:process_action, callback, name, options)
           end
         end
```

https://github.com/rails/rails/blob/5-2-stable/actionpack/lib/abstract_controller/callbacks.rb#L186

before_actionを定義している部分をかいつまんで抜き出しました。

define_methodで動的にメソッドを作っています。

before_actionを実行すると `_insert_callbacks` を実行して、そのブロックの中で `set_callback` しているとわかります。

この2つのメソッドがそれぞれ何をしているかを調べていきます。

## _insert_callbacks

```ruby
       def _insert_callbacks(callbacks, block = nil)
         options = callbacks.extract_options!
         _normalize_callback_options(options)
         callbacks.push(block) if block
         callbacks.each do |callback|
           yield callback, options
         end
       end
```

https://github.com/rails/rails/blob/5-2-stable/actionpack/lib/abstract_controller/callbacks.rb#L91

このメソッドは引数として渡された `callbacks` をeachでグルグル回して、 `options` と一緒に `yield` にわたすということをしています。
また第二引数としてblockが渡されているとしたら、それをcallbacksに追加して、最終的には `yield` に渡されます。

## set_callback

このメソッドは任意のメソッドに対して、コールバックを設定できるメソッドとなります。

このメソッドの使い方は、こちらのAPIドキュメントに詳しく書いてあります。

```ruby
class Audit
  def before(caller)
    puts 'Audit: before is called'
  end

  def before_save(caller)
    puts 'Audit: before_save is called'
  end
end

class Account
  include ActiveSupport::Callbacks

  define_callbacks :save
  set_callback :save, :before, Audit.new

  def save
    run_callbacks :save do
      puts 'save in main'
    end
  end
end
```

apiドキュメントより引用

https://api.rubyonrails.org/classes/ActiveSupport/Callbacks/ClassMethods.html#method-i-define_callbacks

`set_callback` を使ってコールバックするためには、 `define_callbacks` をしておく必要があるのと、 `run_callbacks` を実行するようにしておく必要があります。

## before_actionの定義

`_insert_callbacks` と `set_callback` のことがわかったところで、もう一回 `before_action` を見てみます。

```ruby
       [:before, :after, :around].each do |callback|
         define_method "#{callback}_action" do |*names, &blk|
           _insert_callbacks(names, blk) do |name, options|
             set_callback(:process_action, callback, name, options)
           end
         end
```

https://github.com/rails/rails/blob/5-2-stable/actionpack/lib/abstract_controller/callbacks.rb#L186

さて、before_actionの定義で、set_callbackは下記のように使われていました。

```
             set_callback(:process_action, callback, name, options)
```

https://github.com/rails/rails/blob/5-2-stable/actionpack/lib/abstract_controller/callbacks.rb#L189

`process_action` メソッドにコールバックをつけていることがわかります。

## process_action

この `process_action` というメソッドは、アクションが実行されるタイミングで実行されるのですが、このモジュールの中で、 `run_callbacks` を実行するように上書きしています。

```
    def process_action(*args)
      run_callbacks(:process_action) do
        super
      end
    end
```

https://github.com/rails/rails/blob/5-2-stable/actionpack/lib/abstract_controller/callbacks.rb#L40

ついでにいうと、callbackが使えるように `define_callbacks` もやっています。

```
      define_callbacks :process_action,
                       terminator: ->(controller, result_lambda) { result_lambda.call; controller.performed? },
                       skip_after_callbacks_if_terminated: true
```

https://github.com/rails/rails/blob/5-2-stable/actionpack/lib/abstract_controller/callbacks.rb#L33

## まとめ

* `process_action` が実行されるタイミングで `run_callback` が実行される。
* `before_action` は `process_action` にset_callbackするもの。

[Ateam Lifestyle x cyma Advent Calendar 2018](https://qiita.com/advent-calendar/2018/ateam-lifestyle-cyma) の16日目は @kyntk さん に書いてもらう予定です。お楽しみに！

エイチームグループでは、一緒に働けるチャレンジ精神旺盛な仲間を募集しています。興味を持たれた方はぜひエイチームグループ採用サイトを御覧ください。
https://www.a-tm.co.jp/recruit/