14
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

DeviseのコードリーティングでRailsを学ぶ

Last updated at Posted at 2019-06-01

普段RailsでWebアプリの開発をしていますが、使わない機能や内部の処理など知らないことは多いです。そこでDeviseで使う機能の実装をみて知見を増やそうかと思います。なぜ、Deviseかというとボリュームが多いGemが良かったのと、認証周りの詳しい動きが知りたかったからです。

概要

今回はauthenticate_user!に焦点を当ててコードの中身を見ていこうと思います。authenticate_userは認証済みのユーザーか確認するメソッドでDeviseで最もよく見るメソッドの1つかと思います。

またコードを読むDeviseのバージョンはv4.6.2にしています。

define_helpers

authenticate_user!の定義場所を探そうとしたら、まずはdefine_helpersというメソッドに出会います。define_helpersでは認証で利用するメソッドの定義をしています。  

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

class_eval

class_eval <<-METHODS, __FILE__, __LINE__ + 1でヒアドキュメントとなっているところが定義部です。authenticate_user!以外にもcurrent_userなどもここで定義しているようです。class_eval`はメタプログラミングで使われるメソッドでこれを使用すると引数に渡した文字列をメソッド定義のコードとして読んでくれます。

Devise利用では多くは認証のモデル名をUserにしますが、任意のモデル名にすることができます。その場合はauthenticate_***と動的に定義しないのでclass_evalを使ってメソッドを実装しています。

class_evalを試しに使ってみた例です。

devise_memo.rb

class DeviseMemo
  def self.define_methods(mapping)
    class_eval <<-METHODS, __FILE__, __LINE__ + 1
      def authenticate_#{mapping}!
        p "called authenticate_#{mapping}!"
      end

      def #{mapping}_signed_in?
        p "called #{mapping}_signed_in?"
      end
    METHODS
  end
end

DeviseMemo.define_methods("user")
devise_memo = DeviseMemo.new
devise_memo.authenticate_user!
devise_memo.user_signed_in?
$ ruby devise_memo.rb
"called authenticate_user!"
"called user_signed_in?"

warden

authenticate_user!の中身は短い、というか結局はwardenというGemを使って認証確認しているようですね。
ここを見ないといけなくなりました。これは次の機会にしておきます。

def authenticate_#{mapping}!(opts={})
  opts[:scope] = :#{mapping}
  warden.authenticate!(opts) if !devise_controller? || opts.delete(:force)
end

current_useruser_sessionもwardenを使っているので、Deviseの根幹を握っているようです。

add_mapping

define_helpersの呼び出し場所を確認してみるとlib/devise.rbの中にあるadd_mappingで呼ばれています。

https://github.com/plataformatec/devise/blob/2e5b5fcd705b06c518ab0156b96badb91c4cb6ea/lib/devise.rb#L349

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

ちなみにadd_mappingdevise_forの中で呼ばれます。devise_forも次の機会で詳しく中身を見ていきたいです。

lib/devise/rails/routes.rb
def devise_for(*resources)
  @devise_finalized = false
  raise_no_secret_key unless Devise.secret_key
  options = resources.extract_options!

  (省略)

  resources.each do |resource|
    mapping = Devise.add_mapping(resource, options)

extract_options!は可変長の引数を分解できるメソッドです。
下記が簡単な例です。extract_options!はActiveSupportのメソッドなのでRailsコンソールで試すのが楽です。

def devise_for_test(*resources)
  p resources
  options = resources.extract_options!
  p resources
  p options
end

devise_for_test :users, controllers: [ sessions: 'users/sessions' ]
# 出力

[:users, {:controllers=>[{:sessions=>"users/sessions"}]}]
[:users]
{:controllers=>[{:sessions=>"users/sessions"}]}

devise_forでモデル名とオプションに分けられてDevise.add_mapping(resource, options)と呼び出されます。
そしてadd_mapping内でmapping = Devise::Mapping.new(resource, options)とマッピングを作成しています。

mappingdefine_helpersmapping = mapping.namenameだけが使われていましたが、Devise::Mappingの中を覗いてみると、

lib/devise/controllers/helpers.rb
class Mapping #:nodoc:
  
  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

となっています。
nameに注目してコードを繋げてみると、認証モデルがUserである場合は@singular = :users.to_s.tr('/', '_').singularize.to_symとみれます。singularizeは複数形を単数形に変換するメソッドで、最終的に@singular = :userとなりsingularのエイリアスがnameとなっているのでmapping.name:userが取得できます。
するとdefine_methodsの引数に:userが渡されauthenticate_user!が出来上がるという流れになっています。

最後に

authenticate_user!をみても普段使わないようなメソッドや手法がありなかなか有意義でした。これはDeviseのほんの一部なのでまだまだみるべきところはたくさんあります。今後もDeviseのコードリーディングで勉強していこうと思いますが、今日の学びからwardenの方を先に読んでいくことになりそうです。

参考記事

14
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?