静的型付け言語原理主義者によるRailsチュートリアル(第7章)

image

第7章 ユーザー登録

  • いよいよユーザー登録機能を追加
    • HTML フォームを使って Web アプリケーションに登録情報を送信する
    • ユーザーを新規作成して情報を DB に保存する

7.1 ユーザーを表示する

  • ユーザーの名前とプロフィール写真を表示するためのページを作成する

7.1.1 デバッグと Rails 環境

  • このアプリにおける初めての真に動的なページを作成する

    • Web サイトのレイアウトにデバッグ情報を追加する
    • ビルトインの debug メソッドと param 変数を使ってページにデバッグ情報が表示されるようになる

      <!DOCTYPE html>
      <html>
      .
      .
      .
      <body>
      <%= render 'layouts/header' %>
      <div class="container">
      <%= yield %>
      <%= render 'layouts/footer' %>
      <%= debug(params) if Rails.env.development? %>
      </div>
      </body>
      </html>
  • 本番環境でデバッグ情報を表示しないための if Rails

    • Rails には3つの環境がデフォルトで用意されている
      • テスト環境 (test)
      • 開発環境 (development)
      • 本番環境 (production)
  • デバッグ情報整形のために CSS も更新

7.1.2 Users リソース

  • DBに登録されているユーザー情報を Web アプリケーション上に表示する
    • REST アーキテクチャの慣習に従う
      • データの作成、表示、更新、削除をリソースとして扱う
  • ユーザーをリソースとみなす場合、id = 1 のユーザーを参照するということは、/users/1 という URL に対して GET リクエストを発行することを意味する
    • ここでの show というアクションは暗黙のリクエストになる
    • Rails のREST 機能が有効になっていると、GET リクエストは自動的に show アクションとして扱われる
      • すなわち Rails はREST 機能を有効無効にできるということか
  • /users/1 のURL を有効にするために、 config/routes.rb を更新

    Rails.application.routes.draw do
    root 'static_pages#home'
    get  '/help',    to: 'static_pages#help'
    get  '/about',   to: 'static_pages#about'
    get  '/contact', to: 'static_pages#contact'
    get  '/signup',  to: 'users#new'
    resources :users
    end
  • この1行の追加で RESTful な Users リソースで必要となる全てのアクションが利用可能になる

  • ユーザー情報表示用の仮のビューを作成

    <%= @user.name %>, <%= @user.email %>
  • Users コントローラの show アクションに対応する @user 変数を定義する

    class UsersController < ApplicationController
    
    def show
    @user = User.find(params[:id])
    end
    
    def new
    end
    end
  • Users コントローラにリクエストが正常に送信されると、 params[:id] の部分はユーザー id の1に置き換わる

    • つまり、 User.find(1) と同じ
  • ビューとアクションが定義されたので、/users/1 は動作するようになった

7.1.1 debugger メソッド

  • byebug gem でより直接的なデバッグが可能

    class UsersController < ApplicationController
    
    def show
    @user = User.find(params[:id])
    debugger
    end
    
    def new
    end
    end
  • ターミナルで Rails コンソールのようにコマンドを呼び出すことが可能になる

    • gdb みたい

7.1.4 Gravatar 画像とサイドバー

  • ユーザーのプロフィール写真で Gravatar を使用する
  • gravatar_for ヘルパーメソッドで Gravatar の画像を利用できるようになる
  • Gravatar の URL はユーザーのメールアドレスを MD5 でハッシュ化している

    • Ruby では Digest ライブラリの hexdigest メソッドで MD5 のハッシュ化が実現できる

      module UsersHelper
      
      # 引数で与えられたユーザーのGravatar画像を返す
      def gravatar_for(user)
      gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
      gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}"
      image_tag(gravatar_url, alt: user.name, class: "gravatar")
      end
      end
  • モックアップに近づけるため、ユーザーのサイドバーを作っていく

  • HTML 要素と CSS クラスを配置したことにより、プロフィールページに SCSS でスタイルを与えることができるようになった

    • これは Asset Pipeline でSass エンジンが使われている場合に限られる

7.2 ユーザー登録フォーム

  • ユーザー登録フォームを作る

