Rubyのgem、deviseをコードリーディングして理解を深める
良質なコードを読むことの大切さをこちらの文章をもとに
前回の投稿で記載させていただきました。
掲題にもあるとおり、gemのdeviseを読み解きたいと思います。
自分のアプリケーション開発でも使用頻度が高く、大枠の機能ができていることや、deviseを導入すれば”signed_in”や"current_user"が使えるようになりますが、それが何故そうなっているのか本質を理解できておらず、コードリーディングすることで、理解を深めたいと思ったからです。
deviseのすべてを理解しようとすると、ボリュームが多いと思ったので、
今回は"current_user"をピックアップし、深掘りしたいと思います。
そもそもdeviseとは?からです。
それでは、順を追って読み解いていきたいと思います。
READMEを読む
deviseは全部で10個のモジュールが存在しており、
これらから、deviseの機能を構成しています。
deviseを導入して主に使用していた機能は、ユーザー登録やログイン根幹になっているであろうRegisterableや、一度サインインすると、毎回ログインせずとも一定時間内であればログインできるようcookie上で記憶できるように処理するRememberable、一定期間のログインが無いとログインを求めるTimeoutableが多そうです。
今回のcurrent_userは、この辺りに関連がある機能な気がします。
また、deviseを導入することで利用可能になるヘルパーメソッドとして、
以下の記載を見つけました。
#ユーザーがサインインしているかどうかを確認
user_signed_in?
#現在サインインしているユーザーの場合に使用可能なヘルパー
current_user
#このスコープのセッションにアクセスが可能
user_session
これに加えて、以下の記載に注目しました。
#例えば、DeviseモデルがUserではなくMemberと呼ばれている場合、
利用できるヘルパーは以下の通りであることに注意してください。
before_action :authenticate_member!
member_signed_in?
current_member
member_session
いままで、"user_signed_in?"や、"current_user"は
それが構文の1つとして存在していると思っていましたが、
"(モデル名)_signed_in?"や"current_(モデル名)"が
正確な構文ということがわかりました。
ディレクトリ/ファイル構造を読む
deviseは、以下のファイル群で構成されています。
ファイル構造全体を理解しようとすると、自分のなかでも混乱しそうなので、テーマを絞って進めたいと思います。
今回は"current_user"について調べたいので、ヘルパー関連のファイルを探したところ、2つのファイルを見つけました。
・app>helpers>devise_helper.rb
・lib>devise>controllers>helpers.rb
この2つに絞って見ていきます。
・app>helpers>devise_helper.rbを見る
# frozen_string_literal: true
module DeviseHelper
# Retain this method for backwards compatibility, deprecated in favor of modifying the
# devise/shared/error_messages partial.
def devise_error_messages!
ActiveSupport::Deprecation.warn <<-DEPRECATION.strip_heredoc
[Devise] `DeviseHelper#devise_error_messages!` is deprecated and will be
removed in the next major version.
Devise now uses a partial under "devise/shared/error_messages" to display
error messages by default, and make them easier to customize. Update your
views changing calls from:
<%= devise_error_messages! %>
to:
<%= render "devise/shared/error_messages", resource: resource %>
To start customizing how errors are displayed, you can copy the partial
from devise to your `app/views` folder. Alternatively, you can run
`rails g devise:views` which will copy all of them again to your app.
DEPRECATION
return "" if resource.errors.empty?
render "devise/shared/error_messages", resource: resource
end
end
ここでは、"DeviseHelper"というモジュールの中で、
"devise_error_messages!"メソッドが定義されています。
しかし、英文を読み進めていくと
・現在は"devise/shared/error_messages"で記載されているエラーメッセージをしようしており、このメソッドは廃止予定であること
・互換性のために保守されているメソッドであること
という2点が主に記載されています。
廃止される理由は改めて考えてみたいですが、
devise側でモジュールを設けなくとも、エラーメッセージを
validateとviewのみで完結できてしまうから、とか何かしらの意図があると思うので、ここは別途でもう少し考えたいと思います。
・lib>devise>controllers>helpers.rbを見る
ファイル全体はかなりボリューミーなので、抜粋しています。
def self.define_helpers(mapping) #:nodoc:
mapping = mapping.name
class_eval <<-METHODS, __FILE__, __LINE__ + 1
def authenticate_#{mapping}!(opts = {})
opts[:scope] = :#{mapping}
warden.authenticate!(opts) if !devise_controller? || opts.delete(:force)
end
def #{mapping}_signed_in?
!!current_#{mapping}
end
def current_#{mapping}
@current_#{mapping} ||= warden.authenticate(scope: :#{mapping})
end
def #{mapping}_session
current_#{mapping} && warden.session(:#{mapping})
end
METHODS
ActiveSupport.on_load(:action_controller) do
if respond_to?(:helper_method)
helper_method "current_#{mapping}", "#{mapping}_signed_in?", "#{mapping}_session"
end
end
end
Deviseモジュール>class_evalクラス?>ヘルパーに関係する
メソッドが並んでいるという構造です。
まずは、class_evalがクラスなのか、
はたまた別のものを指しているのかを調べました。
class_evalとは動的にクラス変数を定義できるもの、つまり
・レシーバーであるクラスのインスタンス変数(クラスインスタンス変数)やクラス変数を定義したり、上書きすることができる
・レシーバーであるクラスのインスタンスメソッドを定義したり、上書きすることができる
となるので、ここで"current_(モデル名)"の定義を行なっているようです。
REDOMEに書いてあった通り、deviseのヘルパーメソッドは、
"(モデル名)_signed_in?"や"current_(モデル名)"のように、
紐づいているモデル名によって、メソッド名が変わるため、
class_evalのモジュールを使って、動的にヘルパーメソッドを定義していると理解しました。
それでは、class_evalで定義されているメソッドを細かく見てみます。
def current_#{mapping}
@current_#{mapping} ||= warden.authenticate(scope: :#{mapping})
end
ここでいくつか要素が登場しているので、
分解して整理し、それぞれ調べたいと思います。
・mapping
self.define_helpersメソッドのなかで、定義してあります。
mapping = mapping.name
mappingが別のどこかで定義されているはずなので、
呼び出されている部分を確認し、探します。
lib>devise.rb内で以下の記述を見つけました。
def self.add_mapping(resource, options)
mapping = Devise::Mapping.new(resource, options) #これ
@@mappings[mapping.name] = mapping
@@default_scope ||= mapping.name
@@helpers.each { |h| h.define_helpers(mapping) }
mapping
end
"Devise::Mapping.new(resource, options)"でインスタンスが生成されたものが代入されています。
なので、この要素を調べるべくMappingクラスを探すと、lib>devise>mapping内でMappingクラスがあります。
class Mapping #:nodoc:
attr_reader :singular, :scoped_path, :path, :controllers, :path_names,
:class_name, :sign_out_via, :format, :used_routes, :used_helpers,
:failure_app, :router_name
alias :name :singular #ここ
****
def initialize(name, options) #:nodoc:
@scoped_path = options[:as] ? "#{options[:as]}/#{name}" : name.to_s
@singular = (options[:singular] || @scoped_path.tr('/', '_').singularize).to_sym #ここ
Mapping.newで呼び出されたinitializeメソッドを見ます。
第2引数で指定されているoptionsの理解が追いつかなかったのですが、
nameにdeviseに紐づくモデル名が引数として渡ってきた場合、
@singular = (モデル名).to_s.tr(' / ' , ' _' ).singularize.to_sym
となります。
(モデル名)に続く要素は、以下の通りなので、
@singularには、(モデル名)が代入されます。
また、Mappingクラス内にあるalias~は、
既存のメソッドに対して、別名が付けられるメソッドのため、
singularにnameという名前を付け直していることがわかりました。
つまり、(モデル名)がUserだった場合、mapping.nameには
userという値が入っていることになります。
class_eval内で定義されていたメソッドに戻ります。
def current_#{mapping}
@current_#{mapping} ||= warden.authenticate(scope: :#{mapping})
end
先ほど記載した通り、mappingにはuserが代入されているため、
current_userメソッドが定義されているかたちとなります。
メソッド内で、定義されている"@current_#{mapping}"に自己代入されている値を最後に紐解いていきます。
warden.authenticateのwardenですが、調べてみるとgemの一種でした。
deviseファイル内を調べると、かなり多くの"warden"という記述が見つかりました。そもそもdeviseはwardenをもとにして作成されているようです。
早速wardenとは、、を調べてみました。
このまま、、wardenについて書いてしまうと、
本題から外れてしまう、かつ文量が長くなってしまうため、
一旦、deviseのコードリーディングとしてはここまでにしたいと思います。
はじめてコードリーディングを行いましたが、
それぞれの関数の繋がりや、新たな気付きがあったため
とても有意義だったと思います!
是非、初学者の方は一度お試しください!
長くなりましたが、ここまで読んでいただきありがとうございました!