Protected Attributes

3 min read Original article ↗

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
end

That’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:

  1. Add gem 'permitted_params' to your Gemfile.
  2. Create an initializer, e.g. config/initializers/permitted_params.rb:
  3. 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
  4. Then in your controller:
  5. 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.