Forum: Rails Engines solved: plugin models unloading after first request

D2e767681e3f0c502225d4733a8e939b?d=identicon&s=25 elijah (Guest)
on 2008-09-24 11:09
(Received via mailing list)
I haven't been able to find this advice anywhere else, so I am posting
here so that it may help another lost soul.

Some plugins have a problem: if a plugin applies a mixin directly to a
model in app/models, this mixin gets unloaded by rails after the first
request. This only happens in development mode. The symptom of this is
an application that works for the first request but fails on subsequent
requests.

A call to Dispatcher.to_prepare will get around this problem by
re-applying any mixin that modifies the core models on each request. In
production mode, the classes are not unloaded and so the mixin is only
applied once.

"Normal" plugins don't have this problem: they modify active record, and
then the core models call these extensions. When these core models are
reloaded by rails, the plugin code is then reloaded as well. The problem
only shows up when you want to modify a model in apps/model without
requiring any code change to that model.

This style of using plugins is not considered to be a good idea by many.
However, I am writing plugins that are designed exclusively for a
particular application. These plugins enable optional features of a
specific application--they do not try to modify the behavior of rails or
extend active record.

Here is the code:
---------------------------------------------------------------

(1) modify Engines:Plugin

require 'dispatcher'

Engines::Plugin.class_eval do
  def apply_mixin_to_model(model_class, mixin_module)
    Dispatcher.to_prepare {
      model_class  = Kernel.const_get(model_class.to_s)  # \ weird, yet
      mixin_module = Kernel.const_get(mixin_module.to_s) # / required.
      model_class.send(:extend,  mixin_module.const_get("ClassMethods"))
      model_class.send(:include,
mixin_module.const_get("InstanceMethods"))
      model_class.instance_eval &(mixin_module.class_definition())
    }
  end
end

(2) in your plugin's init.rb:

# in this case, there is a model app/models/language.rb, and module
# vendor/plugins/myplugin/app/models/language_extension.rb

apply_mixin_to_model(Language, LanguageExtension)

(3) this is what my language_extension.rb looks like:

module LanguageExtension
  module ClassMethods
  end

  module InstanceMethods
    def percent_complete()
      count = Key.count_all
      if count > 0
        (Key.translated(self).count / count * 100.0).round.to_s + '%'
      end
    end
  end

  def self.class_definition
    lambda {
      has_many :translations, :dependent => :destroy
    }
  end
end

----------------------------------------------

Most the code in apply_mixin_to_model is not really required. But it
seemed to me a little repetitive to type this over and over again for
each mixin:

module MyModule
  def self.included(base)
    base.extend(ClassMethods)
    base.instance_eval do
      include InstanceMethods
      has_many :somethings
      ....
    end
  end
  .....
end

So, most the code in apply_mixin_to_model is just doing that repeated
stuff for you, so long as the submodules are named right (ie
InstanceMethods, etc).

The simple thing to do is just put:

Dispatcher.to_prepare {
  Language.send(:include, LanguageExtension)
}

In the plugin's init.rb. But again, I am trying to get fancy, perhaps at
my own peril.

Hope that helps someone,
-elijah
E4f80e42794094c98a40bfebef4ec292?d=identicon&s=25 Alex Sharp (ajsharp)
on 2008-11-17 09:22
I cannot thank you enough for posting this. Have you found a more
elegant way to handle this?
This topic is locked and can not be replied to.