7.3 ユーザー登録失敗
フォームを理解するにはユーザー登録の失敗のときが最も参考になる。
ここでは、無効なデータ送信を受け付けるユーザー登録フォームを作成し、ユーザー登録フォームを更新してエラーの一覧を表示。
7.3.1 正しいフォーム
7.1.2で、resources :usersにより、自動的にRailsアプリケーションがRESTful URIに応答するようになった。
特に、/usersへのPOSTリクエストはcreateアクションに送られる。
ここでは、次のように機能を実装
(1)createアクションでフォーム送信を受け取り
↓
(2)User.newで新しいユーザーオブジェクトを作成し
↓
(3)ユーザーを保存(または保存に失敗)し
↓
(4)再度の送信用のユーザー登録ページを表示
ユーザー登録の失敗に対応できるcreateアクション
controllers/users_controller.rb
def create ←追加 @user = User.new(params[:user]) #実装が終わってないことに注意!ストロングパラメータを使ってないから。 if @user.save #保存の成功 else render 'new' end end
このコードの動作を理解するもっともよい方法は、実際に無効なユーザー登録データを送信してみること。
実際に送信すると失敗する。
送信失敗の原因(デバッグ情報より)
デバッグ情報のパラメーターハッシュのuserの部分を見てみる。
"user"=>{"name"=>"", "email"=>"", "password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]"},
これは、"user" => □というハッシュ(userがキー、□が値)で、値□は{"name"=>"", … , "password_confirmation"=>"[FILTERED]"}というハッシュ。ハッシュの入れ子構造になっている。このハッシュはUsersコントローラにparamsとして渡されるので、値□を取り出したいときにはparams[:user]とすればよさそう(ただし、先の送信失敗で分かるように、これだとエラーが出る。解決のためには後述するStrong Parametersを使う)
このハッシュのキーが、inputタグにあったname属性の値になる。例えば次のように、
<input id="user_email" name="user[email]" type="email" />
"user[email]"という値は、userハッシュの:emailキーの値と一致する。
昔のバージョンのRailsでは
@user=User.new(params[:user])
このコードでも動いたが、悪意のあるユーザーによってアプリケーションのDBが書き換えられないように慎重な対策をとる必要がでた。そこでRails4.0以降では、上のコードをエラーとすることでセキュリティーを高め、また、Strong Parametersと呼ばれるテクニックで対策することにした。
7.3.2 Strong Parameters
次のコードでは
@user=User.new(params[:user])
paramsハッシュ全体を初期化しているため、セキュリティー上、極めて危険。
これは、ユーザーが送信したデータをまるごとUser.newに渡していることになる。
例えば、Userモデルに管理者権限のadmin属性があるとし、admin='1'という値をparams[:user]の一部に紛れ込ませて渡せば、この属性をtrueにすることができる。
すなわち、paramsハッシュがまるごとUser.newに渡されてしまうと、どのユーザーでもadmin='1'をWebリクエストに紛れ込ませるだけで、Webサイトの管理者権限を奪うことができるというわけ。うーん、怖い!!
Strong Paramaters
Rails4.0ではコントローラ層で、Strong Parametersというテクニックを使うことが推奨されている。Strong Parametrsを使うことで、必須のパラメータと許可されたパラメータを指定することができる。
今の場合、paramsaハッシュでは:user属性を必須とし、名前、メールアドレス、パスワード、パスワードの確認の属性を許可し、それ以外を許可しないようにしたい。
class UsersController < ApplicationController : def create @user = User.new(user_params) if @user.save # 保存の成功 else render 'new' end end private def user_params params.require(:user).permit(:name, :email, :password, :password_confirmation) end
この時点で、送信ボタンを押してもエラーが出ないという意味で、ユーザー登録フォームは動く。
また、
(1)間違った送信しても何もフィードバックは返ってこない。
(2)有効なユーザー情報を送信しても新しいユーザーが作成されない。
以下、(1)、(2)を解決していく。
7.3.3 エラーメッセージ
ユーザー登録に失敗した場合の最後の手順として、エラーメッセージを追加する。
コンソールで確認
Railsでは、エラーメッセージはUserモデルの検証時に自動的に生成してくれる。
例えば、ユーザー情報のメールアドレスが無効で、パスワードが短すぎる状態で保存しようとしてみる。
$ rails c >> user = User.new(name: "Foo Bar", email: "foo@invalid", password: "dude", password_confirmation: "dube") >> user.save => false >> user.errors.full_messages => ["Email is invalid", "Password confirmation doesn't match Password", "Password is too short (minimum is 6 characters)"]
errors.full_messagesオブジェクトは、エラーメッセージの配列を持っている点に注目。
エラーメッセージの表示
エラーメッセージをブラウザで表示するには、ユーザーのnewページでエラーメッセージのパーシャルを出力する。
このとき、form-controlというCSSクラスも一緒に追加。
すると、Bootstrapがうまく取り扱ってくれる。
views/users/new.html.erb
<% 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'%> ←classを追加 <%= f.label :email %> <%= f.text_field :email,class: 'form-control' %> ←classを追加 <%= f.label :password %> <%= f.password_field :password ,class: 'form-control'%> ←classを追加 <%= f.label :password_confirmation, "Confirmation" %> <%= f.password_field :password_confirmation,class: 'form-control' %> ←classを追加 <%= f.submit "Create my account", class: "btn btn-primary" %> <% end %> </div> </div>
ここで、'shared/error_messages'というパーシャルをrenderしている点に注目。
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 %>
①:any?メソッドはオブジェクトが空の場合はtrue、それ以外はfalseを返す。empty?の逆
>> user.errors.empty? => false >> user.errors.any? => true
②:countメソッドは、ここでは、エラーの数を返す。
>> user.errors.count => 2
pluralizeテキストヘルパーは次のようになる。
>> helper.pluralize(1, "error") => "1 error" >> helper.pluralize(5, "error") => "5 errors"
pluralizeを使うことで、コードは次のようになる。
<%= pluralize(@user.errors.count, "error") %>.
7.3.4 失敗時のテスト
Railsではフォーム用のテストを書くことができ、このようなプロセスを自動化することができる!ラッキー!
ここでは、無効な送信をしたときの正しい振る舞いについてテスト、とくに統合テストを書いていく。
統合テストとは、手続きや関数といった個々の機能を結合させて、うまく連携・動作しているかを確認するテスト(weblioより)
新規ユーザー登録用の統合テスト
新規ユーザー登録用の統合テストを生成する。コントローラーの慣習である「リソース名は複数形」にちなんで、統合テストのファイル名はusers_signup
$ rails g integration_test users_signup invoke test_unit create test/integration/users_signup_test.rb←①
すると、「integration」フォルダの下に「users_sighup_test.rb」というファイルが生成される①。
このテストでは、ユーザー登録ボタンを押したとき、ユーザーが作成されないことを確認する。
test/integration/users_signup_test.rb
require 'test_helper' class UsersSignupTest < ActionDispatch::IntegrationTest test "invalid signup information" do get signup_path ←① assert_no_difference 'User.count' do ←② post users_path, params: { user: { name: "", ←③ email: "user@invalid", password: "foo", password_confirmation: "bar" } } end assert_template 'users/new' end
①:getメソッドを使ってユーザー登録ページにアクセス
②:assert_no_difference(expressions, message = nil, &block)
式を評価した結果の数値は、ブロックで渡されたものを呼び出す前と呼び出した後で「違いがない」と主張。
今の場合ですと、assert_no_differenceのブロックを実行する前後で引数の値(User.count)が変わらないことをテスト。
すなわち、ユーザー数を覚えた後にデータを投稿してみて、
ユーザ数が変わっていなければ、ユーザー生成が失敗でtrue、変わっていればfalseというわけ。
③:users_pathに対して、POSTリクエストを送信している。params[:user]というハッシュに、User.newで必要なデータをまとめている。ただし、User.countがかわらなくするため、ユーザーが生成されないデータであることに注意。