My Rails views and controllers are littered with redirect_to
, link_to
, and form_for
method calls. Sometimes link_to
and <
This is the simplest solution I was able to come up with with minimal side effect.
class Person < Contact
def self.model_name
Contact.model_name
end
end
Now url_for @person
will map to contact_path
as expected.
How it works: URL helpers rely on YourModel.model_name
to reflect upon the model and generate (amongst many things) singular/plural route keys. Here Person
is basically saying I'm just like Contact
dude, ask him.
Following the idea of @Prathan Thananart but trying to not destroy nothing. (since there is so much magic involved)
class Person < Contact
model_name.class_eval do
def route_key
"contacts"
end
def singular_route_key
superclass.model_name.singular_route_key
end
end
end
Now url_for @person will map to contact_path as expected.
You can try this, if you have no nested routes:
resources :employee, path: :person, controller: :person
Or you can go another way and use some OOP-magic like described here: https://coderwall.com/p/yijmuq
In second way you can make similar helpers for all your nested models.
Overriding model_name
seems dangerous. Using .becomes
seems like the safer option.
One issue is in cases where you don't know what model you are dealing with (and thus the base model).
I just wanted to share that in such cases, one can use:
foo.becomes(foo.class.base_class)
For ease of use, I've added this method to my ApplicationRecord
:
def becomes_base
becomes(self.class.base_class)
end
Adding .becomes_base
to a few route helper methods doesn't seem like too big of a deal to me.
I recently documented my attempts to get a stable STI pattern working in a Rails 3.0 app. Here's the TL;DR version:
# app/controllers/kase_controller.rb
class KasesController < ApplicationController
def new
setup_sti_model
# ...
end
def create
setup_sti_model
# ...
end
private
def setup_sti_model
# This lets us set the "type" attribute from forms and querystrings
model = nil
if !params[:kase].blank? and !params[:kase][:type].blank?
model = params[:kase].delete(:type).constantize.to_s
end
@kase = Kase.new(params[:kase])
@kase.type = model
end
end
# app/models/kase.rb
class Kase < ActiveRecord::Base
# This solves the `undefined method alpha_kase_path` errors
def self.inherited(child)
child.instance_eval do
def model_name
Kase.model_name
end
end
super
end
end
# app/models/alpha_kase.rb
# Splitting out the subclasses into separate files solves
# the `uninitialize constant AlphaKase` errors
class AlphaKase < Kase; end
# app/models/beta_kase.rb
class BetaKase < Kase; end
# config/initializers/preload_sti_models.rb
if Rails.env.development?
# This ensures that `Kase.subclasses` is populated correctly
%w[kase alpha_kase beta_kase].each do |c|
require_dependency File.join("app","models","#{c}.rb")
end
end
This approach gets around the problems that you list as well as a number of other issues that others have had with STI approaches.
If I consider an STI inheritance like this:
class AModel < ActiveRecord::Base ; end
class BModel < AModel ; end
class CModel < AModel ; end
class DModel < AModel ; end
class EModel < AModel ; end
in 'app/models/a_model.rb' I add:
module ManagedAtAModelLevel
def model_name
AModel.model_name
end
end
And then in the AModel class:
class AModel < ActiveRecord::Base
def self.instanciate_STI
managed_deps = {
:b_model => true,
:c_model => true,
:d_model => true,
:e_model => true
}
managed_deps.each do |dep, managed|
require_dependency dep.to_s
klass = dep.to_s.camelize.constantize
# Inject behavior to be managed at AModel level for classes I chose
klass.send(:extend, ManagedAtAModelLevel) if managed
end
end
instanciate_STI
end
Therefore I can even easily choose which model I want to use the default one, and this without even touching the sub class definition. Very dry.