初心者がDeviseのコードを見て仕組みをふんわり理解する【registrations new】


はじめに

Railsチュートリアルがやっと1周終わった者です。
gemのDeviseを入れてみたら、Railsチュートリアルでの5~6章分の実装が10分ほどで終わり、驚愕しています。

なんでできたのか分からない...
そして、テストも書きたいけどどうやって書いたらいいのか分からない。
そこで「公式やソースコードを読みましょう」ということで、読んでみたけどこれまたさっぱり分からない...ながらも少しずつ理解したいので、読み込んでいきます。

読みながら自分が調べたことをまとめていきます。

・初心者だけどDeviseの仕組みを知りたい

そんな方の役に立てたら幸いです。

DeviseのGitHub
https://github.com/heartcombo/devise

Deviseの概要

GitHubのREADMEの一番最初に、概要の説明があります。

  • Deviseは、Wardenに基づくRails向けの柔軟な認証ソリューションです。
  • ラックベースです。
  • Railsエンジンに基づく完全なMVCソリューションです。
  • 複数のモデルに同時にサインインさせることができます。
  • モジュール性の概念に基づいています。本当に必要なものだけを使用してください。

ここで出てくるWardenというのは認証のためのgemで、devise内でこれを引っ張ってきているようです。

また、10個のモジュールで構成されており、必要なものはコメントアウトを外したりしながら使ってね、ということのようです。
モジュールというかもう機能ですね。
Qiita内で表にしてくださっている方がいらっしゃったので、下記にて引用します。

機能 概要
database_authenticatable サインイン時にユーザーの正当性を検証するためにパスワードをハッシュ化してDBに登録します。認証方法としてはPOSTリクエストかHTTP Basic認証が使えます。
registerable 登録処理を通してユーザーをサインアップします。また、ユーザーに自身のアカウントを編集したり削除できるようにします。
recoverable パスワードをリセットし、それを通知します。
rememberable 保存されたcookieから、ユーザーを記憶するためのトークンを生成・削除します。
trackable サインイン回数や、サインイン時間、IPアドレスを記録します。
validatable Emailやパスワードのバリデーションを提供します。独自に定義したバリデーションを追加することもできます。
confirmable メールに記載されているURLをクリックして本登録を完了する、といったよくある登録方式を提供します。また、サインイン中にアカウントが認証済みかどうかを検証します。
lockable 一定回数サインインを失敗するとアカウントをロックします。ロック解除にはメールによる解除か、一定時間経つと解除するといった方法があります。
timeoutable 一定時間活動していないアカウントのセッションを破棄します。
omniauthable intridea/omniauthをサポートします。TwitterやFacebookなどの認証を追加したい場合はこれを使用します。

引用元:[Rails] deviseの使い方(rails6版)
https://qiita.com/cigalecigales/items/16ce0a9a7e79b9c3974e

また、Deviseは、コントローラーとビュー内で使用するヘルパーを作成します。
よく使用するコマンドを予め設定したものです。

デバイスモデルが 'User'であると想定してヘルパー名は例示していますが、
デバイスモデルがユーザー以外の場合は、「_ user」を「_yourmodel(任意のモデル名)」に置き換えると、同じロジックが適用されます。

こちらもQiita内で表にしてくださっている方がいらっしゃったので引用します。

メソッド 用途   
before_action :authenticate_user! コントローラーに設定して、ログイン済ユーザーのみにアクセスを許可する
user_signed_in? ユーザーがサインイン済かどうかを判定する
current_user サインインしているユーザーを取得する
user_session ユーザーのセッション情報にアクセスする

引用元:Rails deviseで使えるようになるヘルパーメソッド一覧
https://qiita.com/tobita0000/items/866de191635e6d74e392

registrations -- サインアップ・アカウント編集・削除

最も基本的なアカウントのCRUDはこのregistrationが担っているので、ここが分からないと応用の機能部分のコードリーディングは難しそうです。

