【sign_up編②】registrations_controllerのbuild_resourceの解説

rails

本記事では、devise_token_authのregistration_controllerのcreateアクション(signup時の処理)の中のbuild_resourceについてまとめました!

ソースコード

本記事では、devise_token_authのregistrations_controllerのcreateアクションの中のbuild_resourceの説明をしていきます。

build_resourceを読解をする

それでは、build_resourceについて中身を見ていきます。

# 101行目
def build_resource
  @resource            = resource_class.new(sign_up_params)
  @resource.provider   = provider

  # honor devise configuration for case_insensitive_keys
  if resource_class.case_insensitive_keys.include?(:email)
    @resource.email = sign_up_params[:email].try(:downcase)
  else
    @resource.email = sign_up_params[:email]
  end
end

見た感じ、割とコードはすっきりとしていますが、特に@resourceのコードは追っていくとかなりボリューミーです。

一つ一つコードを追っていって、コードの内容をしっかりと理解していこうと思います。

@resourceを取得する

@resourceを作成する部分から見ていきます。

# 102行目
@resource = resource_class.new(sign_up_params)

resource_classにparamsを渡してインスタンスを作成しています。

@resourceが何を定義しているのかを知るためには、resource_classと、sign_up_paramsについて知る必要があります。

sign_up_paramsは91行目に、resource_classはapplication_controller.rbに記載があります。

それぞれのコードを追うのが意外と大変だったので、それぞれ解説していきます。

まずはsign_up_paramsを見ていきます。

sign_up_paramsを追っていく

# 91行目
def sign_up_params
  params.permit(*params_for_resource(:sign_up))
end

ここでは、permitで許可するシンボルを定義しています。具体的には、*params_for_resource(:sign_up)で指定していますね。ソースを見ていきます。

管理しているファイルが変わって、application_controller.rbにparams_for_resourceはあります。

# (1)params_for_resource 37行目
def params_for_resource(resource)
  devise_parameter_sanitizer.instance_values['permitted'][resource].each do |type|
    params[type.to_s] ||= request.headers[type.to_s] unless request.headers[type.to_s].nil?
  end
  devise_parameter_sanitizer.instance_values['permitted'][resource]
end

devise_token_authって色々なところにコードがあり、追っていくとどのコードがどれかがわかりづらくなっていくため、番号を振っていきます。(1-1)とか(1-2)とかそんな感じで((1)で追いたいコードが2つある場合に、(1-1),(1-2)としてそれぞれ追っていきます)。

引数のresourceには:sign_upが渡っていますが、まずはdevise_parameter_sanitizerがクラスなのかインスタンスなのかを知る必要がありそう。ソースコードはどこかというと、なんとdeviseにあります。

# (1-1)devise_parameter_sanitizer 158行目
def devise_parameter_sanitizer
  @devise_parameter_sanitizer ||= Devise::ParameterSanitizer.new(resource_class, resource_name, params)
end

devise_token_auth内での処理は、deviseのコードを一部importしており、時にはdeviseのコードを追わないといけないので大変です…

ここでは、Devise::ParameterSanitizerクラスからインスタンスを作成しようとしています。実際に該当するクラスを見てみると、下記のようになっています。

# (1-1-1) ParameterSanitizer 44行目
def initialize(resource_class, resource_name, params)
  @auth_keys      = extract_auth_keys(resource_class)
  @params         = params
  @resource_name  = resource_name
  @permitted      = {}

  DEFAULT_PERMITTED_ATTRIBUTES.each_pair do |action, keys|
    permit(action, keys: keys)
  end
end

クラスが実行された時、initializeメソッドが自動で走り、@で指定されたプロパティを定義しています。

プロパティを全部調査するのは大変なので、今回は取得したいものに絞って調査しようと思います。今回取得していきたいプロパティは、(1)の下記の部分からわかります。

# (1)
devise_parameter_sanitizer.instance_values['permitted'][resource].each do |type|

instance_valuesメソッドはrubyの標準のメソッドで、@を除いた形の変数名と、値とでハッシュ化するものです。instance_valuesメソッドで[‘permitted’]のキーを指定していることから、(1-1-1)の@permittedの値が必要です。

