DRYer strong_parameters in Rails
This post documents a very simple pattern for DRYing up strong_parameters knowledge to make your Rails controllers cleaner and virtually maintenance free (at least in the face of model attribute changes).
Starting point
A typical Rails app will have a model with some attributes.
class Window < ActiveRecord::Base
# Window has attributes :manufacturer, :thickness, and :glass_type
end
Following the conventional pattern, the WindowsController
will probably have a private window_params
method to whitelist the attributes allowed by Rails (v4 and up) for mass assigmnent.
class WindowsController < ApplicationController
private
def window_params
params.require(:window).permit(:manufacturer, :thickness, :glass_type)
end
end
If later you add an attribute to Window
, say, :num_panes
, you’ll need to append it to your permitted attributes.
class WindowsController < ApplicationController
private
def window_params
params.require(:window).permit(:manufacturer, :thickness, :glass_type, :num_panes)
end
end
Adding complexity
There are two latent problems (not problems per se, but… perhaps inconveniences?) with this approach.
First, the diff for your commit will include a patch to app/controllers/windows_controller.rb
when in reality it’s very likely you made no substantial changes to that file. Mechanically you DID alter the code there, but there was no logical modification (in the abstract). The point here is minor - perhaps even philosophical - but it makes for unnecessarily noisy commits, which makes for more difficult debugging later.
Second, suppose your app had a House
concept.
class House < ActiveRecord::Base
has_many :windows
accepts_nested_attributes_for :windows
end
Which had the same whitelisting in its controller.
class HousesController < ApplicationController
private
def house_params
params.require(:house).permit(
:address, :color, :value,
window_attributes: [:manufacturer, :thickness, :glass_type],
)
end
end
Oops! Not only are we duplicating knowledge (the list of window_attributes
), but we already forgot to add a reference to :num_panes
in HousesController#house_params
. No bueno.
DRY out this knowledge
There’s a quick and easy solution, which is to declare a class method on each model returning an array of whitelisted attributes for that model.
class Window < ActiveRecord::Base
# Window has attributes :manufacturer, :thickness, :glass_type, and :num_panes
def self.window_params
[:manufacturer, :thickness, :glass_type, :num_panes]
end
end
Now our WindowsController
can be cleaned up.
class WindowsController < ApplicationController
private
def window_params
params.require(:window).permit(*Window.window_params)
end
end
As can our HousesController
.
class HousesController < ApplicationController
private
def house_params
params.require(:house).permit(
:address, :color, :value,
window_attributes: Window.window_params,
)
end
end
And neither needs to be changed as we modify information in our Window
model. If we apply the same technique to the attributes of House
as well we can remove even more knowledge that doesn’t belong from that controller.
class HousesController < ApplicationController
private
def house_params
params.require(:house).permit(
*House.house_params,
window_attributes: Window.window_params,
)
end
end
Adding [more] complexity
Suppose we now introduce a Vehicle
class. We decide, in our infinite wisdom, that we’ll share code using a concern. We can reuse the same pattern to keep our controllers tight.
module Windowable
extend ActiveSupport::Concern
included do
has_many :windows
accepts_nested_attributes_for :windows
def self.windowable_params
{ window_attributes: Window.window_params }
end
end
end
With all of our models using this pattern, our final HousesController
is completely oblivious to the implementation details of our models.
class HousesController < ApplicationController
private
def house_params
params.require(:house).permit(*House.house_params, House.windowable_params)
end
end
And we can live happily ever after knowing that as we add, remove, and modify attributes on our models, we have a single source of truth for whitelisted attributes. AND this truth is grouped with all the other authoritative information about the state of the model; that is to say, it lives in the model itself. Win-win.
A word about purism
There is at least one possible argument against this approach — the pattern I’ve outlined does violate an academically strict understanding of minimal coupling. Rails 4 introduced strong_parameters with the express intent of moving parameter whitelisting behavior OUT of the model and INTO the controller. While this pattern doesn’t violate that intent it does keep the configuration of said whitelisting in the model. You might even call it an antipattern.
But no matter which way you slice it, either you’ll be duplicating knowledge or violating a high-brow interpretation of encapsulation. So you’ll have to decide which brand of Evil™ you prefer to drink.
Constructive feedback and/or discourse? Comment below or tweet me. Send all hatemail to /dev/null
(if you don’t, I will).