8.2 ログイン
モデルを使わずにSessionを使う。
class ApplicationController < ActionController::Base
include SessionsHelper
end
ApplicationControllerでモジュール(便利メソッドの詰め合わせ)を展開したら、ApplicationControllerを継承しているすべてのControllerで使えるようになる。DRY(コードを重複させない)にするために必要な機能。
8.2.1 log_inメソッド
session[:user_id] = user.id
Railsのsessionメソッドでブラウザに保存。変数のようにブラウザにアクセスできる。
演習 1
有効なユーザーで実際にログインし、ブラウザからcookiesの情報を調べてみてください。このとき、sessionの値はどうなっているでしょうか? ヒント: ブラウザでcookiesを調べる方法が分からない? 今こそググってみるときです!(コラム 1.2)
確認のみなので省略。
演習 2
先ほどの演習課題と同様に、Expiresの値について調べてみてください。
Expires=有効期限(ブラウザ セッションの終了時)
8.2.2 現在のユーザー
<%= current_user.name %>
current_user
メソッド:ログインしていたら、ログインしている情報を返してくれるメソッド。
def current_user
if session[:user_id]
User.find_by(id: session[:user_id])
end
end
User.find_by(id: session[:user_id])
はDBに問い合わせをしている。if文を使うことで、DBへの問い合わせをできるだけ減らしている。
ログインしているか、していないかでレイアウトを変更したい→何度もDBにログインしているか確認する場面が出てくる=何度もlogged_in?メソッド、current_userメソッドが呼び出されてしまう。
module SessionsHelper
# 渡されたユーザーでログインする
def log_in(user)
session[:user_id] = user.id
end
# 現在ログイン中のユーザーを返す(いる場合)
def current_user
if session[:user_id]
@current_user ||= User.find_by(id: session[:user_id])
end
end
# ユーザーがログインしていればtrue、その他ならfalseを返す
def logged_in?
!current_user.nil?
#current_userがnil"ではない"からログインしている
end
end
そこで便利なのが、インスタンスメソッド
@current_user ||= User.find_by(id: session[:user_id])
1回のリクエストに対して何度もcurrent_userが呼び出されても、1回インスタンスメソッドにしておけば、何度もDBに問い合わせしなくてもよくなる。
上記のコードを省略せずに書くと、下記のとおり。
@current_user = @current_user || User.find_by(id: session[:user_id])
# or 左側または右側のどちらかがtrueだったらtrueを返す
# コンピュータの原則として、左から順に処理をしていく
# ||の左側がtrueだったら右側は実行しない(if => false)
# ||の左側がfalse/nilだったら右側を実行する(if => true)
演習 1
Railsコンソールを使って、User.find_by(id: ...)で対応するユーザーが検索に引っかからなかったとき、nilを返すことを確認してみましょう。
>> User.find_by(id: 100)
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 100], ["LIMIT", 1]]
=> nil
演習 2
先ほどと同様に、今度は:user_idキーを持つsessionハッシュを作成してみましょう。リスト 8.17に記したステップに従って、||=演算子がうまく動くことも確認してみましょう。
>> session = {}
=> {}
# sessionに空のハッシュを代入して、空のハッシュが返ってきた
>> session[:user_id] = nil
=> nil
# session[:user_id]にnilを代入してnilが返ってきた
>> @current_user ||= User.find_by(id: session[:user_id])
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" IS NULL LIMIT ? [["LIMIT", 1]]
=> nil
# 上の文章を省略せずに記述すると下記のとおりになる
# @current_user = @current_user || User.find_by(id: session[:user_id])
# 2回目にsession[:user_id] = nil をしているのでnilが返ってきた
>> session[:user_id]= User.first.id
User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
=> 1
# session[:user_id]にUser.first.idを代入して1が返ってきた
>> @current_user ||= User.find_by(id: session[:user_id])
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> #<User id: 1, name: "Rails Tutorial", email: "example@railstutorial.org", created_at: "2021-02-13 18:10:29", updated_at: "2021-02-13 18:10:29", password_digest: [FILTERED]>
# 省略せずに記述すると...
# @current_user = @current_user || User.find_by(id: session[:user_id])
# 3回目に@current_user = nilになっているので、||の右側の処理が実行されて、DBに問い合わせ、User情報が@current_userに代入された。
>> @current_user ||= User.find_by(id: session[:user_id])
=> #<User id: 1, name: "Rails Tutorial", email: "example@railstutorial.org", created_at: "2021-02-13 18:10:29", updated_at: "2021-02-13 18:10:29", password_digest: [FILTERED]>
# 省略せずに記述すると...
# @current_user = @current_user || User.find_by(id: session[:user_id])
# 5回目に@current_user = User.find_by(id: session[:user_id])をしているので、||の左側が実行され、@current_userに入っている情報が返ってきた
# DBに問い合わせはしていない
8.2.3 レイアウトリンクを変更する
<% if logged_in? %>
# trueが返ってきたらログインしている
# ログインユーザー用のリンク
<% else %>
# falseが返ってきたらログインしていな
# ログインしていないユーザー用のリンク
<% end %>
def logged_in?
!current_user.nil?
#current_userがnilではないからログインしている
end
演習 1
ブラウザのcookieインスペクタ機能を使って(8.2.1.1)、セッション用のcookieを削除してみてください。ヘッダー部分にあるリンクは非ログイン状態のものになっているでしょうか? 確認してみましょう。
確認のみなので省略。
演習 2
もう一度ログインしてみて、ヘッダーのレイアウトが変わったことを確認してみましょう。その後、ブラウザを再起動させ、再び非ログイン状態に戻ったことも確認してみてください。注意: もしブラウザの[閉じたときの状態に戻す]機能をオンにしていると、セッション情報も復元される可能性があります。もしその機能をオンにしている場合、忘れずにオフにしておきましょう(コラム 1.2)。
確認のみなので省略。
8.2.4 レイアウトの変更をテストする
def User.digest(string)
cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
# 1行で書くif文
# MIN_COSTがtrue: 開発環境・テスト環境のときは頑張らない
# MIN_COSTがfalse:本番環境のときは元に戻せないくらい頑張る
BCrypt::Password.create(string, cost: cost)
end
test "login with valid information" do
# 正しいデータでログイン情報を渡したときのテスト
get login_path
post login_path, params: { session: { email: @user.email, password: 'password' } }
assert_redirected_to @user
# どこにリダイレクトされるかテスト
follow_redirect!
assert_template 'users/show'
# ログインに成功したらプロフィールページに移動する
assert_select "a[href=?]", login_path, count: 0
# login_pathが無くなっているかどうか
assert_select "a[href=?]", logout_path
# logout_pathがあるかどうか
assert_select "a[href=?]", user_path(@user)
# プロフィールページへのリンクがあるかどうか
end
演習 1
リスト 8.15の8行目にあるif userから下をすべてコメントアウトすると、ユーザー名とパスワードを入力して認証しなくてもテストが通ってしまうことを確認しましょう(リスト 8.26)。通ってしまう理由は、リスト 8.9では「メールアドレスは正しいがパスワードが誤っている」ケースをテストしていないからです。このテストがないのは重大な手抜かりですので、テストスイートで正しいメールアドレスをUsersのログインテストに追加して、この手抜かりを修正してください(リスト 8.27)。テストが red (失敗)することを確認し、それから先ほどの8行目以降のコメントアウトを元に戻すと green (パス)することを確認してください(この演習の修正は重要なので、この先の 8.3のメインのコードにも修正を反映してあります)。
test "login with valid email/invalid password" do
get login_path
assert_template 'sessions/new'
post login_path, params: { session: { email: @user.email, password: "invalid" } }
assert_template 'sessions/new'
assert_not flash.empty?
get root_path
assert flash.empty?
end
演習 2
“safe navigation演算子”(または“ぼっち演算子)と呼ばれる&.を用いて、リスト8.15の8行目の論理値(boolean値)のテストを、リスト 8.2812 のようにシンプルに変えてください。Rubyのぼっち演算子を使うと、obj && obj.methodのようなパターンをobj&.methodのように凝縮した形で書けます。変更後も、リスト 8.27のテストがパスすることを確認してください。
確認のみなので省略します。
8.2.5 ユーザー登録時にログイン
ユーザーの手間にならないようにユーザー登録中にログインを済ませておきましょう。
演習 1
リスト 8.29のlog_inの行をコメントアウトすると、テストスイートは red になるでしょうか? それとも green になるでしょうか? 確認してみましょう。
FAIL["test_valid_signup_information", #<Minitest::Reporters::Suite:0x0000558709955f98 @name="UsersSignupTest">, 12.02340296300099]
test_valid_signup_information#UsersSignupTest (12.02s)
Expected false to be truthy.
test/integration/users_signup_test.rb:23:in `block in <class:UsersSignupTest>'
redになりました。
演習 2
現在使っているテキストエディタの機能を使って、リスト 8.29をまとめてコメントアウトできないか調べてみましょう。また、コメントアウトの前後でテストスイートを実行し、コメントアウトすると red に、コメントアウトを元に戻すと green になることを確認してみましょう。ヒント: コメントアウト後にファイルを保存することを忘れないようにしましょう。また、テキストエディタのコメントアウト機能については『開発基礎編: テキストエディタ』の 「コメントアウト機能」などを参照してみてください。
確認のみなので省略します。
8.3 ログアウト
def log_out
session.delete(:user_id)
@current_user = nil
end
def destroy
log_out
# log_outメソッドを実行
redirect_to root_url
# root_urlに移動
end
log_outメソッドが呼ばれる→root_urlにリダイレクトされる→ヘッダーが非ログイン状態になる
test "login with valid information followed by logout" do
get login_path
post login_path, params: { session: { email: @user.email, password: 'password' } }
assert is_logged_in?
assert_redirected_to @user
follow_redirect!
assert_template 'users/show'
assert_select "a[href=?]", login_path, count: 0
assert_select "a[href=?]", logout_path
assert_select "a[href=?]", user_path(@user)
delete logout_path
assert_not is_logged_in?
# ログインしていないか
assert_redirected_to root_url
# root_urlにリダイレクトされているか
follow_redirect!
assert_select "a[href=?]", login_path
# login_path があるか
assert_select "a[href=?]", logout_path, count: 0
# logout_path が無いか
assert_select "a[href=?]", user_path(@user), count: 0
# user_path が無いか
end
演習 1
ブラウザから[Log out]リンクをクリックし、どんな変化が起こるか確認してみましょう。また、リスト 8.35で定義した3つのステップを実行してみて、うまく動いているかどうか確認してみましょう。
確認のみなので省略します。
演習 2
cookiesの内容を調べてみて、ログアウト後にはsessionが正常に削除されていることを確認してみましょう。
確認のみなので省略します。
さいごに
コードをひとつひとつ、読み下していくと理解がしやすいと思いましたが、時間がかかりがちなのが難点ですね。
「完全なる理解 > まずは前へ進む」を意識しないと、学習のための学習になってしまいそうです。
やはり多くの先輩方がおっしゃるとおり、今はインプットの時期で、ポートフォリオ作成に入るまでは過去の学習を忘れないようにはやく進めるのが良さそうです。