The polymorphs plugin dynamically injects methods into child models. This means that if you referenced a child model before the parent was loaded, the methods would be missing.
inversion of control or what have you
I solved the problem by adding a dependency injection mechanism. Here’s the entire code:
module Dependencies
mattr_accessor :injection_graph
self.injection_graph = Hash.new([])
def inject_dependency(target, *requirements)
target, requirements = target.to_s, requirements.map(&:to_s)
injection_graph[target] =
((injection_graph[target] + requirements).uniq - [target])
requirements.each {|requirement| mark_for_unload requirement }
end
def new_constants_in_with_injection(*descs, &block)
returning(new_constants_in_without_injection(*descs, &block)) do |found|
found.each do |constant|
injection_graph[constant].each {|req| req.constantize}
end
end
end
alias_method_chain :new_constants_in, :injection
end
explanation, usage
See what it does? Imagine that the Tag and Tagging classes modify the Recipe class (by injecting a new method, or relationship, or something). Normally in development mode if Recipe gets reloaded, the injections will get lost, since Rails doesn’t know it has to re-evaluate Tag and Tagging after Recipes is refreshed. But now we can do as follows:
Dependencies.inject_dependency("Recipe", "Tag", "Tagging")
This way, when the Recipe constant gets refreshed, Tag and Tagging will also get refreshed, and can go patch up Recipe again. Because constantize()
doesn’t reload the same constant multiple times, there is no danger of infinite cycles.
remember, only for development mode
This is not at all useful in production mode, since classes aren’t reloaded. But in development mode it can make a big difference in sanity.
If there is interest I can release it as a separate plugin.
postscript: on the plugin boot process
Also in version 27.1, there is some method chaining to let the plugin finish booting itself after the config.after_initialize
block runs. This is useful because users are supposed to set plugin start-up options in config.after_initialize
(see here).
The startup sequence is like so (compressed from railties/lib/initializer.rb
):
def process
load_environment # environment.rb
load_plugins # init.rb for your plugin
load_observers
initialize_routing
after_initialize # where your user configures your plugin
end
What if things in init.rb
need to know the configuration options? Check lib/has_many_polymorphs/autoload.rb
for an example of a fix.
Unfortunately config.after_initialize
doesn’t allow multiple blocks the way Dispatcher.to_prepare
does. There is a Rails patch waiting to happen here…
This is wonderful news. Too bad it came a couple days after I figured it out.
has_many_polymorphs
is a wonderful addition that I’m truly surprised isn’t already in core. Just wanted to say thank you for putting this tool out to the world.How many plugins is HMP composed of now? 30? 50?
Regardless, cool. We shoulda thought of this sooner. Nice work.
Just wanted to add my thanks.
Thanks!
Why not just hook into
config.to_prepare
slashDispatcher.to_prepare
?Dispatcher.to_prepare
doesn’t prepare the console,script/runner
, migrations, any of that.Yeah, though your
init.rb
covers that, you’re just adding aDispatcher.to_prepare
to handle reloading in dev… but as you say, still doesn’t help when you’ve done areload!
in console.Maybe the best solution would be a proper reload hook for Rails, rather than
to_prepare
?A hack would be to make
reload!
callto_prepare
. Execute this afterreload!
and your all your plugin patchy goodness is available again.Thanks for a series of great articles. Unfortunately, I was unable to get this to work. I, was playing w/ a plugin module called Rubaidh::TabularForm to generate tabular forms. In dev mode, first iteration it works, but afterwards it fails b/c ApplicationHelper has been reloaded and this module, which is included into ApplicationHelper via an
init.rb
filesend(:include)
method, doesn’t get reloaded. I tried to specify the module asreloadable
, but that didn’t do it, and using this Dependencies extension and calling it in my app init configuration didn’t do it either.Any ideas are appreciated. Thanks.
You say the extension “didn’t do it”—do you have more details as to why it didn’t work? It works fine for me.