Rails: ファイルを役割別に整理する


目的

自己整理のために、railsで使用するファイルを役割別にまとめています。

なお、ログイン機能を実装した実際のコードを実例として載せています。

現時点では理解が及んでいない部分も多々ございます。

学習を進めながら、気づきが起こり次第、都度修正していきます。

MVCモデル

設計モデルの1つで、Model, View, Controllerの頭文字をとったもの。
それぞれ役割が異なり、以下のように設計されています。

  • Model(モデル):データベースに問い合わせ、データの新規作成、更新、削除などを担当する

  • View(ビュー):クライアントに表示するページを生成する

  • Controller(コントローラー):rooterからの指示をもとにサーバー側で必要な処理を行う

【その他】
※Rooter(ルーター):クライアントからのリクエストに応じて、コントローラーに指示を出す。

※Helper(ヘルパー):コントローラーで使用するメソッドを定義しておける場所。
           

Rooter

ルーターは、各コントローラに対してアクションを指定し、命令を出します。

設定は、routes.rbというファイル1つに集約してまとめられています。

例えば、冒頭の一文(get '/home', to: 'static_pages#home')は、次の意味を表します。

■code意味:
/homeのURLで、GETリクエストをクライアントから受けた場合、static_pagesコントローラーのhomeアクションを実行しなさい。

また、resource: に対してコントローラー名を渡すと、railsがよく使う7つのアクション(index, show, new, create, edit, update, destory)を自動的に作成してくれます。

なお、rootはクライアントがはじめに飛ぶページを設定しています。

config/routes.rb
Rails.application.routes.draw do
  get  '/home',    to: 'static_pages#home'
  get  '/about',   to: 'static_pages#about'
  get  '/contact', to: 'static_pages#contact'
  get  '/signup',  to: 'users#new'
  get    '/login',   to: 'sessions#new'
  post   '/login',   to: 'sessions#create'
  delete '/logout',  to: 'sessions#destroy'
  resources :users
  root 'static_pages#home'
end

Controller

コントローラーは、ルーターからの指示を受けて、具体的な処理を行います。

生成したコントローラーの数だけファイルが存在し、<コントローラー名>.controller.rbというファイルに処理内容をまとめます。

実例では、はじめのnewアクション

処理を未定義であるため、railsのデフォルト動作をします。

デフォルトでは、対応するビュー(<コントローラー名>/<アクション名>.html.erb)に飛びます。


2つ目のcreateアクションでは

  • クライアントから送信されたパラメーターに入っているセッション内のemailをもとにユーザー情報を検索し、user変数に格納します
    1. user変数がtrueでかつ、送信されてきたセッション内のパスワードを鍵にユーザー認証がされれば、
      1. ユーザをログインさせて
      2. もしクライアントがremember meのチェックボックスにチェックが入っていれば、PCにユーザ情報を覚えさせ、なければ忘れさせます。
    2. 一方で、ユーザー認証が失敗すれば
      1. 1度だけ’emailとpasswordのバリデーション通りませんでした’というエラーメッセージを表示させて
      2. ログインページへ移動させる

といった処理を記述しています。


3つ目のdestroyアクションでは

  • もしログインしていた場合は、ログアウトさせ、ルートで設定されているURLに移動させる

といった処理を記述しています。

app/controllers/<コントローラー名>_controller.rb
class SessionsController < ApplicationController
  def new; end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user&.authenticate(params[:session][:password])
      log_in user
      params[:session][:remember_me] == '1' ? remember(user) : forget(user)
      redirect_to user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new'
    end
  end

  def destroy
    log_out if logged_in?
    redirect_to root_url
  end
end

Helper

ヘルパーでは、コントローラーで使用するメソッドを定義できます。

ヘルパーも生成されたコントローラーの数だけ、ファイルが存在します。

<コントローラー名>_helper.rbという名称で作成されます。

ヘルパーで、分かりやすいメソッド名でより詳細の処理を定義して、コントローラー側では、可読性が高いコードを記述します。

要するに、コントローラーでは、デフォルトメソッド+ヘルパーメソッドを用いてコードを書き、最小限に文字数を抑えて、見やすいコードにします。

例えば、current_userでは、ケースに分けて2つの動作が行われる複雑な処理がされます。

1、セッションの中にユーザIDがあるなら:
 @current_userがnilの場合、DBからユーザIDを鍵にユーザー情報を取得する
 (@current_userがあれば、DBへの問合せをしない = クエリ数を減らす)

2、もしくは、cookieの中にユーザIDがあるなら:
 DBからユーザIDを鍵にユーザー情報を取得して、userに格納して、
 そのuserのnilチェックをした上で、DBに保存しているremember_tokenで認証されれば
 ログインさせて(セッションにユーザIDを入れる)
 インスタンス変数に格納しておく(2回目以降はDB問い合わせ不要になる)

これをコントローラー側に定義すると、読み解くことが難しくなるので
current_user(現在のユーザー情報という意味)といったメソッドを
ヘルパー側で定義しています。