(1-1-1)の@permittedを見てみると、{ }になっているため、一見値が入っていないように見えますが、その下のpermitメソッドで@permittedの値を取得しています。

permit関数の値をみる前に、ループ処理の大元となっているDEFAULT_PERMITTED_ATTRIBUTESを確認をすると下記(1-1-1-1)のようになっています。sign_in、sign_up、account_updateそれぞれでループを回しており、今回だとsign_upの部分が必要そうです。

# (1-1-1-1) DEFAULT_PERMITTEDの中身 37行目
DEFAULT_PERMITTED_ATTRIBUTES = {
  sign_in: [:password, :remember_me],
  sign_up: [:password, :password_confirmation],
  account_update: [:password, :password_confirmation, :current_password]
}

(1-1-1)のループ文において、sign_upのみにフォーカスした場合、permit(action, keys: keys)のactionにはsign_up、keys: keysの右のkeysに[:password, :password_confirmation]が入ります。

permit関数の引数がわかったところで、permit関数の中身を見ていきます。

# (1-1-1-2) permitメソッドの中身 110行目
def permit(action, keys: nil, except: nil, &block)
   if block_given?
     @permitted[action] = block
   end

   if keys.present?
     @permitted[action] ||= @auth_keys.dup
     @permitted[action].concat(keys)
   end

   if except.present?
     @permitted[action] ||= @auth_keys.dup
     @permitted[action] = @permitted[action] - except
   end
 end 

ここで、今回は、keysしか渡されていないので、見るのは2つ目のif keys.present?の部分になります。

ここでは、@auth_keys.dupが定義されており、(1-1-1)の@auth_keysの中身も確認する必要がありそうですね。

(1-1-1)に戻ってみてみると、 @auth_keysはextract_auth_keysメソッドで定義されています。

# (1-1-1) ParameterSanitizer 44行目
def initialize(resource_class, resource_name, params)
  @auth_keys      = extract_auth_keys(resource_class)
  @params         = params
  @resource_name  = resource_name
  @permitted      = {}

  DEFAULT_PERMITTED_ATTRIBUTES.each_pair do |action, keys|
    permit(action, keys: keys)
  end
end

extract_auth_keysメソッドを見てみると、

# (1-1-1-3) extract_auth_keysの中身 157行目
def extract_auth_keys(klass)
  auth_keys = klass.authentication_keys

  auth_keys.respond_to?(:keys) ? auth_keys.keys : auth_keys
end

このようになっています。まず、3行目の処理ですが、今回klassは、resource_classになります。これは、devise_token_authを紐付けたモデルで、大体の人はuserになる(resource_classについては次の章で追っています。)かなと思います。

そして、authentication_keysは、deviseをインストールしたときに作成された、configファイルの中に記載されています。

# (1-1-1-3-1)49行目
# config.authentication_keys = [:email]

多分、何も触ってなければ、上記のようにコメントアウトしてあると思います。コメントアウトしてある場合、config.authentications_keysにはデフォルトで[:email]が入ります。よって、auth_keysには今回だと[:email]が入ります。

auth_keys.respond_to?(:keys)メソッドでは、auth_keysがハッシュとなっている場合にkeyが存在すればtrueとなりますが、今回は配列なので、falseとなり、そのまま[:email]が返されます。

(1-1-1-3)の説明が長くなってしまいましたが、ここでの目的は@auth_keysを取得することで、extract_auth_keys(klass)の返り値が@auth_keysになるため、@auth_keys=[:email]となり、無事取得することができました!

さて、ここで(1-1-1-2)へ戻ります。だいぶ大回りしましたが、元々は、if keys.present?で@auth_keysが必要で@auth_keys探しの旅に出ており、無事見つかって帰還してきた感じです。

# (1-1-1-2) permitメソッドの中身 110行目
def permit(action, keys: nil, except: nil, &block)
   if block_given?
     @permitted[action] = block
   end

   if keys.present?
     @permitted[action] ||= @auth_keys.dup
     @permitted[action].concat(keys)
   end

   if except.present?
     @permitted[action] ||= @auth_keys.dup
     @permitted[action] = @permitted[action] - except
   end
 end 

@permitted[action]には、@auth_keys.dup(@auth_keysのコピー)が入ります。要は@permitted[aciton]に[:email]がそのまま入ります。