7.2.1 form_for を使用する

  • form_for ヘルパーメソッドをユーザー登録ページで使う
    • Active Record のオブジェクトを取り込み、そのオブジェクトの属性を使ってフォームを構築する
  • ユーザー登録ページ /signup のルーティングはUsers コントローラーの new アクション紐付けられている

    • new アクションに @user 変数を追加する

      class UsersController < ApplicationController
      
      def show
      @user = User.find(params[:id])
      end
      
      def new
      @user = User.new
      end
      end
  • フォームは SCSS で見栄えを整える

7.2.2 フォーム HTML

<%= form_for(@user) do |f| %>
  • f というオブジェクトは、HTML フォーム要素に対応するメソッドが呼び出されると、@user の属性を設定するための HTML を返す

    <%= f.label :name %>
    <%= f.text_field :name %>
  • 上記では、User モデルの name 属性を設定するラベル付きテキストフィールド要素を作成するのに必要な HTML を返す

    • 埋め込み Ruby からモデルの属性を設定できる…だと…
  • テキストフィールドでは内容をそのまま表示

  • パスワードフィールドではセキュリティの都合上文字が隠蔽される

    • 完璧な気遣い
  • ユーザーの作成で重要なのは input ごとにある特殊な name 属性

    <input id="user_name" name="user[name]" - - - />
    
  • Rails は name の値を使い、初期化したハッシュを構成する

    • このハッシュは入力された値に基づいてユーザーを作成するときに使われる
  • Rails は form タグを作成するときに @user オブジェクトを使う

    • すべての Ruby クラスは自分のクラスを知っているので、Rails は @user のクラスが User であることを認識する
    • また、@user は新しいユーザーなので、Rails は post メソッドを使ってフォームを構築すべきだと判断する

      <form action="/users" class="new_user" id="new_user" method="post">
  • 上記 classid 属性はアーキテクチャとしては基本的に無関係

  • /users に対して HTTP の POST リクエストを送信するということが大事

7.3 ユーザー登録失敗

  • フォームを理解するにはユーザー登録の失敗のときが最も参考になる

7.3.1 正しいフォーム

  • /users への POST リクエストは create アクションに送られる
    • create アクションでフォーム送信を受け取り、 User.new を使って新しいユーザーオブジェクトを作成し、ユーザーを保存し、再度の送信用のユーザー登録ページを表示する
      • やること多いな
  • ユーザー登録の失敗に対応できる create アクションを定義

    class UsersController < ApplicationController
    
    def show
    @user = User.find(params[:id])
    end
    
    def new
    @user = User.new
    end
    
    def create
    @user = User.new(params[:user])    # 実装は終わっていないことに注意!
    if @user.save
      # 保存の成功をここで扱う。
    else
      render 'new'
    end
    end
    end
  • 不正なデータでユーザー登録しようとするとエラーになる

  • パラメーターハッシュの user は Users コントローラに params として渡される

    • このハッシュのキーが、 input タグにあった name 属性の値になる

      <input id="user_email" name="user[email]" type="email" />
  • 例えば user[email]user ハッシュの :email キーの値と一致する

  • Rails は文字列ではなく params[:user] のようにシンボルとして Users コントローラにハッシュのキーを渡している

  • つまり、下記の2つのコードはほぼ同じである

    @user = User.new(params[:user])
    @user = User.new(name: "Foo Bar", email: "foo@invalid", password: "foo", password_confirmation: "bar")
  • 昔のバージョンの Rails では1つ目のコードでも動いたが、脆弱性があったため Rails 4.0 移行ではエラーとしている

    • Strong Parameters というテクニックで対策することを標準とした

7.3.2 Strong Parameters

  • 値のハッシュを使って Ruby の変数を初期化することをマスアサインメントという

    @user = User.new(params[:user])    # 実装は終わっていないことに注意!
  • params ハッシュ全体を初期化することは、ユーザーが送信したデータをまるごと User.new に渡していることになり、セキュリティ上極めて危険

  • 以前のバージョンの Rails ではモデル層で attr_accessible を使うことで防止していたが、 Rails 4.0 ではコントローラ層で Strong Parameters というテクニックを使うことが推奨されている。

    • Strong Pararmeters を使うことにより、必須パラメータと許可されたパラメータを指定することが可能
    • さらに、prams ハッシュを丸ごと渡すとエラーが発生する

      params.require(:user).permit(:name, :email, :password, :password_confirmation)
  • 上記の params ハッシュでは :user 属性を必須とし、名前、メールアドレス、パスワード、パスワードの確認をそれぞれ許可し、それ以外を許可しないようにしている

  • 上記コードの戻り値は、許可された属性のみが含まれた params のハッシュ

  • これらのパラメータを使いやすくするために、user_params という外部メソッドを使うのが慣習

    • このメソッドは適切に初期化したハッシュを返し、params[:user] の代わりとして使われる

      • Rails でよく出てくる慣習の由来とは何なんだろう 🤔

        @user = User.new(user_params)
  • この user_params メソッドは Users コントローラの内部でのみ実行され、Web 経由で外部ユーザーにさらされる必要はないため、Ruby の private キーワードを使って外部から使えないようにする

    • クラス内に唐突に現れる private 空間は inner class 的なものだろうか

      class UsersController < ApplicationController
      
      private
      
      def user_params
      params.require(:user).permit(:name, :email, :password,
                                 :password_confirmation)
      end
      end

