Ruby on Railsチュートリアル第6版の第8章のまとめです。
個人的に気になったところ、難しかったところ、わからなかったところを中心にまとめていきます。基本的に自分の理解を助けることと、復習しやすくすることを目的とした記事ですが、今Ruby on Railsチュートリアルをやってる人に役立つ情報があるかもしれません。
僕の学習の順番は、1周目はRailsチュートリアル第4版(Rails5.1)を使い、2周目で第6版を使うという感じになってます。これは1周目が終わったところで第6版のWebテキスト版がリリースされたからです。
ちなみに、2周目は解説動画を購入してやってます。オススメです。
第8章 基本的なログイン機構
第8章ではユーザーがログインできるような仕組みを作っていきます。だんだん難しくなってくるので、しっかりと理解しながら進んでいきます。
8.1 セッション
HTTPは1回1回のやり取りが独立したものらしく、HTTPの仕組みだけではログインの仕組みを作れないようです。
昔、PHPを使ってユーザー登録できるWebアプリケーションを作ろうとしたときに苦労しました。結局理解できずに挫折しましたが。。。
今でもちゃんとわかってるかは微妙なところですが、なんとか理解しながら進めてると思ってます。
ということで、いつものようにブランチを作ります。
8.1.1 Sessionsコントローラ
Sessionsコントローラを作って、そこにログインの仕組みを作っていきます。ここでもRESTfulにやっていきます。
今回はUsersコントローラとは違って、7つすべてのルーティングは必要なので、必要なものだけを設定します。
8.1.2 ログインフォーム
ログインフォームを作りますが、今回はユーザー登録のときとは違ってモデルがないので、form_withヘルパーの書き方が違います。
form_with(url: login_path, scope: :session, local: true)
こんな感じに書くんですが、「url: login_path」はいいとして、「scope: :session」がよくわかりません。解説動画で丁寧に説明されてますが、そもそも「scope:」自体の動きについては触れられてなかったと思います。
そうはいっても、すごくわかりやすいので、Railsチュートリアルのテキストでよくわからない場合は、解説動画を見ることをオススメします。

