ActiveRecordのフック

Ruby on Railsを弄っている。ActiveRecordで、属性attrに関するフックを、attr関連のメソッドの近くに配置したいのだけれど、よくわからない。

2009-05-08追記: 端っからクラスメソッドbefore_validationを使ってbefore_validation :testのように書けるので、この記事は意味がない。ドキュメントUnlike all the other callbacks, after_find and after_initialize will only be run if an explicit implementation is definedと書いてあるのをちゃんと読め < 昔の自分


ActiveRecordではbefore_saveとかいくつかのフックポイントが用意されているからこのメソッドをオーバーライドすればDBへの格納やvalidationの途中に何か処理を挟めるわけだ。でも、このやり方だと、各attrへのvalidation処理がbefore_validate1カ所に集中してしまったりして、ぱっと見の、そのattrの性質っていうものが掴みにくいように思うのだ。

これをどうにかする仕組みが無いかどうかちょっと探してみたもののよく分からなかったので、とりあえず、こんなコードを書いてしのいでみた。


#
# enhancement for ActiveRecord::Base
#
class ActiveRecord::Base
  %w(
     before_validation before_validation_on_create before_validation_on_update
     after_validation after_validation_on_create after_validation_on_update
     before_save before_create before_update
     after_save after_create after_update
  ).each do |name|
    eval <<-EOF, binding, __FILE__, __LINE__
      def self.#{name}_hooks
        @#{name}_hooks or @#{name}_hooks = []
      end
      def self.#{name}_hooks=(value)
        @#{name}_hooks = value
      end

      def self.hooks_#{name}(*hook, &proc)
        self.#{name}_hooks += hook
        self.#{name}_hooks << proc if proc
      end

      def #{name}
        super
        self.class.#{name}_hooks.each do |hook|
          case hook
          when NilClass: # do nothing
          when Symbol: self.send(hook)
          when String: eval(hook, binding)
          when Proc, Method: hook.call()
          when UnboundMethod: hook.bind(self).call()
          else
            raise "for #{name}, the hook \#{hook} must be " +
              "an either Symbol, String, Proc, or Method"
          end
        end
      end
    EOF
  end

  def self.validation_hooks
    @validation_hooks or @validation_hooks = []
  end
  def self.validation_hooks=(value)
    @validation_hooks = value
  end
  def self.hooks_validation(attr, message, *hook, &proc)
    hook << proc unless proc.nil?
    if hook.size != 1
      raise ArgumentError, "wrong # of argument: #{hook.size} for 1"
    end
    self.validation_hooks.push [attr, message, hook[0]]
  end

  def validate
    self.class.validation_hooks.each do |attr, msg, assertion|
      unless
          case assertion
          when Symbol: self.send(assertion)
          when String: eval(assertion, binding)
          when Proc, Method: assertion.call()
          when UnboundMethod: assertion.bind(self).call()
          else
            raise "for validate(), the hook #{assertion} must be " +
              "an either Symbol, String, Proc, or Method"
          end
      then
        errors.add(attr, msg)
      end
    end
  end
end

こんな感じに使用する。


class Test < ActiveRecord::Base
  validates_length_of :test, :is => 3, :allow_nil => true
  hooks_before_validation %q{ self[:test] =  nil if @test.empty? }
  def test
   ... なんか@testがらみの処理
  end

  def test2
   ...なんか@test2がらみの処理
  end
  hooks_validation :test2, "is invalid", %q{ !@test2.valid? }
end

フックを作る時点ではまだクラス定義を評価中であり、アクティブレコードのインスタンスが存在しないので、それをselfとするようなProcを作れなくて、結局文字列として渡してevalさせないといけないっていうのがいまいち使いづらい。

ActiveRecord本体に何か便利な機能は無いんだろうか。もうちょっと探してみる。