【Rails】undefined method `メソッド名(authenticate)’ for nil:NilClass
はじめに
前提
筆者はRailsチュートリアル第7版の動画(テキストも付属)を購入済みですが、8章以降が未リリースであるとの認識の下、ここでは第6版の教材を引用します。第7版のコードや説明と異なる部分があるかもしれませんが、ご了承ください。
引用
動画「前編: Sessionsコントローラ、ログインフォーム」
テキスト「8.1.2 ログインフォーム」
エラー発生コード
class SessionsController < ApplicationController
def new
end
# POST /login
def create
user = User.find_by(params[:session][:email].downcase)
if user.authenticate(params[:session][:password])
# Success => redirect to the user page
else
# Failure => Display error message
render 'new'
end
end
end
エラー内容
このエラー「NoMethodError: undefined method authenticate for nil:NilClass」は、user変数がnilである状態でauthenticateメソッドを呼び出そうとしているために発生。
rails console で確認。
irb(main):006> user = User.find_by(email: '')
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", ""], ["LIMIT", 1]]
=> nil
nilオブジェクトに対してauthenticateメソッドを呼び出してみる。
irb(main):009> nil.authenticate
(irb):9:in `<main>': undefined method `authenticate' for nil:NilClass (NoMethodError)
nil.authenticate
^^^^^^^^^^^^^
RubyではnilはNilClassクラスのインスタンスだが、NilClassにはauthenticateというメソッドが定義されていないためエラーが発生。
※ タイポなどでデータベースに存在しないemailを取り出そうとしてもnilが返ってくる。
エラー解決コード
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by(email: params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
# Success => redirect to the user page
else
# Failure => Display error message
render 'new'
end
end
def destroy
end
end
具体的な修正の解説
User.find_byメソッドが一致するユーザーを見つけられない場合、結果としてnilが返される。そのため、Userオブジェクトを操作する前に、その変数がnilではないかをチェックする必要がある。具体的には、Userオブジェクトがnilの場合、User認証は失敗する一方、適切な値を受け取った場合は、ログイン処理が成功するようにコードを修正する。
以下のコードでは、userがnilではなく、かつuser.authenticate(params[:session][:password])
がtrueを返す場合にログイン成功の処理を行う。
# POST /login
def create
user = User.find_by(params[:session][:email])
if !user.nil? && user.authenticate(params[:session][:password]) # ここ
# Success => redirect to the user page
else
# Failure => Display error message
render 'new'
end
end
以下は、上記の条件式をより簡略化したコードである。Rubyではnilとfalseは条件式でfalseと評価され、それ以外の値はtrueと評価される。そのため、userオブジェクトがnilの場合は自動的にfalseとして扱われ、user.authenticate(...)以降は評価されない。
if user && user.authenticate(params[:session][:password])
この簡略化された条件式では、繰り返しにはなるがuserがnilの場合には最初の条件userがfalseを返し、認証メソッドが呼び出されることはない。userが存在する場合のみ、user.authenticate(...)が評価される。
補足
(このケースで)Userオブジェクトをインスタンス変数(@user)にしない理由
1. Viewでの使用が不要なため:
もしUserオブジェクトをViewテンプレートで使用しない場合、インスタンス変数にする必要はない。例えば、Userオブジェクトを作成や更新後にリダイレクトするだけのアクションではローカル変数で十分。
2. スコープ限定をするため:
Userオブジェクトがそのコントローラの特定のアクション内でのみ必要である場合、ローカル変数を使用することでスコープを限定し、不必要に他のアクションやビューに変数が露出する(アクセス可能になってしまう)ことを防げる。
参考
最後に
ここまで読んでいただきありがとうございました!
今回の記事が良かったと思ったらぜひ「いいね」を押していただけると嬉しいです(大変励みになります💪)
noteでも記事を執筆していますので、ぜひチェックしてみてください。
👇他にもこのようなことについて記載しているのでぜひチェックしてください!
今回もご精読いただきありがとうございました!!!
Discussion