[Rails]gemを使わないWizard形式の登録フォームを作った

#はじめに
どうもこんにちは、Chihaと申します。
某スクールにてチームで某フリマアプリのコピーを作成しており、実装物のコードについて記録を残しつつ、開発チームで共有する目的で当記事を書いています。

タイトルの通り、gemを使わないWizard形式のユーザー登録フォームを作ったので、今回はそのことについて記事を書こうと思います。（フォーム処理の終わりにdeviseのメソッドを利用しています）

# そもそもWizard形式のフォームとは？
Wizard型とかWizard形式とか呼ばれてますが、正確に説明のある記事を見つけられませんでした。
複数ステップによる登録フォーム形式です。
某[メ◯カリ](https://www.mercari.com/jp/signup/registration/)の新規登録ページなどに採用されています。
ページあたりの入力量が減って見通しが良くなるような気がします。
~~[こういう記事](https://f-tra.com/ja/blog/column/5696)を読んでいると、単一フォームと複数フォームはどちらが良いのか良く分からなくなりましたが。~~

それは置いといて、本題に移ります。

#概要
今回は下のようなページ構成でフォームを実装しました。
<img width="957" alt="スクリーンショット 2019-10-03 19.07.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492745/b66bf336-5a4b-d70b-f7e1-63454997757d.png">

フォームの間で動く処理はこんな感じです
### wizardフォーム概要図
![Wizardフォーム改二.002.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492745/496a22a8-e745-f6f5-1e24-e022a5ff6b93.jpeg)

メソッド名は簡略化して書いています。
##### 以後、図の①~⑥及び❶~❻という表記が頻出します。

フォームとフォームの間にpostアクションでバリデーション判定を挟み、入力値をsessionに登録することで、フォームから得た情報をDBに保存することなく複数フォームでの入力を可能にしました。
~~見辛かったら申し訳ありません。初めてkeynote使いました。~~

今回はクレジット認証やSMS認証の内容は置いといて、wizard形式の仕組みとバリデーションについて書いていきます。

#開発環境
- Ruby on Rails 5.2.2
- Ruby 2.5.1
- haml
- gem devise

#書いたコード
全部書くと非常に長くなるので
![Wizardフォーム改.002.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492745/463bda1a-ef73-e35e-e2d6-dce59721fc8f.jpeg)
今回はこの部分を中心に説明していきます。

### モデルとアソシエーション
データベース構成は以下の通りです。
#### Usersテーブル

|Column|Type|Options|
|------|----|-------|
|nickname|string|null: false|
|email|string|null: false,unique: true|
|password|string|null: false|
|encrypted_password|string|null: false|

#### アソシエーション 

- has_one  :profile,         dependent: :destroy


#### Profilesテーブル

|Column|Type|Options|
|------|----|-------|
|user_id|references|foreign_key: true|
|avatar|string||
|birthyear|integer|null: false|
|birthmonth|integer|null: false|
|birthday|integer|null: false|
|family_name|string|null: false|
|personal_name|string|null: false|
|family_name_kana|string|null: false|
|personal_name_kana|string|null: false|
|postal_code|integer|null: false|
|prefecture|string|null: false|
|city|string|null: false|
|address|string|null: false|
|building|string||
|tel|integer||
|post_family_name|string|null: false|
|post_personal_name|string|null: false|
|post_family_name_kana|string|null: false|
|post_personal_name_kana|string|null: false|

#### アソシエーション

- belongs_to :user

#### バリデーション

```ruby:user.rb
class User < ApplicationRecord
  # userにはdeviseを使用しています（ログイン、パスワードの暗号化に使うため）
  devise   :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable
  #　アソシエーション 
  has_one  :profile,       dependent: :destroy
  has_one  :creditcard,    dependent: :destroy

  ~~~中略~~~

  # 各項目のバリデーション
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  PASSWORD_VALIDATION = /\A(?=.*?[a-z])(?=.*?\d)[a-z\d]{7,128}+\z/i
  validates :email,                 presence: true, uniqueness: { case_sensitive: false }, format: { with: VALID_EMAIL_REGEX }
  validates :password,              presence: true, length: {minimum: 7, maximum: 128},    format: { with: PASSWORD_VALIDATION }
  validates :password_confirmation, presence: true, length: {minimum: 7, maximum: 128}
  validates :nickname,              presence: true, length: {maximum: 20}
end

```

```ruby:profile.rb
class Profile < ApplicationRecord
  # アソシエーション 
  belongs_to :user
  POSTAL_CODE_VALID = /\A\d{3}-\d{4}\z/i
  # バリデーション
  validates :birthyear,               presence: true
  validates :birthmonth,              presence: true
  validates :birthday,                presence: true
  validates :family_name,             presence: true, length: {maximum: 35}
  validates :personal_name,           presence: true, length: {maximum: 35}
  validates :family_name_kana,        presence: true, length: {maximum: 35}
  validates :personal_name_kana,      presence: true, length: {maximum: 35}
  validates :postal_code,             presence: true, length: {maximum: 100}, format: { with: POSTAL_CODE_VALID }
  validates :prefecture,              presence: true, length: {maximum: 100}
  validates :city,                    presence: true, length: {maximum: 50}
  validates :address,                 presence: true, length: {maximum: 100}
  validates :post_family_name,        presence: true, length: {maximum: 35}
  validates :post_personal_name,      presence: true, length: {maximum: 35}
  validates :post_family_name_kana,   presence: true, length: {maximum: 35}
  validates :post_personal_name_kana, presence: true, length: {maximum: 35}
  validates :tel,                                     length: {maximum: 100}
end
```

### ルーティング

```ruby:routes.rb
Rails.application.routes.draw do
  devise_for :users, skip: :all
  devise_scope :user do
    delete 'destroy' => 'devise/sessions#destroy',as: :current_user_destroy
  end
  
  #〜〜〜中略〜〜〜

  resources :signup ,only: [:index,:create] do  #createが図❻ 
    collection do
      #今回はここから
      get 'registration'                               # 図①  
      post 'registration' => 'signup#first_validation' # 図❶
      #ここまでを中心に解説します
      get 'sms_authentication'                         # 図②
      post 'sms_authentication' => 'signup#sms_post'   # 図❷
      get 'sms_confirmation'                           # 図③
      post 'sms_confirmation' => 'signup#sms_check'    # 図❸
      get 'address'                                    # 図④
      post 'address' => 'signup#second_validation'     # 図❹
      get 'creditcard'                                 # 図⑤
      get 'done'                                       # 図⑥
    end
  end
end
```
図と名称が違う部分がありますが文字数の関係です。
postアクションを間に挟まず次のgetアクションに飛ばしても良かったのですが、ユーザーの入力値は個人情報です。
ということで、get→post→get→post…とすることで、個人情報をより安全に処理できるように実装を行いました。
getとpostの違いについては、僕もあまり詳しく理解できていないのですが、
>GET メソッドは Web サーバーのリソースを取得するもので、Web サーバーのリソースの変更などを伴わない場合に使用します。パラメーターは URL の後ろに追加され、長さにも制限があります。

>一方、POST メソッドは Web サーバーのリソースを変更する場合に使用します。パラメーターはリクエストボディに書き込まれるため、URL にパラメーターは表示されません。また、基本的に長さの制限もありません。

>GET メソッドは URL にパラメーターが表示されるので、アクセスログに情報が残ってしまいます。

>対象 Web サーバーのアクセスログに情報が残ってしまうと、その情報が個人情報やログイン情報だった場合、重要情報としてアクセスログを管理する必要が出てきてしまいます。アクセスログが漏洩したら、重要情報の漏洩になってしまうためです。

>ですので、重要情報を送信する場合は、最初から問題が起こらないように POST メソッドを使用するようにしましょう。

>引用：[重要情報は POST で送信する](https://www.websec-room.com/2013/02/24/204)

とあるように、getアクションで値を送信するのは避けたほうが良いようです。
このことから、上記の仕様で実装しました。

### ビューファイル
```haml:registration.html.haml
- content_for(:html_title) {'会員情報入力'}
= render partial: 'signup-header1'
.signup-container
  %h2.signup-header 会員情報入力
  .registration-form

    = form_for @user, url: registration_signup_index_path,method: :post do |f|
      .signup-form-container.registration-form__first-container
        .signup-form-container__title
          = label :nickname, 'ニックネーム', class: 'signup-label'
        = f.text_field :nickname, placeholder: '例）メルカリ太郎'
      .signup-form-container
        .signup-form-container__title
          = label :email, 'メールアドレス'
        = f.text_field :email, placeholder: 'PC・携帯どちらでも可'
      .signup-form-container
        .signup-form-container__title
          = label :password, 'パスワード'
        = f.password_field :password,placeholder: '７文字以上'
      .signup-form-container
        .signup-form-container__title
          = label :password_confirmation, 'パスワード（確認）'
        = f.password_field :password_confirmation, placeholder: '7文字以上'

      //今回はprofileにも同時に値を送りたいのでfields_forを使っています。
      = fields_for @profile,url: registration_signup_index_path,method: :post do |o|
        .signup-form-container
          .signup-form-container__title
            = label :name, 'お名前(全角)', class: 'signup-label'
          .signup-form-container__name
            = o.text_field :family_name, placeholder: '例）山田'
            = o.text_field :personal_name, placeholder: '例）彩'
      ~~~項目が多いので中略~~~
      = f.submit '次へ進む'

```
見やすくするためにclassの表記や間のp要素などを簡略化しています。
### コントローラ
```ruby:signup_controller.rb
class SignupController < ApplicationController
  layout 'form_layout'
  # 図②への移動時に動く不正アクセス対策
  before_action :redirect_to_index_from_sms,only: :sms_authentication

  #~~~中略~~~
  def index
  end
  # 図①の処理
  def registration
    @user = User.new
    @profile = Profile.new
  end

  # 図❶の処理
  def first_validation
    #入力値を全てsessionに保存
    session[:nickname] = user_params[:nickname]
    session[:email] = user_params[:email]
    session[:password] = user_params[:password]
    session[:password_confirmation] = user_params[:password_confirmation]
    session[:birthyear] = profile_params[:birthyear]
    session[:birthmonth] = profile_params[:birthmonth]
    session[:birthday] = profile_params[:birthday]
    session[:family_name] = profile_params[:family_name]
    session[:personal_name] = profile_params[:personal_name]
    session[:family_name_kana] = profile_params[:family_name_kana]
    session[:personal_name_kana] = profile_params[:personal_name_kana]
    #バリデーション判定用にuserをnewします
    @user = User.new(
      nickname: session[:nickname],
      email: session[:email],
      password: session[:password],
      password_confirmation: session[:password_confirmation]
    )
    #プロフィールも同様にnewします。未入力の項目はバリデーションに引っかからない値を仮置きします
    @profile = Profile.new(
      user: @user,
      birthyear: session[:birthyear],
      birthmonth: session[:birthmonth],
      birthday: session[:birthday],
      family_name: session[:family_name],
      personal_name: session[:personal_name],
      family_name_kana: session[:family_name_kana],
      personal_name_kana: session[:personal_name_kana],
      post_family_name: "仮登録",
      post_personal_name: "仮登録",
      post_family_name_kana: "カリ",
      post_personal_name_kana: "トウロク",
      prefecture: '沖縄',
      city: '那覇市',
      address: 'テスト',
      postal_code: '888-8888'
    )
    # バリデーションエラーを事前に取得させる（下のunlessでは全て取得できない場合があるため）
    check_user_valid = @user.valid?
    check_profile_valid = @profile.valid?
    #reCAPTCHA（私はロボットではありませんのアレ）とユーザー、プロフィールのバリデーション判定
    unless verify_recaptcha(model: @profile) && check_user_valid && check_profile_valid
      render 'signup/registration' 
    else
      # 問題がなければsession[:through_first_valid]を宣言して次のページへリダイレクト
      session[:through_first_valid] = "through_first_valid"
      redirect_to sms_authentication_signup_index_path
    end
  end

 # 図②のアクション
  def sms_authentication
    @profile = Profile.new
  end
  ~~~続く~~~

```
次のsms_authenticationアクションに移行する際に、session[:through_first_valid]の有無を判定するメソッドをbefore_actionで動かしています。

```ruby:signup_controller.rb
 ~~~続き~~~
 # 中略
 private
   #　userとprofileのストロングパラメータ
  def user_params
    params.require(:user).permit(:nickname,:email,:password,:password_confirmation)
  end

  def profile_params
    params.require(:profile).permit(:birthyear,:birthmonth,:birthday,:family_name,:personal_name,:family_name_kana,:personal_name_kana,:postal_code,:prefecture,:city,:address,:building,:tel,:post_family_name,:post_personal_name,:post_family_name_kana,:post_personal_name_kana)
  end

   # 前のpostアクションで定義されたsessionがなかった場合登録ページトップへリダイレクト
  def redirect_to_index_from_sms
    redirect_to signup_index_path unless session[:through_first_valid].present? && session[:through_first_valid] == "through_first_valid"
  end
```
不正アクセス対策として、session[:through_first_valid]を持っていなかったら次のフォームへの遷移を止め、登録ページトップへとリダイレクトするようにしています。

コントローラーに長い処理を書いてしまいましたが、分かりやすくするためです。
~~そんなことないので次からはモデルに切り出します。~~

**上記のようにページごとの入力内容をsessionに登録しつつバリデーション判定をかける作業を繰り返し行い、最後のページのpostでsessionの値を用いてユーザーをcreateします。**

```ruby:signup_controller.rb
  # ユーザー情報の一括create（図❻のアクション）
  def create
    @user = User.new(nickname: session[:nickname],email: session[:email],password: session[:password],password_confirmation: session[:password_confirmation])
    # 万一ユーザーがcreateできなかった場合、全sessionをリセットして登録ページトップへリダイレクト
    unless @user.save
      reset_session
      redirect_to signup_index_path
      return
    end
    # userが作れたらuserに紐づけてprofileを作ります
    @profile = Profile.create(
      user: @user,
      birthyear: session[:birthyear],
      birthmonth: session[:birthmonth],
      birthday: session[:birthday],
      family_name: session[:family_name],
      personal_name: session[:personal_name],
      family_name_kana: session[:family_name_kana],
      personal_name_kana: session[:personal_name_kana],
      post_family_name: session[:post_family_name],
      post_personal_name: session[:post_personal_name],
      post_family_name_kana: session[:post_family_name_kana],
      post_personal_name_kana: session[:post_personal_name_kana],
      prefecture: session[:prefecture],
      city: session[:city],
      address: session[:address],
      postal_code: session[:postal_code],
      tel: session[:tel],
      building: session[:building]
    )
    # 最後のフォームでクレジット認証を行なっているため、ここでカードの顧客情報を作り、userと紐づけてDBに保存する処理を行なっています
    customer = Payjp::Customer.create(card: params[:payjp_token])
    @creditcard = Creditcard.new(user: @user,customer_id: customer.id,card_id: customer.default_card)
    # カード情報まで保存に成功したら全sessionをリセットしてユーザーidのみsessionに預け、完了画面へリダイレクト
    if @creditcard.save
      reset_session
      session[:id] = @user.id
      redirect_to done_signup_index_path
      return 
    else
      #失敗したらsessionを切って登録ページトップへリダイレクト
      reset_session
      redirect_to signup_index_path
    end
  end

  # 図⑥のアクション
  def done
    # session[id]がなければ登録ページトップへリダイレクト
    unless session[:id]
      redirect_to signup_index_path 
      return
    end
    # deviseのメソッドを使ってログイン
    sign_in User.find(session[:id])
  end
  
```
かなり簡略化しましたが、このような流れでwizard形式でページ変遷をしつつ、最後に一括createをするフォームを作成しました。

SMS認証の処理については、こちら（[[Rails]Twilioを使った簡易SMS認証を作った](https://qiita.com/chiha-chiha/items/dc1080db79d90132932f)）に書いています。自信はありませんが是非読んでみてください。

クレジットカード認証の処理については今後記事を書こうと思います。

# 参考記事
今回の実装において、ページごとのバリデーション周りの記述については、
[ウィザード形式の登録フォームでバリデーション](https://qiita.com/ATORA1992/items/c073620be3469bfa1f54)
こちらを参考にさせていただきました。

# 最後に
コントローラーに処理をほとんど書いてしまったことが今回の反省点だと思っています。
今後はファットモデルを目指せるようなコーディングを意識していけたらいいなぁ。
現在開発しているアプリケーションのうち、僕が担当した機能についてはできるだけ記事に残していこうと思います。

~~スクールでの学習も終盤に差し掛かり卒業間近となりましたが、日々コードを書くのが楽しいので就活のことなんぞ考えずにコード書いていたい。~~
そうは行かないので、就活と学習、もといコーディングの両立ができるように頑張っていこうと思います。

まだまだ粗末な点も多いと思いますが、より良いコード、間違った点などがあればご教授頂けると幸いです。