そして、@permitted.concat(keys)で@permittedにkeys配列を結合しています。ここで、keysは何だったかというと…

# (1-1-1-1) DEFAULT_PERMITTEDの中身 37行目
DEFAULT_PERMITTED_ATTRIBUTES = {
  sign_in: [:password, :remember_me],
  sign_up: [:password, :password_confirmation],
  account_update: [:password, :password_confirmation, :current_password]
}

ここのsign_upの値の部分でした。よって、@permittedは[:password, :password_confirmation, :email]となることがわかりました!

ここで、@permittedを取得する理由を改めて思い出してみます

今回取得していきたいプロパティは、(1)の下記の部分からわかります。

# (1)
devise_parameter_sanitizer.instance_values['permitted'][resource].each do |type|

instance_valuesメソッドはrubyの標準のメソッドで、キーを@を除いた変数名、バリューを変数の値にしたハッシュを作成するものです。instance_valuesメソッドで[‘permitted’]のキーを指定していることから、(1-1-1)の@permittedの値が必要です。

ということで、(1)に戻ってみましょう。

# (1)params_for_resource 37行目
def params_for_resource(resource)
  devise_parameter_sanitizer.instance_values['permitted'][resource].each do |type|
    params[type.to_s] ||= request.headers[type.to_s] unless request.headers[type.to_s].nil?
  end
  devise_parameter_sanitizer.instance_values['permitted'][resource]
end

ここで、引数のresourceはsign_upでしたね。本来devise_parameter_sanitizer.instance_values[‘permitted’]は、sign_up、sign_in、account_updateそれぞれのデータをとってきますが、[sign_up]とキーを指定することで、[:password, :password_confirmation, :email]を取得することができます。

よって、長くなりましたが、この章で取得したかった*params_for_resource(:sign_up)は:password, :password_confirmation, :emailとなります。*はsplat演算子で、配列は展開されます。

sign_up_paramsは、sign_up時に必要なparamsを指定するものということが分かりました!

# 91行目
def sign_up_params
  params.permit(*params_for_resource(:sign_up))
end

resource_classの処理を追っていく

resource_classについては下記の記事で詳しく調査しています。

結論として、resource_classにはdevise_token_authを紐付けたモデルが入ります。おそらくほとんどの人はUserモデルですかね。よって、下記のコードは、devise_token_authのインスタンスを新しく作成する処理になります。

# 102行目
@resource = resource_class.new(sign_up_params)

@resource.providerを解読する

# 101行目
def build_resource
  @resource            = resource_class.new(sign_up_params)
  @resource.provider   = provider

  # honor devise configuration for case_insensitive_keys
  if resource_class.case_insensitive_keys.include?(:email)
    @resource.email = sign_up_params[:email].try(:downcase)
  else
    @resource.email = sign_up_params[:email]
  end
end

これは、@resourceオブジェクトにprovider属性を付与して、providerを渡しています。providerはどこにあるかというと、concernsにありました。

# 55行目
def provider
  'email'
end

ここではemailが入るみたいです。(関数化する意味はあるのだろうか…)

@resource.emailを解読する

# (1) 101行目
def build_resource
  @resource            = resource_class.new(sign_up_params)
  @resource.provider   = provider

  # honor devise configuration for case_insensitive_keys
  if resource_class.case_insensitive_keys.include?(:email)
    @resource.email = sign_up_params[:email].try(:downcase)
  else
    @resource.email = sign_up_params[:email]
  end
end

ここでは、emailを大文字小文字を区別するかどうかを設定しています。config/initializers/devise.rbで設定することができます。

# (1-1)61行目
config.case_insensitive_keys = [:email]

デフォルトでは、上記のように設定してあります。ここにemailを設定してある場合は、大文字、小文字を区別しません。

emailが設定してある場合は、(1)のif文でtrueとなり、try(:downcase)メソッドにより、大文字が小文字に変換されます。falseの場合は、大文字と小文字を区別するので、paramsはそのまま@resource.emailとして設定できます。

build_resourceまとめ

build_resourceでは、paramsを受け取り、インスタンスを作成するメソッドであることがわかりました。

今回まとめた記事をもとに、createアクションに対する理解を深めていきます。

コメント

タイトルとURLをコピーしました