Rails 総復習1ヶ月チャレンジ 2日目(Railsチュートリアル編)

2日目早速入っていきます。

2日目(Ruby on Rails チュートリアル 7章ユーザー登録〜8章ログイン機能 Sessionsコントローラーまで)

キーワード引数

これはチュートリアル演習の7.12中でこのような説明がありました。

module UsersHelper

  # 引数で与えられたユーザーのGravatar画像を返す
  def gravatar_for(user, options = { size: 80 })
    size = options[:size]
    :
  end
end

キーワード引数でこのようにも渡せる

module UsersHelper

  # 引数で与えられたユーザーのGravatar画像を返す
  def gravatar_for(user, size: 80)
   :
  end
end

何となく上と下はイコールなんだなと理解は出来ますが、どういう時に使えるのかというのがイマイチよく分からなかったので調べてみました。

Ruby入門(チェリー本)p154より拝借。
このようなメソッドがあったとして

def buy_burger(menu, drink, potato)
  #ハンバーガーを購入
  if drink
  #ドリンクを購入
  end
  if potato
  #ポテトを購入
  end
end

# チーズバーガーとドリンクとポテトを購入する
buy_burger('cheese', true, true)

# フィッシュバーガーとドリンクを購入する
buy_burger('fish', true, false)

メソッドの引数を確認しているので特に違和感なくすんなりと理解が出来ますが、では下記のように与えられたらどうでしょう。

buy_burger('cheese', true, true)
buy_burger('fish', true, false)

???
正直2つ目、3つ目の引数が何を表しているのか理解が出来ません・・・
そういった場合に役に立つのがキーワード引数です!キーワード引数を使うと可読性が上がります。
上記のbuy_burgerメソッドをキーワード引数を使うと次になります。

def buy_burger(menu, drink: true, potato: true)
  #省略
end

キーワード引数のついたメソッドを呼び出すには次のようにします。

buy_burger('cheese', drink: true, potato: true)
buy_burger('fish', drink: true, potato: false)

また引数にデフォルト値が設定されている場合は引数を省略も可能です。
そして呼び出し時の順番も自由に入れ替えられます。(これは実際にはあんまり使わないかも、他の人が見たら混乱しそうだし。。。)

#drinkはデフォルト値のtrueを適用
buy_burger('fish', potato: false)

#順番入れ替え
buy_burger('fish', potato: false, drink: true)

content_tag

これは単純に知らなかったのでメモ程度です。
下記のようにコードにHTMLとERBが混ざっている場合、

 <% flash.each do |message_type, message| %>
   <div class="alert alert-<%= message_type %>"><%= message %></div>
 <% end %>

content_tagというヘルパーを使うとコードがスッキリします。

<% flash.each do |message_type, message| %>
  <%= content_tag(:div, message, class: "alert alert-#{message_type}") %>
<% end %>

参考:railsdoc.com

ログイン機能

前提

HTTPはステートレス(Stateless)なプロトコル。文字通り「状態(state)」が「ない(less)」ので、HTTPのリクエスト1つ1つは、それより前のリクエストの情報をまったく利用できない、独立したトランザクションとして扱われます。その為、ユーザーIDを保持しておく手段としてSessionを用いるのが一般的。

  • Session ユーザーログインの必要なWebアプリケーションでは、Sessionと呼ばれる半永続的な接続をコンピュータ間(ユーザーのパソコンのWebブラウザRailsサーバーなど)に別途設定する必要がある。
  • cookies ユーザーのブラウザに保存される小さなテキストデータ。あるページから別のページに移動した時にも破棄されず、ユーザーIDなどの情報を保存できる。

8章、9章にまたがっていますので日にちを跨ぎながらやっていきたいと思います。
流れとしては 1: ブラウザを閉じるとログインを破棄する(Railsのsessionメソッドを用いる)
2: ユーザーのログイン情報を自動で保存する(Railsのcookiesメソッドを用いる)
3: ユーザーがチェックボックスをオンにした場合のみログインを保存する(Remember me)
という形です。

では1:ブラウザを閉じるとログインを破棄する(Railsのsessionメソッドを用いる)からやっていきます。

Sessionsコントローラー作成

$ bundle exec rails generate controller Sessions new

ルーティング設定

Rails.application.routes.draw do
  :
  get    '/login',   to: 'sessions#new'
  post   '/login',   to: 'sessions#create'
  delete '/logout',  to: 'sessions#destroy'
  resources :users
end

ログインフォーム作成

#app/views/sessions/new.html.erb

<% provide(:title, "Log in") %>
<h1>Log in</h1>

<div class="row">
  <div class="col-md-6 col-md-offset-3">
    <%= form_with url: login_path, scope: :session, local: true) do |f| %>

      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>

      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>

      <%= f.submit "Log in", class: "btn btn-primary" %>
    <% end %>

    <p>New user? <%= link_to "Sign up now!", signup_path %></p>
  </div>
</div>

ここで注意が必要なのが、入力フォーム作成で使っているform_withです。
SessionではデータをDB上で保存はしないのでSessionモデルというものは存在しません。その為、User新規作成フォームのようにインスタンス変数@userをmodelに渡すといった記載は出来ません。

form_with model: @user, local: true do |f|

Railsでは上のように書くだけで、「フォームのactionは/usersというURLへのPOSTである」と自動的に判定します。しかしSessionではUrlとscope(inputタグのname属性にプレフィックスを追加する)指定する必要があります。

form_with url: login_path, scope: :session, local: true |f|

生成されたログインフォームのHTML

<form accept-charset="UTF-8" action="/login" method="post">
  <input name="authenticity_token" type="hidden"
         value="NNb6+J/j46LcrgYUC60wQ2titMuJQ5lLqyAbnbAUkdo=" />
  <label for="session_email">Email</label>
  <input class="form-control" id="session_email"
         name="session[email]" type="email" />
  <label for="session_password">Password</label>
  <input id="session_password" name="session[password]"
         type="password" />
  <input class="btn btn-primary" name="commit" type="submit"
       value="Log in" />
</form>

試しにinputのemailの部分を見てみると

  <input class="form-control" id="session_email" name="session[email]" type="email" />

name="session[email]"となっています。これがform_withでscopeで渡した部分です。
仮にscopeを使わずurlのみ渡した場合は下記となり、これでは正しく値を渡すことは出来ません。

 <input class="form-control" id="session_email" name="email" type="email" />

Sessionsコントローラー記載

class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      # ユーザーログイン後にユーザー情報のページにリダイレクトする
    else
      flash.now[:danger] = 'Invalid email/password combination' 
      render 'new'
    end
  end

  def destroy
  end
end

createアクションの流れ
ログインフォームから入力されたパラメーターよりDBにemailカラムの値と一致するものを探し、userに代入。その後if文でuserにhas_secure_passwordが提供するauthenticateメソッド使い認証を行なっています。

user && user.authenticate(params[:session][:password])

&&(and)は取得したユーザーが有効かどうかを決定しています。Rubyではnilとfalse以外の全てのオブジェクトは、真偽値でtrueになる性質があります。これを考慮すると入力されたメールアドレスを持つユーザーがDBに存在し、かつ入力されたパスワードがそのユーザーのものであった場合のみif文がtrueになります。

本日は以上です!明日はまた続きからやっていきます!