ウィザード形式の新規登録を実装〜SMS認証を添えて〜

# はじめに
ウィザード形式は、登録フォームをいくつかのページに分けて実装するやり方です。入力項目がたくさんあるサービスの場合に有効です。
ウィザード形式でのフォーム実装は既に色々な方が記事にしており、実装方法も十人十色です。sessionで値を保持したり、ページ毎にDBに値を格納したり。。
やり方としては数多く存在しますが、今回の記事では、そんなウィザード形式の実装方法の一つをご紹介できればと思います。ウィザード形式の実装をするに当たって、参考になった記事は下記に掲載しています。細かな導入方法（deviseの導入方法やTwilioの登録方法など）については本記事では割愛いたしますので、リンク先の記事をご確認くださいませ。。

# 開発条件
- Ruby 2.5.1
- Rails 5.2.3
- Haml・Sass記法
- 'devise'のgemを使用
- Twilioの登録（SMS認証実装のため）

# 実装概要
- 1ページ目でuserテーブルに格納する値を入力してもらう
- 2ページ目でuser_addressテーブルのtelephoneだけ入力してもらう（任意）
- 3ページ目でSMS認証が可能（任意）
- 4ページ目でuser_addressテーブルの残りのカラムを入力してもらう
- 5ページ目で登録完了画面！

# DB設計
ユーザ関係のDBは次のとおりです！
今回は２つのDBを用意して、それぞれに値を格納します。
## usersテーブル
|Column|Type|Options|
|------|----|-------|
|nickname|string|null: false|
|email|string|null: false|
|password|string|null: false|
|password_confirmation|string|null: false|
|last_name|string|null: false|
|first_name|string|null: false|
|last_name_kana|string|null: false|
|first_name_kana|string|null: false|
|birthday|date|null: false|

## user_addressesテーブル
|Column|Type|Options|
|------|----|-------|
|user_id|references|null: false, foreign_key: true|
|last_name|string|null: false|
|first_name|string|null: false|
|last_name_kana|string|null: false|
|first_name_kana|string|null: false|
|postal_code|integer|null: false|
|prefecture|integer|null: false|
|city|string|null: false|
|address|string|null: false|
|building|string||
|telephone|integer||

# 実装コード！
完成したコードがこちらです！
## routes
ルート設定は以下のとおり。もう少しスマートに書きたい。。
HTTPのメソッドとしては、最初に呼び出される`user_info`と最後に表示される`user_complete`はGETで、2,3,4ページはPOSTで通信を行なっています。

```ruby:routes.rb
Rails.application.routes.draw do
 devise_for :users, controllers: {
    sessions: 'users/sessions',
    registrations: 'signup'
  } 

  resources "signup", only: [:index, :create], path: "/signup" do
    collection do
      get 'user_info' # １ページ目
      post 'user_tel' # ２ページ目
      post 'user_tel_verification' # ３ページ目
      post 'user_address' # ４ページ目
      get 'user_complete' # 登録完了後のページ
    end
  end
end
```

## models
モデルも２つ用意しました！
バリデーションはお好みで実装できます。

```ruby:user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  has_one :user_address
  accepts_nested_attributes_for :user_address
end
```
```ruby:user_address.rb
class UserAddress < ApplicationRecord
  belongs_to :user, inverse_of: :user_address
```

## controller
`before_action`が多くなってしまいましたが、取り敢えずはこんな感じに実装。
SMSに関する記述はコントローラーに書いてます。`session`はかなり苦戦した部分ではありますが、今では（気持ちの一方通行ではありますが）マブダチだと思っています！

