LoginSignup
0
0

More than 1 year has passed since last update.

around_actionで引数ありのメソッドをlambdaで指定する

Last updated at Posted at 2021-10-18

概要

around_actionに引数を持つメソッドを指定する際に無限に詰まったので同じ状況で困っている人のために残しておく。

指定方法だけ知りたい人は解決までジャンプしてもらえれば書いてある。

状況

なぜ詰まったかというと、まずrailsでAction Controllerのフィルターを使用する際に引数を持つメソッドを指定する場合、ブロックを渡す必要がある。下記のようにしないとエラーが出てしまう。

class HogeController < ApplicationController
  # 引数なしの場合
  before_action :fuga
  # 引数ありの場合
  before_action -> { fuga(x, y) }
end

before_actionafter_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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0