app/helpers/<コントローラー名>_helper.rb
module SessionsHelper
  def log_in(user)
    session[:user_id] = user.id
  end

  def remember(user)
    user.remember
    cookies.permanent.signed[:user_id] = user.id
    cookies.permanent[:remember_token] = user.remember_token
  end

  def current_user
    if (user_id = session[:user_id])
      @current_user ||= User.find_by(id: user_id)
    elsif (user_id = cookies.signed[:user_id])
      user = User.find_by(id: user_id)
      if user&.authenticated?(cookies[:remember_token])
        log_in user
        @current_user = user
      end
    end
  end

  def logged_in?
    !current_user.nil?
  end

  def forget(user)
    user.forget
    cookies.delete(:user_id)
    cookies.delete(:remember_token)
  end

  def log_out
    forget(current_user)
    session.delete(:user_id)
    @current_user = nil
  end
end

Model

モデルは、DBへの問い合わせを行います。

生成したモデルの数だけ、<モデル名>.rbというファイルが存在します。

まず、上部コードについて簡単に解説ですが、
・attr_accessor : ゲッター・セッターの作成(ここではremember_tokenを指す)
・before_save : 保存直前の動作指示
・validates : 各種制限をかける(文字数や必須入力させる等)
・has_secure_password : authenticateなど特殊なメソッドを使用できるようにする

さらに、下部コードでは、DBに関わる(作成、更新、削除)といけない動作は
各モデルのファイルにメソッドを定義します。

例えば、rememberのように
User.new_tokenで定義されたランダムな文字列で生成されたものをremember_tokenに代入し、
User.digestで定義されたハッシュ化されたremember_tokenを
DBのremember_digestカラムに紐付けて、値を更新させる
といったメソッドは、更新する挙動を設定したいので、モデルのファイルで定義します。

また、forgetも同様で、
DBの:remember_digestに、nilを代入することで忘却させますので
更新する挙動を設定したいので、同ファイルに定義されています。

app/models/<モデル名>.rb
class User < ApplicationRecord
  attr_accessor :remember_token

  before_save { self.email = email.downcase }
  validates :name, presence: true, length: { maximum: 50 }
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i },
                    uniqueness: true
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }

  def self.digest(string)
    cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
    BCrypt::Password.create(string, cost: cost)
  end

  def self.new_token
    SecureRandom.urlsafe_base64
  end

  def remember
    self.remember_token = User.new_token
    update_attribute(:remember_digest, User.digest(remember_token))
  end

  def authenticated?(remember_token)
    return false if remember_digest.nil?

    BCrypt::Password.new(remember_digest).is_password?(remember_token)
  end

  def forget
    update_attribute(:remember_digest, nil)
  end
end

View

ビューは、クライアントに表示するページ生成を担当します。

コントローラーに定義しているアクションによって、表示先を指定することができます。

<コントローラー名>/<アクション名>.html.erbという名称でファイルを作成します。

拡張子に注目いただきたいのですが、erbという埋め込みrubyのファイル形式を使用します。

これでHTML形式の中に<%= %>などを用いて、rubyコードが記述可能になります。

また、railsには自動的にHTMLを生成してくれる便利なメソッドがあります。

その一つがform_withメソッドで、下記コードの場合は
クライアントから/loginのURLで入力された情報を
POSTリクエストとして送信する
設定を行ってくれます。

app/views/<コントローラー名>/<アクション名>.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.label :remember_me, class: "checkbox inline" do %>
        <%= f.check_box :remember_me %>
        <span>PCにログイン情報を記憶させる</span>
      <% end %>

      <%= f.submit "ログイン", class: "btn btn-primary" %>
    <% end %>

    <p>新規アカウント作成の場合は <%= link_to "こちらをクリック", signup_path %></p>
  </div>
</div>

動作について

1つのモデルケースの検証ですが、
クライアントからのWEBサイトにアクセスがあり、
remember meをチェックされて
loginボタンを押された
ケースを想定すると


  • View:new.html.erb
     URLを「/login」とPOSTリクエストを設定されたページで  ユーザーが入力してリクエストを送る

  • Rooter:routes.rb
     ユーザーから送信された  /loginのURLとPOSTリクエストから  指令を出すべきコントローラーを判断して指示を出す

  • Controller:sessions.controller.rb
     Rooterからの指示を受けて、createアクションを実行する。
     (DBからユーザー検索をして、認証手続きをしてユーザーページに飛ばします。)

 ・・・Helper:<コントローラー名>_helper.rb
    便利メソッドを定めて、Controllerの動作を補助する。
    

  • Model:user.rb
     クライアントがremember meにチェックしたことにより、digestに記録する  トークンを生成して、DBに保存させる。
     (ログアウトしない限り、2回目以降のログイン時にパスワードなど入力不要になる)

  • View:show.html.erb
     ユーザー検索してヒットした該当ユーザーのページを表示させる

といった動作となります。

おわりに

自分なりにrailsの挙動を整理したくて、まとめてみました。

まだ、理解が浅いので、随時で修正すると思います…

最後までお読みいただき、ありがとうございました!