```ruby:signup.controller.rb
class SignupController < ApplicationController
  #before_action多すぎ？？
  before_action :save_user_info_to_session, only: :user_tel
  before_action :save_user_tel_to_session, only: :user_tel_verification
  before_action :send_sms_code, only: :user_tel_verification
  before_action :check_sms_code, only: :user_address
  before_action :save_user_address_to_session, only: :create

  def user_info #1ページ目(userテーブル)
    @user = User.new
    @user.build_user_address
  end

  def save_user_info_to_session #1ページ目登録後に呼び出し、sessionに値を渡す
    session[:user_params] = user_params
    @user = User.new(session[:user_params]) # インスタンス作成時にsessionを引数として渡す
    render '/signup/user_info' unless @user.valid?
  end 

  def user_tel #2ページ目(user_addressテーブル)
    @user = User.new
    @user.build_user_address
  end

  def save_user_tel_to_session #2ページ目登録後に呼び出し、sessionに値を渡す
    session[:user_address_attributes_after_user_tel] = user_params[:user_address_attributes] # この時点では[:user_address_attributes]にはtelephoneしか格納されていない
    @user = User.new(session[:user_params]) #　ここでもsessionを渡す
    @user.build_user_address(session[:user_address_attributes_after_user_tel]) # インスタンス作成時にsessionを引数として渡す
    render '/signup/user_tel' unless @user.valid?
  end

  def send_sms_code #2ページ目で登録した携帯番号にSMSを飛ばす記述
    @user = User.new
    @user.build_user_address
    send_phone_number = PhonyRails.normalize_number session[:user_address_attributes_after_user_tel][:telephone], country_code:'JP' #入力された携帯番号を変数に渡す
    session[:secure_code] = random_number_generator(4) #ランダムな4桁を生成するアクションに飛んだのち、sessionに渡す
    begin
      client = Twilio::REST::Client.new
      result = client.messages.create(
        from: ENV["TWILIO_PHONE_NUMBER"], #Twilioで取得した電話番号
        to:   send_phone_number, # 送り先
        body: "認証番号：#{session[:secure_code]}" # 送る内容
      )
    rescue Twilio::REST::RestError => e #エラー時の処理
      @messages = "エラーコード[#{e.code}] ：”#{e.message}”"
      render '/signup/user_tel'
    end
  end

  def random_number_generator(n) #ランダムな4桁の番号を生成するアクション
    ''.tap { |s| n.times { s << rand(0..9).to_s } }
  end

  def user_tel_verification #3ページ目(SMSで送ったコードの確認ページ)
    @user = User.new
    @user.build_user_address
  end

  def check_sms_code #3ページ目で入力された認証コードを照らし合わせる
    session[:input_code] = user_params[:user_address_attributes] #入力されたコードをsessionに渡す
    @user = User.new
    @user.build_user_address
    redirect_to action: :user_tel_verification unless session[:input_code].present? #値が入力されているか確認
    render '/signup/user_tel_verification' and return unless session[:secure_code] == session[:input_code][:telephone] #SMSで送ったコードと実際に入力されたコードの照合
  end

  def user_address #4ページ目(user_addressテーブル)
    @user = User.new
    @user.build_user_address
  end

  def save_user_address_to_session #4ページ目登録後に呼び出し、sessionに値を渡す
    session[:user_address_attributes_after_user_address] = user_params[:user_address_attributes]
    session[:user_address_attributes_after_user_address].merge!(session[:user_address_attributes_after_user_tel]) # ４ページ目に登録したアドレスと2ページ目に登録した電話番号を結合
    @user = User.new
    @user.build_user_address
    render '/signup/user_address' unless session[:user_address_attributes_after_user_address].present? #本来はここでバリデーションしたいが、上手く行かないため止むを得ずpresent?
  end

  def create #4ページ目までの登録が完了した時点で、一気にDBに突っ込む
    @user = User.new(session[:user_params])
    @user.build_user_address(session[:user_address_attributes_after_user_address])
    if @user.save
      sign_in User.find(@user.id) #ログイン状態を保持する記述
      redirect_to user_complete_signup_index_path #登録完了画面にリダイレクト
    else
      render user_address_signup_index_path # ダメだった場合4ページ目にリダイレクト
    end
  end

  def user_complete #登録完了ページにて、sessionの削除(明示的に消さないとずっと残ってしまうため)
    session[:user_params].clear
    session[:secure_code].clear
    session[:input_code].clear
    session[:user_address_attributes_after_user_address].clear
  end

  private

  def user_params #ストロングパラメーターでユーザに関わる情報を保護
    params.require(:user).permit(
      :nickname, 
      :email, 
      :password, 
      :password_confirmation, 
      :last_name, 
      :first_name, 
      :last_name_kana, 
      :first_name_kana, 
      :birthday, 
      user_address_attributes: [:id, :last_name, :first_name, :last_name_kana, :first_name_kana, :postal_code, :prefecture, :city, :address, :building, :telephone]
    )
  end
end
```
## views
全てのページの実装コードを記述してしまうと量が膨大になってしまうので省略。
`form_for`は非推奨なので、`form_with`に書き換えたほうが良いですね。。
そうなると、書き方としては、`= form_with model: @user, url: ○○_path, local: true do |f|`となると思います。

```ruby:user_address.html.haml
.signup-panel
  %p.signup-panel__title 住所入力
  = form_for @user, url: ○○○○_path, method: :post, html: {class: "inputForm"} do |f|
    = f.fields_for :user_address do |p| #user_addressテーブルと紐付ける
      .signup-form
        .field
          = p.label "お名前"
          %span.form-require
          = p.text_field :last_name, class: "field__input", placeholder: "例）山田"
          = p.text_field :first_name, class: "field__input", placeholder: "例）太郎"
# ------------------------------------<中略>----------------------------------------------------
      .signup-btn
        = f.submit "次へ進む"
```

ちなみに。。
SMSで送った認証コードを入力してもらうフォームですが、`label`や`text_field`に`:input_code`と指定することはできません。なので、取り敢えずは下記のように`:telephone`を指定してあげるとエラーは起きません。DBには保存せず、sessionに渡してあげるだけなので、このような記載でも問題ないと思います！

```ruby:user_tel_verification.html.haml
.signup-panel
    %p.signup-panel__title
      電話番号認証
    = form_for @user, url: ○○○○_path, method: :post, html: {class: "inputForm"} do |f|
      = f.fields_for :user_address do |p|
        .signup-form
          .field
            = p.label :telephone, "認証番号"
            %span.form-require
            = p.text_field :telephone, placeholder: "認証番号を入力"
```
# 終わりに
今回、復習をかねての記事投稿でしたが、アウトプットするとやはり知識の定着度は違いますね！取り敢えずは動くようになりましたが、まだまだリファクタリングが必要な部分もあるかと思いますし、これからも勉強継続は必要だなと思います。文章書くのは苦手ですが、アウトプットもなんとか継続していきたいです笑

# 参考
### ウィザード形式関係
[ウィザードフォーム形式フォームの実装 -devise×session×データを複数テーブルに分けて保存 ver.- (rails)](https://qiita.com/hrwtrhnk/items/657375db8b6b79bc35e0)
[[Rails]ウィザードフォーム実装でfields_forを使って複数のテーブルに保存する](https://qiita.com/NT90957869/items/614842934feff9812203)
### SMS認証関係
[【Rails】Twilioを使ってSMS認証を実装してみる（めも）](https://qiita.com/ozackiee/items/f224a2d57b93523c8aec)