views:

780

answers:

4

Hi, I'm developing a ruby on rails app and I want to be able to excecute a method on every AR object before each save.

I thought I'd create a layer-super-type like this:

MyObject << DomainObject << ActiveRecord::Base

and put in DomainObject a callback (before_save) with my special method (which basically strips all tags like "H1" from the string attributes of the object).

The catch is that rails is asking for the domain_object table, which I obviously don't have.

My second attempt was to monkeypatch active record, like this:

module ActiveRecord
  class Base
    def my_method .... end
  end
end

And put that under the lib folder.

This doesnt work, it tells me that my_method is undefined.

Any ideas?

+3  A: 

Try using an abstract class for your domain object.

class DomainObject < ActiveRecord::Base
  self.abstract_class = true
  # your stuff goes here
end

With an abstract class, you are creating a model which cannot have objects (cannot be instantiated) and don't have an associated table.

From reading Rails: Where to put the 'other' files from Strictly Untyped,

Files in lib are not loaded when Rails starts. Rails has overridden both Class.const_missing and Module.const_missing to dynamically load the file based on the class name. In fact, this is exactly how Rails loads your models and controllers.

so placing the file in the lib folder, it will not be run when Rails starts and won't monkey patch ActiveRecord::Base. You could place the file in config/initializers, but I think there are better alternatives.

Michael Sepcot
Thanks mike, this is exactly what I meant, any ideas why the 'monkeypatching' under the lib folder didn't work? if you can update the answer with that, I believe it'll the the accepted one, and I'll close the question. Thanks again
Pablo Fernandez
Generally, if you have an addition to ActiveRecord::Base, you place it in the lib directory and include it in environment.rb. I agree that an abstract class solves your problem better, though.
Sarah Mei
+1  A: 

Another method that I used at a previous job for stripping HTML tags from models is to create a plugin. We stripped a lot more than just HTML tags, but here is the HTML stripping portion:

The initializer (vendor/plugins/stripper/init.rb):

require 'active_record/stripper'
ActiveRecord::Base.class_eval do
  include ActiveRecord::Stripper
end

The stripping code (vendor/plugins/stripper/lib/active_record/stripper.rb):

module ActiveRecord
  module Stripper
    module ClassMethods
      def strip_html(*args)
        opts = args.extract_options!
        self.strip_html_fields = args
        before_validation :strip_html
      end
    end

    module InstanceMethods
      def strip_html
        self.class.strip_html_fields.each{ |field| strip_html_field(field) }
      end
    private
      def strip_html_field(field)
        clean_attribute(field, /<\/?[^>]*>/, "")
      end
      def clean_attribute(field, regex, replacement)
        self[field].gsub!(regex, replacement) rescue nil
      end
    end

    def self.included(receiver)
      receiver.class_inheritable_accessor :strip_html_fields
      receiver.extend         ClassMethods
      receiver.send :include, InstanceMethods
    end
  end
end

Then in your MyObject class, you can selectively strip html from fields by calling:

class MyObject < ActiveRecord::Base
  strip_html :first_attr, :second_attr, :etc
end
Michael Sepcot
+1  A: 

The HTML stripping plugin code already given would handle the specific use mentioned in the question. In general, to add the same code to a number of classes, including a module will do this easily without requiring everything to inherit from some common base, or adding any methods to ActiveRecord itself.

module MyBeforeSave
  def self.included(base)
    base.before_save :before_save_tasks
  end

  def before_save_tasks
    puts "in module before_save tasks"
  end
end

class MyModel < ActiveRecord::Base
  include MyBeforeSave
end

>> m = MyModel.new
=> #<MyModel id: nil>
>> m.save
in module before_save tasks
=> true
Jeff Dallien
The module approach has drawbacks. It can only be included by an instance of AR:Base, every class has to include it, and if you want to add more common (unrelated) behavior, you'd have to create another module. I don't see why inheriting is a problem
Pablo Fernandez
Including a module in every class is no more work than changing where every one inherits from. Many would consider a new module for each type of unrelated behavior to be a good thing, separation of concerns. What if you add 3 types of functionality to the base but want a new class to only have 2?
Jeff Dallien
I'd rather have the extra method on the new class than 3 include lines on every single class of my domain. This is an explicit case of inheritance, throwing modules to the problem just makes it more complex. Check your included method, are you sure it's not cryptic?
Pablo Fernandez
I dont mean to be rude here, I appreciate your point of view and I understand it's a perfectly valid one, just that I don't share it.
Pablo Fernandez
A: 

I'd monkeypatch ActiveRecord::Base and put the file in config/initializers:

class ActiveRecord::Base

  before_create :some_method

  def some_method
  end

end
Matt Darby