静的型付け言語原理主義者によるRailsチュートリアル(第7章)
第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)
- Rails には3つの環境がデフォルトで用意されている
- デバッグ情報整形のために CSS も更新
7.1.2 Users リソース
- DBに登録されているユーザー情報を Web アプリケーション上に表示する
- REST アーキテクチャの慣習に従う
- データの作成、表示、更新、削除をリソースとして扱う
- 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 のハッシュ化が実現できる
- Ruby では
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
メソッドを使ってフォームを構築すべきだと判断する
- すべての Ruby クラスは自分のクラスを知っているので、Rails は
<form action="/users" class="new_user" id="new_user" method="post">
- 上記
class
とid
属性はアーキテクチャとしては基本的に無関係 - /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番目の引数の英単語を複数形に変更したものを返す
- なんて細かいヘルパーなのか
- 最初の引数に整数が与えられると、それに基づいて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 の一般的な慣習に倣いユーザー登録成功時にはページを描画せずに別のページにリダイレクトさせる- 具体的には新規作成されたユーザーのプロフィールページへリダイレクト
- 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 を使用していないこと
- たしかに
- 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 のサブドメインでのみ有効な手段
- Heroku の SSL 証明書に便乗する
7.5.2 本番環境用の Web サーバー
- Heroku のデフォルト Web サーバーは WEBrick だが、著しいトラフィックを扱うのには適していない
- Puma に置き換える
- 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 の慣習」というワードがよく出てきたのもこの章の特徴であったように思える。慣習とはどこからやってくるのか 🤔