We recently upgraded Closing Time from Rails 3.2 to Rails 4.1. One of the challenges we encountered was moving from attr_accessible to Strong Parameters. At first, we used the protected_attributes gem so we could avoid changing all of our models and controllers, but eventually we decided to bite the bullet and make the change.
The new convention is to define a private method in each controller, e.g.:
class UsersController < ActionController::Base
def create
User.create(user_params)
end
private
def user_params
params.require(:user).permit(:username, :password)
end
endThat’s all well and good for toy controllers, but it quickly gets unwieldy in real applications, once you start to introduce nesting and attributes that are only permitted for certain types of users or certain controller actions:
class UsersController < ActionController::Base
def create
User.create(user_params)
end
private
def user_params
user_attrs = [:username, :password]
person_attrs = [:name]
# admins can change the is_admin flag
user_attrs << :is_admin if current_user.admin?
# email address can be set on create, but not changed after the fact
person_attrs << :email if action_name == 'create'
params.require(:user).permit(*user_attrs, person_attributes: person_attrs)
end
end
Not only has that become much harder to read, but you also end up with a lot of duplication. Every controller that accepts user_attributes or person_attributes now has to duplicate code from the UsersController. We didn’t like that very much.
Ryan Bates did an excellent RailsCast on the subject, where he proposes moving this logic into a separate PermittedParams class. We thought this was a great start, but not quite DRY enough for our tastes; we found that our PermittedParams class had a lot of boilerplate:
class PermittedParams < Struct.new(:params, :user)
def user
params.require(:user).permit(*user_attributes)
end
def person
params.require(:person).permit(*person_attributes)
end
def user_attributes
user_attrs = [:username, :password, person_attributes: person_attributes]
user_attrs << :is_admin if user.admin?
end
def person_attributes
person_attrs = [:name]
person_attrs << :email if params[:action] == 'create'
end
end
Note how for each controller foo we have to define this method:
def foo
params.require(:foo).permit(*foo_attributes)
end
The foo_attributes methods are a bit kludgey as well. I don’t like the fact that we have to define a local variable to hold the array of whitelisted attribute names, nor do I like the fact that when you’re nesting a person, you have to write person_attributes: person_attributes.
We came up with a solution and packaged it in a gem called permitted_params. Here’s how you use it:
- Add
gem 'permitted_params'to your Gemfile. - Create an initializer, e.g.
config/initializers/permitted_params.rb: - Then in your controller:
PermittedParams.setup do |config|
config.user do
scalar :username, :password
scalar :is_admin if current_user.admin?
nested :person
end
config.person do
scalar :name
scalar :email if action_is :create
end
end
class UsersController < ActionController::Base
def create
User.create(permitted_params.user)
end
end
Much more readable, no? I’d love to hear your feedback and suggestions for improving the gem.