概要
around_action
に引数を持つメソッドを指定する際に無限に詰まったので同じ状況で困っている人のために残しておく。
指定方法だけ知りたい人は解決
までジャンプしてもらえれば書いてある。
状況
なぜ詰まったかというと、まずrailsでAction Controllerのフィルターを使用する際に引数を持つメソッドを指定する場合、ブロックを渡す必要がある。下記のようにしないとエラーが出てしまう。
class HogeController < ApplicationController
# 引数なしの場合
before_action :fuga
# 引数ありの場合
before_action -> { fuga(x, y) }
end
before_action
とafter_action
を使用する場合は上記の記述で問題ないけど、around_action
を使用する場合は1つ問題が発生する。
Railsガイドを読むと下記のように記述がある。
「around系」フィルタを使う場合は、フィルタ内のどこかで必ずyieldを実行して、関連付けられたアクションを実行してやる義務が生じます。これはRackミドルウェアの動作と似ています。
引数なしのaround_action
を使用する場合は下記のように記述する。
class HogeController < ApplicationController
around_action :fuga, only: :show
private
def fuga
ActiveRecord::Base.transaction do
begin
# ここでonlyに指定されたshowアクションが実行される
yield
ensure
raise ActiveRecord::Rollback
end
end
end
end
つまり、around_action
を使用する場合はメソッドに対して(上記の場合はfuga
)ブロックを渡す必要がある。引数なしのメソッドなら何も問題ないが、引数を持っている場合は問題がある。
class HogeController < ApplicationController
# -> { fuga(x, y) }にブロックを渡している
around_action -> { fuga(x, y) }, only: :show
private
def fuga(x, y)
ActiveRecord::Base.transaction do
begin
# ここでブロックを受け取る必要があるが、-> { fuga(x, y) }にブロックが渡されていてfugaにブロックが渡されていない
yield
ensure
raise ActiveRecord::Rollback
end
end
end
end
around_action
で設定しているメソッド内でyield
を使用するのでメソッドに対してブロックを渡す必要があるが、引数を渡すために-> { fuga(x, y) }
としているため渡せないという問題が発生する。
ここで-> { fuga(x, y) }
からfuga
にブロックを渡そうとしても渡すことができない。Proc.new { fuga(x, y)}
とかProc.new { fuga(x, y, &block) }
やってもダメ。
ブロックを渡す方法
self.send(params[:action])
を使用する。
参考:https://stackoverflow.com/questions/34949830/rails-4-around-action-with-parameter
class HogeController < ApplicationController
# -> { fuga(x, y) }にブロックを渡している
around_action -> { fuga(x, y) }, only: :show
private
def fuga(x, y)
ActiveRecord::Base.transaction do
begin
# 変更箇所
# ブロックを渡せないからアクションを呼ぶことで解決している
self.send(params[:action])
ensure
raise ActiveRecord::Rollback
end
end
end
end
Rubyの黒魔術の説明によく出てくるsend
メソッド。ここでもその力を遺憾無く発揮してる。
send
がどういうものかはリファレンスを見て貰えばわかると思う。
参考:https://docs.ruby-lang.org/ja/latest/method/Object/i/send.html
これでaround_action
に設定したメソッドを実行することはできる。
しかし、今度はコントローラのアクション終了時に自動でviewに遷移してくれなくなってしまった。調べても分からなかったが、self.send
でアクションを実行するとその後のレンダリングが行われないらしい。
そのためこちらで本来Railsで行っているレンダリング処理を記述する必要がある。
解決
class HogeController < ApplicationController
around_action -> { fuga(x, y) }, only: :show
private
def fuga(x, y)
ActiveRecord::Base.transaction do
begin
self.send(params[:action])
ensure
raise ActiveRecord::Rollback
end
end
# 変更箇所
default_render unless performed?
end
end
default_render unless performed?
という処理を追加した。これによってコントローラのアクションでrender
が定義されていない場合にアクション名に対応するviewを自動的にレンダリングしてくれる。
render
が行われるとresponse_body
にデータが設定されるが、preformed?
はresponse_body
でデータがあるか判定してくれるらしい。
https://apidock.com/rails/v6.1.3.1/AbstractController/Base/performed%3F
default_render
はコントローラのアクション名に対応するviewを自動でレンダリングしてくれるRails
が定義しているメソッド。
そのためコントローラのアクションで明示的にrender
していても、unless performed?
を設定していることでAbstractController::DoubleRenderError
が発生するのを防いでくれる。
Railsがどうやってテンプレートをレンダリングしているか調べる際に参考にした記事
参考:https://qiita.com/ngtk/items/cd406961cf62531a81c5