今回はRailsのコントローラでは必須のStrong Parameterとメタプログラミングを使ってコードをすっきりさせてみます。
With this plugin Action Controller parameters are forbidden to be used in Active Model mass assignments until they have been whitelisted. This means you’ll have to make a conscious choice about which attributes to allow for mass updating and thus prevent accidentally exposing that which shouldn’t be exposed. rails/strong_parameters
簡単に言うと、どのパラメータをモデルに渡すかを決めるホワイトリスト方式のパラメータフィルターのことです。 ホワイトリスト方式でフィルタリングすることで、DBのカラムの不正な更新や作成を防ぎます。
上記したように、DBのカラムを不正(予期せず)更新しないためにコントローラでフィルタリングしますが、create時やupdate時でどのパラメータを通すか分けて書いたり、モデルの数が増えると同じコードがそこら中のコントローラに散見されるので、メタプログラミング利用してDRYにしようという背景があります。
今回は以下のような方針で実装します。
通常のような場合だと以下のようなコードに書かれると思います。今回のケースではupdateではemailとpasswordの変更はpeopleのコントローラを通してはできないという想定で行きたいと思います。
class ::PeopleController < ::BaseController
before_action :find_person!, only: [:update]
def create
@person = Person.new permit_create_params
end
def update
@person.update! permit_update_params
end
private
def find_person!
Person.find params[:id]
end
def permit_create_parasms
params.require(:person).permit
[:name, :email, :password, :age, :phone_number]
end
def permit_update_parasms
params.require(:person).permit [:name, :age, :phone_number]
end
end
この状態だとコントローラがDBにどんなカラムを持っているのか常に知っていないと行けないのでカラムの変更があった時にコントローラも変更し忘れないうにしないといけません。(例えば新たにlocationというカラムが増えたとか…)
これだと、疎結合ではないので手法1の方針で書き換えると以下のようになります。
class ::PeopleController < ::BaseController
before_action :find_person!, only: [:update]
def create
@person = Person.new permit_create_params
end
def update
@person.update! permit_update_params
end
private
def find_person!
Person.find params[:id]
end
def permit_create_parasms
params.require(:person).permit ::Person::ATTRIBUTES_FOR_CREATE
end
def permit_update_parasms
params.require(:person).permit ::Person::ATTRIBUTES_FOR_UPDATE
end
end
class ::Person < ActiveRecord::Base
ATTRIBUTES_FOR_CREATE = [:name, :email, :password, :age, :phone_number]
ATTRIBUTES_FOR_UPDATE = [:name, :age, :phone_number]
end
まずどのパラメータを通すかはモデルに定数を用意します。こうする事によってコントローラがモデルのカラムを気にしなくていいので疎結合になります。
コントローラから見るとDBのカラムを把握して、createの時は何を通して、updateの時は何を通すということを意識せずにモデルから言われたものだけ通すと言うかたちになりより抽象的になります。DBに変更があったときでもmodelのファイルだけ気にすればいいのがうれしいですね。
ストロングパラメータの処理はpeopleだけじゃなくて他のコントローラでも書かないといけないです。(仮にcompanyのコントローラがあったり) 今回コントローラはBaseControllerを継承しているということを想定して、BaseControllerにメソッドを定義するというのは一つの手です。
class ::BaseController < ActionController::Base
def permit_create_parasms model
params.require(model).permit "#{camelized(model)}::ATTRIBUTES_FOR_CREATE)
end
def permit_update_parasms model
params.require(model).permit "#{camelized(model)}::ATTRIBUTES_FOR_UPDATE)
end
end
class ::PeopleController < ::BaseController
before_action :find_person!, only: [:update]
def create
@person = Person.new permit_create_params :person
end
def update
@person.update! permit_update_params :person
end
private
def find_person!
Person.find params[:id]
end
end
これならコントローラからも自分のモデルを引数に渡してもらえればBaseControllerに書かれたストロングパラメータが機能します。
でもpeople_controller.rb単体で見た時にこのメソッドはどこからでてきたんだ?みたいな印象を受けます。
それからupdateとcreateという字面だけ違うだけで同じ処理をしているので、まだ無駄あるように見えます。
メソッド名をpermit_paramsに変えて引数にmodelとactionの2つをとるという方法も考えられますが、別の方法でまとめたいと思います。 手法2で述べたようにどのアクションにストロングパラメータを適用するか明示的に宣言できるようにします。
class ::BaseController < ActionController::Base
class << self
def define_permit_params model, actions
actions.each do |action|
define_method "permit_#{action}_params" do
params.require(model).permit(
"#{camelized(model)}::#{model_attr(action)}".constantize)
end
end
end
end
private
def model_attr action
"attributes_for_#{action}".upcase
end
def camelized model
model.to_s.camelize
end
end
end
class ::PeopleController < ::BaseController
before_action :find_person!, only: [:update]
define_permit_params :person, [:create, :update]
def create
@person = Person.new permit_create_params
end
def update
@person.update! permit_update_params
end
private
def find_person!
Person.find params[:id]
end
end
BaseControllerの仲が一気に複雑になりました。 やっていることはBaseControllerの特異メソッドを作り、そのメソッドが呼ばれたらdefine_methodを利用してpermit_create_paramsやpermit_update_paramsを動的に定義しようということです。そしてPeopleControllerの中でどんなアクションにストロングパラメータを適応するか宣言するようにします。 こうすることでpeople_controller.rbを単体で見た時にpermit_create_paramsのメソッドがコントローラ内で定義されたんだなと知らない人が見ても感じ取れます。
最後に、BaseControllerのdefine_permit_paramsメソッドが複雑すぎて、今度はbase_controller.rbを見た時に何をしているのかわかりづらくなるのでモジュールにして何をしてるのか抽象的にしてしまいましょう。
class ::BaseController < ActionController::Base
include StrongParameterDefiner
end
module StrongParameterDefiner
extend ActiveSupport::Concern
module ClassMethods
def permit_params model, actions
actions.each do |action|
define_method "#{model}_#{action}_params" do
params.require(model).permit(
"#{camelized(model)}::#{model_attr(action)}".constantize)
end
end
end
end
private
def model_attr action
"attributes_for_#{action}".upcase
end
def camelized model
model.to_s.camelize
end
end
ActiveSupport::ConcernをextendすることでClassMethods内に書かれたメソッドはミックスインしたクラスの特異メソッドとしてextendされます。 BaseControllerの責任も軽くなり、モジュールの名前から何をやっているのかわかりやすくなりました。
最終的には以下ようなコードになりました。 記述量が増えてしまいましたが、コントローラの中がスッキリ保たれて良いのではないかと思っています。 これで、どんなモデルコントローラでもdefine_permit_paramsと定数を用意するだけでストロングパラメータを利用することができるようになりました。
class ::PeopleController < ::BaseController
before_action :find_person!, only: [:update]
define_permit_params :person, [:create, :update]
def create
@person = Person.new permit_create_params
end
def update
@person.update! permit_update_params
end
private
def find_person!
Person.find params[:id]
end
end
class ::Person < ActiveRecord::Base
ATTRIBUTES_FOR_CREATE = [:name, :email, :password, :age, :phone_number]
ATTRIBUTES_FOR_UPDATE = [:name, :age, :phone_number]
end
class ::BaseController < ActionController::Base
include StrongParameterDefiner
end
module StrongParameterDefiner
extend ActiveSupport::Concern
module ClassMethods
def permit_params model, actions
actions.each do |action|
define_method "#{model}_#{action}_params" do
params.require(model).permit(
"#{camelized(model)}::#{model_attr(action)}".constantize)
end
end
end
end
private
def model_attr action
"attributes_for_#{action}".upcase
end
def camelized model
model.to_s.camelize
end
end