[Update: Stephan Wehner actually showed me how to get this working (see his branch). Something else was wrong in the program I was actually using, which went away when I built this simplified version- I just never really tested with the simplified version. Thus, this post is now pointless. Thanks Stephan.]
Recently, I had need to create a plugin that held a polymorphic :has_many, :through relationship. I spent a day or so trying to get this to work, only to realize that it is essentially impossible to do inside a pluggable Rails module, but possible if you templatize the classes.
PolymorphicPlugin Example Project
There’s a StackOverflow thread that describes this problem and the solution a bit; however, Based on the difficulty, and the fact that I couldn’t find any documentation specific to this problem, I figured I’d write it up as an example, and create a PolymorphicPlugin gem to illustrate it better.
The PolymorphicPlugin inside-module branch is an example of the way you’d distribute a pluggable Rails module with all classes contained in the module. The generator only creates a migration in the Rails project.
The PolymorphicPlugin outside-module branch is an example of the way you’d distribute a pluggable Rails module in such a way that the generator creates a migration in and also copies model files to the Rails project.
Problem Description
The idea is that there’s an “acts_as” like plugin that I want to add to my Rails models:
module PolymorphicPlugin
module PolymorphicHolder
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def holds_things
has_many :thing_groupings, :as => :thingable
has_many :things, :through => :thing_groupings
include PolymorphicPlugin::PolymorphicHolder::InstanceMethods
end
end
module InstanceMethods
def has_things?
true
end
end
end
end
You can see that anything that calls holds_things should have a :has_many relationship with Thing through the ThingGrouping join table. This is a standard paradigm for a polymorphic :has_many, :through relationship.
Basically, I want to do this:
class User < ActiveRecord::Base holds_things end class Admin < ActiveRecord::Base holds_things end user = User.new user.has_things? -> true admin = Admin.new admin.has_things? -> true
The failing case
Seems pretty simple, right?
Turns out it’s not. Take a look at the directory layout that I tried to use. This seems like a standard for a gemmable plugin. The model files are contained in the gem library, and accessed as part of the module. When you use gem 'polymorphic_plugin', the only thing that gets copied to your Rails project is the migration file. Nice and clean.
Unfortunately, that doesn’t work. If you clone this branch and run rake test you’ll get exceptions. Specifically, a NameError: uninitialized constant User::ThingGrouping because our User model, by extending the PolymorphicHolder module, doesn’t have access to this unknown model called ThingGrouping.
And no, using things like :class_name and :source were no help. I tried everything, or seemingly everything. Nothing would make this work.
The passing case
That is until I pulled the models outside of the plugin module.
Take a look at the directory layout now and you’ll notice that the Thing and ThingGrouping models are no longer in the plugin’s lib directory. Moving them to the templates directory is the unfortunate answer.
I say “unfortunate” because this means that the models need to be distributed by copying them to the Rails project. In other words, there’s a line in my PolymorphicPluginGenerator that reads: template "thing.rb", File.join('app/models', "thing.rb"). In other words: Copy the Thing model into this project so it can be accidentally futzed with by users and maybe, if we’re lucky, cause some interesting name collisions.
Thing and ThingGroup are no longer contained inside my PolymorphicPlugin module. They are top-level classes equivalent to anything generated with rails generate model. Not a solution that I’m fond of, but one that passes my tests. We can at least have a polymorphic :has_many, :through relationship now.
Outcome
Polymorphic :has_many, :through relationships are available in pluggable Rails modules, but it’s a bit of a kludge. You’ll need to make sure you name your polymorphic classes smartly to avoid name collisions with the calling Rails project. You’d hate to create a plugin with a model like Book only to find that Book already exists in someone’s Rails project!
I’d love to be proven wrong on this, by the way. Go ahead and grab the Github repository and play with the inside-module branch. If you can make it work, I’ll buy you a beer!