そこでは説明がなかった「scope:」について調べてみました。
「Rails 5.1〜6: ‘form_with’ APIドキュメント完全翻訳」には次のように書かれてます。
inputフィールド名のプレフィックスにスコープを追加します。これにより、送信されたパラメータをコントローラでグループ化できます。
Rails 5.1〜6: ‘form_with’ APIドキュメント完全翻訳
わかるような、わからないような。前半はinputフィールド名にプレフィックス(接頭辞)が追加されるということなので、これは調べてみればわかります。
まず、scopeを付けなかった場合。コードを全部書くと長くなるので、メールアドレスのところまでにしときます。
<%= form_with(url: login_path, local: true) do |f| %>
<%= f.label :email %>
<%= f.email_field :email, class: 'form-control' %>
このように書くと、HTMLは
<label for="email">Email</label>
<input class="form-control" type="email" name="email" id="email" />
となりました。
次に「scope: :session」を付けてみます。今度はform_withのところだけにします。
form_with(url: login_path, scope: :session, local: true)
HTMLは、
<label for="session_email">Email</label>
<input class="form-control" type="email" name="session[email]" id="session_email" />
確かに、labelタグのfor属性とinputタグのid属性に「session_」がついてますね。さらに、inputタグのname属性は「session[email]」となってます。
この形になれば、第7章でやったform_withと同じなので、同じように使うことが出来そうですね。
ついでに、sessionじゃなくて、testに変えてみました。
form_with(url: login_path, scope: :test, local: true)
これは、
<label for="test_email">Email</label>
<input class="form-control" type="email" name="test[email]" id="test_email" />
となりました。sessionのところが全部testに変わってます。ということは、この先でどうなのかはわかりませんが、scopeには自由にな名前が付けられそうです。
8.1.3 ユーザーの検索と認証
先にログインが失敗した場合の処理を実装していきます。
ログインフォームからcreateアクションにリクエストが投げられるので、createアクションを作ります。
render 'new'
これが追加されるんですが、renderメソッドの使い方を「renderを使用する- Railsガイドv6.0」で調べてみました。「2.2.4 まとめ」に書いてありますが、renderメソッドの書き方はたくさんあるみたいです。
上にある書き方だと「同じコントローラのnewテンプレート(ビュー)を表示する」みたいな感じになるようです。
試しに「views/sessions」ディレクトリに「test.html.erb」を作って、
def create
render 'test'
end
として、ログインボタンをクリックしてみたら(createアクションを実行してみたら)、URLが「/login」で、「test.html.erb」が表示されました。デバッグ情報のアクションはcreateでした。
さらに、
def create
render 'rails'
end
def rails
render 'test'
end
として、「rails.html.erb」を作り、ルーティングで
get '/rails', to: 'sessions#rails'
としてみました。この場合はcreateアクションで「rails.html.erb」が表示されました。
ということは、renderメソッドではアクションを経由せずに直接ビューを呼び出してるということですね。
ちなみに、
def create
redirect_to rails_path
end
def rails
render 'test'
end
にすると、railsアクションで「test.html.erb」が表示されました。URLは「/rails」でした。
8.1.4 フラッシュメッセージを表示する
ログインがうまくいかなかったときのエラーメッセージを表示できるようにします。
簡単に実装できますが、別のページに移動してもメッセージが残るというバグが出ます。さらに別のページに移動するとメッセージが消えてくれます。
8.1.5 フラッシュのテスト
バグを直す前に、そのバグをつかまえるテストを書きます。必要なのは統合テストですね。
バグを直すときは先にテストを書くと、また同じバグが出ることを防止(というより、バグが出たときに対処しやすくなる?)できるみたいです。
8.2 ログイン
ログインできたときの処理を実装します。
ここでSessionsヘルパーをApplicationコントローラに読み込ませるようにします。なぜこれをしたのかは、この先でわかります。
8.2.1 log_inメソッド
sessionメソッドが出てきますが、これはSessionsコントローラとは無関係とのことです。わかりにくくて混乱しそうですね。
sessionメソッドはRailsに定義されてるそうです。
「Railsガイド」のセッションに関するところをピックアップして引用してみます。
コントローラ内では、sessionインスタンスメソッドを使ってセッションにアクセスできます。
セッションの値は、ハッシュに似たキー/値ペアを使って保存されます。
セッションに何かを保存したければ、ハッシュのようにキーにそれを割り当てます。
セッションからデータの一部を削除したい場合は、そのキーバリューペアを削除します。
セッションにアクセスする – Railsガイドv6.0
sessionメソッドもRailsでよく出てくる「ハッシュのようなメソッド」みたいですね。なので、log_inメソッドではハッシュのように値を渡してるということです。
ここのところで、userが何回も出てくるのでちょっと混乱しました。
まずlog_inメソッド。
def log_in(user)
session[:user_id] = user.id
end
(app/helpers/sessions_helper.rb)
ここにはuserが3つ出てきてます。
次にSessionsコントローラのcreateアクション。
def create
user = User.find_by(...
if user && user.authenticate(...
log_in user
redirect_to user
else
.
.
.
end
(app/controllers/sessions_controller.rb)
ここは5つですね。
「log_in(user)」のuserと、createアクションのuserは別物で、同じ変数名にする必要はありません。
実際に、
def log_in(u)
session[:user_id] = u.id
end
というように、log_inメソッドのuserだけをuに変更してもテストはGREENで、ログインもできました。この2つを別のものにするとエラーが出ます。
def log_in(u)
session[:user_id] = user.id
end
NameErrorで、「userは定義されてないよ!」と怒られました。
Sessionsコントローラのuserには、ログインしたユーザーが入ってます。User.find_byでデータベースから取り出されてますね。
なので、そのユーザーがlog_inメソッドの引数として与えられて、log_inメソッドでそのユーザーのidがsession[:user_id]に代入されてるということです。このsession[:user_id]はsessionメソッドです。
図にするとこんな感じだと思います。

わざわざまとめる必要もないのかもしれませんが、理解が微妙な感じだと後で大混乱に繋がりそうなので、念のためまとめました。わかんなくなったらこれを見ればいいという安心感。
8.2.2 現在のユーザー
ここまででログインができるようになったので、ログインしてるユーザー(現在のユーザー)のメソッドを追加します。
いろいろコードが変わっていきますが、最終形態はデータベースへの問い合わせを可能な限り少なくしたもののようです。
そこで出てくる、
@current_user ||= User.find_by(id: session[:user_id])
ですが、いきなりこれを見せられたら、何をやってるかわかりません。Railsチュートリアルでも説明されてますが、ここは解説動画の方が圧倒的にわかりやすいと思います。

簡単に言うと、「@current_userに@current_userを代入、@current_userがnilだったら、@current_userにUser.find_by…を代入」という感じです。
コードが短くなりますが、オリジナルアプリケーションを作ってるときにこのコードを思いつける自信は全くありません。。。
8.2.3 レイアウトリンクを変更する
ログインはできるようになりましたが、ログインしてもレイアウトが変わらないので、よくわかりません。それに、ログインしてるのにログインページへのリンクがあるのも変ですよね。
ということで、レイアウトのリンクを変更します。
ここで出てくるlogged_in?メソッドがちょっとわかりにくいなと思います。
def logged_in?
!current_user.nil?
end
(app/helpers/sessions_helper.rb)
「!current_userがnilかどうかをチェックしてる」のか、「current_user.nil?の論理値を反対にしてる」のかはよくわかりませんが、ユーザーがログインしてるとtrueを返すメソッドになってます。
ちなみに、「!」は否定演算子で、論理値が反対になります(trueならfalseを返し、falseならtrueを返す)。
aにnilを入れて、いろいろ試してみました。
>> a = nil
=> nil
>> a
=> nil
>> !a
=> true
>> a.nil?
=> true
>> !a.nil?
=> false
これでわかるように、aがnilなら「!a.nil?」はfalseを返します。
ということで、「!current_user.nil?」は、「current_user」がnilだったらfalse、何か入ってたらtrueを返すことがわかりました。
ログインしてるかどうかをチェックするlogged_in?メソッドができたので、それを使って条件分岐してレイアウトを変更します。
これで完成かと思いきや、BootstrapのjQueryが動かないのでドロップダウンメニューが出てきません。jQueryを有効にするように設定を変更しますが、JavaScriptのコードのようで理解することはやめました。
いつかJavaScriptをしっかり勉強するときにでも、ここのコードが何をやってるか理解しようと思います。
8.2.4 レイアウトの変更をテストする
次はレイアウトのテストです。
テストで使えるログイン可能なユーザーが必要で、そのためにdigestメソッドを独自に定義するみたいですが、Railsチュートリアルの説明ではよくわかりませんでした。
解説動画の説明で理解できた気がします。BCryptにお任せしてたハッシュ化をメソッドで使えるようにしようという感じみたいです。
ちなみに、解説動画ではクラスメソッドとインスタンスメソッドの説明が出てきますが、使い分けはよくわからないまま。でも、ここではクラスメソッドの方がコードが短くなるということまではわかりました。

テストで必要なユーザーとかの設定をして、テストを書いていきます。
テストの
assert_select "a[href=?]", user_path(@user)
(test/integration/users_login_test.rb)
は、プロフィールページへのリンクのチェックです。
対象となってるコードは、
<%= link_to "Profile", current_user %>
(app/views/layouts/_header.html.erb)
です。実際にそこをコメントアウトするとテストは失敗します。失敗したテストも上に書いたところです。
テストが「user_path(@user)」で、テスト対象が「current_user」なので分かりにくいんですが、「current_user」は「user_path(current_user)」の省略形ということを覚えてれば理解できると思います。
Railsチュートリアルにその説明があったので、念のためコードにコメントを入れました。
演習2に出てくる「safe navigation演算子(ぼっち演算子)」も調べてみました。
「Ruby 2.7.0 リファレンスマニュアル」には次のように説明されてます。
safe navigation operator (ぼっち演算子):
NEWS for Ruby 2.3.0 – Ruby 2.7.0 リファレンスマニュアル
object&.foo という形式のメソッド呼び出し形式が追加されました。これは object が nil でないときにメソッド foo を呼び出します。
Rubyでは(他もそうみたいですが)、「&&」で繋がれた条件式は、「左側がtrueのときだけ右側が実行される」みたいなので、
object && object.foo
では、objectがnilだったら右側の「object.foo」は実行されません。ということで、
object && object.foo
object&.foo
この2つは同じ処理になるということなんだと思います。
8.2.5 ユーザー登録時にログイン
ユーザー登録時に自動的にログインできるようにします。これ自体は簡単で、Usersコントローラのcreateアクションにlog_inメソッドを入れるだけです。
log_inメソッド自体はどこにあるかというと、sessionsヘルパーです。SessionsコントローラにあるメソッドをUsersコントローラで使える理由は、Applicationコントローラ(UsersコントローラとSessionsコントローラの継承元)でSessionsヘルパーを読み込んであるからです。
これをテストしようとすると、テストの方でログインしてるかどうかを確認するメソッドが必要になります。ヘルパーメソッドはテストから呼び出せないのが理由みたいです。
ということで、テストヘルパーにログイン中かどうかをチェックするメソッドを追加して、それを使ってテストを書きます。
8.3 ログアウト
最後にログアウトを実装します。
これは意外なほど簡単で、セッションからuser_idを削除するためにdeleteメソッドを使うだけです。引数にはシンボルを使うようです。
@current_userにもログイン中のユーザー情報が入ってるので、これも削除します。
ログアウトを実装したら、次はテストを書きます。ログインの方も修正して、GREENになったら完了です。
最後に、コミット、マージ、プッシュ、デプロイして第8章は終わりです。
まとめ
第8章はいろいろ実験しながら進めたので、なかなか大変でした。実験しながら「これって意味あるのかな?」みたいなことも考えたりもしましたが、きっと役に立つと信じてます。
Sessionsコントローラとsessionメソッドは混乱の原因になりそうなので、無関係だということをしっかり覚えておこうと思います。
あとはform_withの使い方ですね。Ruby on Railsを使ってWebアプリケーションを作るなら必ず出てくると思うので、使いこなせるようになりたいところです。
メソッドの書き方とか、その処理の流れとかも、他のところが難しいと混乱の原因になりそうです。プログラミング学習でいつも困るのは、「変数名などを変えてもいいのかがわからない」ということです。
その辺も変更を加えながら試してみるのがいいんだなと、今回思いました。
ログインの仕組みを学びつつ、プログラミング学習の方法も学んだような気がしてます。
次の第9章ではログイン機構を発展させます。