7.3.3 エラーメッセージ

  • ユーザー登録に失敗したときのエラーメッセージ
    • Rails はこのようなメッセージを Users モデルの検証時に自動的に生成してくれる
  • モデルの保存に失敗するうと、@user オブジェクトに関連付けられたエラーメッセージの一覧が生成される

    • ユーザーの new ページでエラーメッセージのパーシャルを出力する
    • パーシャルとは部分テンプレートのこと
    • form_control という CSS クラスも一緒に追加することで、Bootstrap がうまく取り扱ってくれるようになる

      <% provide(:title, 'Sign up') %>
      <h1>Sign up</h1>
      
      <div class="row">
      <div class="col-md-6 col-md-offset-3">
      <%= form_for(@user) do |f| %>
      <%= render 'shared/error_messages' %>
      
      <%= f.label :name %>
      <%= f.text_field :name, class: 'form-control' %>
      
      <%= f.label :email %>
      <%= f.email_field :email, class: 'form-control' %>
      
      <%= f.label :password %>
      <%= f.password_field :password, class: 'form-control' %>
      
      <%= f.label :password_confirmation, "Confirmation" %>
      <%= f.password_field :password_confirmation, class: 'form-control' %>
      
      <%= f.submit "Create my account", class: "btn btn-primary" %>
      <% end %>
      </div>
      </div>
  • Rails 全般の慣習として、複数のビューで使用されるパーシャルは専用の shared ディレクトリに置かれる

  • フォーム送信時にエラーメッセージを表示するためのパーシャル

    <% if @user.errors.any? %>
    <div id="error_explanation">
    <div class="alert alert-danger">
      The form contains <%= pluralize(@user.errors.count, "error") %>.
    </div>
    <ul>
    <% @user.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
    </div>
    <% end %>
  • pluuralize という英語専用のテキストヘルパー

    • 最初の引数に整数が与えられると、それに基づいて2番目の引数の英単語を複数形に変更したものを返す
      • なんて細かいヘルパーなのか
  • SCSS でエラーメッセージを整形し、無効なユーザー登録情報を送信したときにわかりやすいエラーメッセージを表示する準備は完了

  • ただし、presence:true によるバリデーションも、has_secure_password によるバリデーションも、空のパスワード(nil)を検知してしまうため、ユーザー登録フォームで空のパスワードを入力すると2つの同じエラーメッセージが表示されてしまう

    • allow_nil:true というオプションでこの問題は解決可能

7.3.4 失敗時のテスト

  • Rails ではフォーム用のテストを書くことが可能
    • 無効な送信をしたときの正しい振る舞いについてテストを書いていく
  • 新規ユーザー登録用の統合テストを生成
  • ユーザーが作成されないことを確認するテストから
    • 現在のユーザー数を覚えた後にデータを投稿し、ユーザー数が変わらないかどうかを検証する
      • assert_no_difference を使うのが慣習

7.4 ユーザー登録成功

  • 新規ユーザーを実際にデータベースに保存できるようにする

7.4.1 登録フォームの完成

  • まだ登録フォームは正常に動かない

    • Rails はデフォルトのアクションに対応するビューを表示しようとするが、create アクションに対応するビューのテンプレートがないことが原因
    • create アクションに対応するテンプレートを作成することも可能だが、Rails の一般的な慣習に倣いユーザー登録成功時にはページを描画せずに別のページにリダイレクトさせる

      • 具体的には新規作成されたユーザーのプロフィールページへリダイレクト

        redirect_to @user
  • これだけでリダイレクト可能

    • つよい
  • 上記コードは以下のコードと等価

    redirect_to user_url(@user)
  • 無事ユーザー登録→プロフィールページリダイレクトが実装できた