コード全体を上から調べた範囲で記入してきますが、ちまちま区切っていくので、分かりくいかもしれません。
横にソースコードを置きながら見てもらえれば、ちょっとは分かりやすいかも...

frozen_string_literal

devise/app/controllers/devise/registrations_controller.rb
# frozen_string_literal: true

コメントアウトされていますが、Rubyのバージョンアップに備えた1文のようです。
参考:frozen_string_literalが入って気づいた、メソッド設計の原則
https://qiita.com/jkr_2255/items/300b5db8c1f04e1e2815

prepend_before_action

devise/app/controllers/devise/registrations_controller.rb
class Devise::RegistrationsController < DeviseController
  prepend_before_action :require_no_authentication, only: [:new, :create, :cancel]   
  prepend_before_action :authenticate_scope!, only: [:edit, :update, :destroy]
  prepend_before_action :set_minimum_password_length, only: [:new, :edit]
  • DeviseControllerを継承しています。 ソースコードのファイルをみると、devise_controller.rbはこのモジュールだけでなく、すべてのモジュールへ引き継いでいます。
  • prepend_before_actionbefore_actionより前に実行するメソッドです。アクセスできるアクションがユーザーのログイン状態で制限されるようにしています。

newアクション

devise/app/controllers/devise/registrations_controller.rb
 # GET /resource/sign_up
  def new
    build_resource
    yield resource if block_given?
    respond_with resource
  end
  • ログインするための最初の部分です。
  • resourceはすでにdevise_controllerで定義されています。
devise/app/controllers/devise_controller.rb
  def resource
    instance_variable_get(:"@#{resource_name}")
  end

  # Proxy to devise map name
  def resource_name
    devise_mapping.name 
  end

  alias :scope_name :resource_name
  • instance_variable_getメソッドはインスタンス変数の値を取得して返します。@user =からの定義と同じもののようです。
  • resource内に#{resource_name}という変数がありますが、その下部分で定義されています。
  • devise_mapping.nameこちらもすでにdevise_controllerで定義されてますが、引用記事をみると、別部分にヒントがありそうです。
devise/app/controllers/devise_controller.rb
  def devise_mapping 
    @devise_mapping ||= request.env["devise.mapping"]
  end

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

端的に言うと、mapping.name:userが取得できるのでそれをresource_nameにしているということでしょうか。

  • registrations_controller下部にbuild_resourceがあります。セッションを新しく作るという意味のようです。
devise/app/controllers/devise/registrations_controller.rb
 def build_resource(hash = {}) #build_resource(hash = {})の定義
    self.resource = resource_class.new_with_session(hash, session)
  end
  • 元のregistrations_controller上部へ戻りましょう。buildはほぼnewと近い役割をしています。build_resourceという1文は、データベースから取り出したユーザーのインスタンス変数とセッションを作るということになります。
  • block_given?はメソッドを実行する時にブロックが渡されていればtrueを返し、渡されていない時はfalseを返します。
    • ブロックとは

do ... end または { ... } で囲まれたコードの断片 (ブロックと呼ばれる)を後ろに付けてメソッドを呼び出すと、そのメソッドの内部からブロックを評価できます。ブロック付きメソッドを自分で定義するには yield 式を使います。
引用元:Ruby 2.7.0 リファレンスマニュアル
https://docs.ruby-lang.org/ja/latest/doc/spec=2fcall.html#block

devise/app/controllers/devise_controller.rb
  def respond_with_navigational(*args, &block)
    respond_with(*args) do |format|
      format.any(*navigational_formats, &block)
    end
  end
devise/app/controllers/devise_controller.rb
  def navigational_formats
    @navigational_formats ||= Devise.navigational_formats.select { |format|Mime::EXTENSION_LOOKUP[format.to_s] }
  end

ピンときていないので勉強して加筆・修正したいと思います。

ここまで複雑なんだなGem...たった数行のコードにいろいろなものが凝縮されていてとても勉強になりました。
newアクションしか書けなかったけど、最終的にはregistrations_controllerの各アクションのだけでも読んだ記録を残したい...!

お付き合いいただきありがとうございました。