7.4.2 flash

  • ユーザー情報登録完了後、表示されるページにメッセージを表示し、2度目以降にはそのページにメッセージを表示しないようにする
    • Rails では flash という変数を使用する
  • ユーザー登録ページにフラッシュメッセージを追加する

    • :success というキーに成功時のメッセージを代入

      class UsersController < ApplicationController
      .
      .
      .
      def create
      @user = User.new(user_params)
      if @user.save
      flash[:success] = "Welcome to the Sample App!"
      redirect_to @user
      else
      render 'new'
      end
      end
      
      private
      
      def user_params
      params.require(:user).permit(:name, :email, :password,
                                 :password_confirmation)
      end
      end
  • flash 変数に代入したメッセージは、リダイレクトした直後のページに表示できるようになる

    • flash 内に存在するキーがあるかを調べ、もしあればその値をすべて表示するようにレイアウトを修正する

7.4.3 実際のユーザー登録

  • 実際にサンプルアプリケーションでユーザー登録を試してみる
  • ユーザー登録後に無事フラッシュメッセージが表示された!
    • 僅かな修正だけでメッセージが表示される凄さよ

7.4.4 成功時のテスト

  • 有効な送信に対するテストを追加する
  • assert_difference で有効なユーザー登録をテストする

    require 'test_helper'
    
    class UsersSignupTest < ActionDispatch::IntegrationTest
    .
    .
    .
    test "valid signup information" do
    get signup_path
    assert_difference 'User.count', 1 do
      post users_path, params: { user: { name:  "Example User",
                                         email: "user@example.com",
                                         password:              "password",
                                         password_confirmation: "password" } }
    end
    follow_redirect!
    assert_template 'users/show'
    end
    end
  • ユーザー登録成功後、どのテンプレートが表示されているかも検証する

    • ビューのテストもここでやることに驚き

7.5 プロのデプロイ

  • プロレベルのデプロイ
    • とは
  • ユーザー登録をセキュアにするために本番用のアプリケーションに機能を追加する

7.5.1 本番環境での SSL

  • サンプルアプリケーションのセキュリティの欠陥
    • SSL を使用していないこと
      • たしかに
  • production.rb という本番用の設定ファイルを修正するだけ
    • まじかよ。。。
  • Heroku では SSL の使用をブラウザに強制しない

    • SSL を強制するように変更する

      Rails.application.configure do
      .
      .
      .
      # Force all access to the app over SSL, use Strict-Transport-Security,
      # and use secure cookies.
      config.force_ssl = true
      .
      .
      .
      end
  • サーバーの SSL セットアップ

    • Heroku の SSL 証明書に便乗する
      • Heroku のサブドメインでのみ有効な手段

7.5.2 本番環境用の Web サーバー

  • Heroku のデフォルト Web サーバーは WEBrick だが、著しいトラフィックを扱うのには適していない
    • Puma に置き換える
      • Puma は聞いたことがある!
  • Rails 5 では Puma はデフォルトで Gemfile に追加されている
    • つよい
  • config/puma.rb を修正
  • Heroku 上で Puma のプロセスを走らせるために./Procfile を作成

7.5.3 本番環境へのデプロイ

  • heroku へデプロイして完了!と思いきやここでドはまり
  • エラーでアプリが起動しない
  • heroku logs --tail でログを見る
    • Address already in use - bind(2) for 0.0.0.0:xxxxx (Errno::EADDRINUSE)
    • どうやらポートがすでに使われているらしい
      • しかし何もしていないぞ…?(何かしているやつのセリフ)
  • 色々ググって格闘し、config/puma.rb の設定ミスと判明
  • デフォルトの設定にチュートリアル記載の設定を追記したが、実は置き換えが正解だったようだ
    • 中身をきちんと呼んでいればポート番号の重複に気づけたのではと反省
  • ようやく heroku でも起動してめでたしめでたし

所感

ユーザー登録という機能を実装するために、わずかな量のコードしか追加していない。Rails の真骨頂を見たような章だった。一方、強力すぎるがゆえに変更には弱いのではという印象が強まってきた。短期立ち上げが必要でかつ継続的開発を行わない Web アプリにおいては Rails は有力な候補になるのではと感じている。

また、「Rails の慣習」というワードがよく出てきたのもこの章の特徴であったように思える。慣習とはどこからやってくるのか 🤔


5261 Words

2020-01-31 13:28 +0000