[Rails]deviseを使ったウィザード形式での新規登録機能の実装

# はじめに
フリマのコピーサイトを作る際に、構造をやや難しくしてしまったために苦労したので記録として残そうと思いました。もっと綺麗にかけるなどご指摘あればお願いします。
# 実装方法

### 開発環境
- Ruby 2.5.1
- Rails 5.0.7.2
- devise

###前提
- deviseは導入済み
- deviseのデフォルト状態でのログイン機能は実装済み
- 1ページ目 → 2ページ目 → トップページ(ログイン)と遷移する
- テーブルは、`users` `profiles` `sending_destinations`の３つ
- 1ページ目は1つのフォームで`Userモデル`と`Profileモデル`の二つを扱っている  
※２つのモデルに同時に値を送る方法はこちらの記事を参考にしています。  
[【Rails】deviseのフォームで２つのモデルに同時に値を送る方法(例: UserモデルとProfileモデル)](https://qiita.com/wonder_meet/items/9238d9bedea542ab975b)
  
### DB設計

#### usersテーブル
|Column|Type|Options|
|------|----|-------|
|name|string|null: false, unique: true, index:true|
|email|string|null: false, unique: true, index:true|
|password|string|null: false|

#### profilesテーブル
|Column|Type|Options|
|------|----|-------|
|first_name|string|null: false|
|family_name|string|null: false|
|first_name_kana|string|null: false|
|family_name_kana|string|null: false|
|introduction|string|null: true|
|year|integer|null: false|
|month|integer|null: false|
|day|integer|null: false|

#### sending_destinations(住所)テーブル
|Column|Type|Options|
|------|----|-------|
|first_name|string|null :false|
|family_name|string|null: false|
|first_name_kana|string|null: false|
|family_name_kana|string|null: false|
|post_code|string|null: false|
|prefecture|string|null: false|
|city|string|null:false|
|house_number|string|null: false|
|building_name|string| - |
|phone_number|string|unique: true, null: true|
|user_id|references|null: false, foreign_key: true|

##コード
###controllerの作成

- devise管理下の`users`コントローラーを作成する

```rails:ターミナル
$ rails g devise:controllers users
```

- どのコントローラーを参照するのかルーティングを設定

```ruby:routes.rb
Rails.application.routes.draw do
  devise_for :users, controllers: { registrations: 'users/registrations' }
  root to: 'items#index'
end
```

###モデルにアソシエーションを記述する
- `User`、`Profile`、`Sending_destination`それぞれのモデルにアソシエーションを記述  
※バリデーションは省略しています。

```ruby:user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  has_one :profile
  accepts_nested_attributes_for :profile
  has_one :sending_destination
end
```
```ruby:profile.rb
class Profile < ApplicationRecord
  belongs_to :user, optional: true
end
```
```ruby:sending_destination.rb
class SendingDestination < ApplicationRecord
  belongs_to :user, optional: true
end
```
`optional: true`は外部キーがnullであることを許可するオプションです。

###newアクションと対応するビューを編集する(1ページ目)

- users/registrations_controller.rbにnewアクションを記述

```ruby:users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
  # 省略
  def new
    @user = User.new
    @user.build_profile
  end 
  # 省略
end
 
```

- newアクションに対応するregistrations/new.html.hamlを編集

```ruby:devise/registrations/new.haml.html
= form_for(@user, url: user_registration_path) do |f|
  = render "devise/shared/error_messages", resource: @user
  = f.text_field :nickname
  = f.email_field :email
  = f.password_field :password
  = f.password_field :password_confirmation
  = f.fields_for :profile do |p|
    = p.text_field :family_name
    = p.text_field :first_name
    = p.text_field :family_name_kana
    = p.text_field :first_name_kana
    = p.select :year
    = p.select :month
    = p.select :day
  = f.submit "次へ"
```
実際はdivやlabel、classの記載がありますが、簡素に書いています。
2つのモデルを扱うために`fields_for`を利用しています。以下の記事を参考にしています。
[【Rails】deviseのフォームで２つのモデルに同時に値を送る方法(例: UserモデルとProfileモデル)](https://qiita.com/wonder_meet/items/9238d9bedea542ab975b)
[【Rails】1つのform_forで複数モデルへデータ登録をする方法](https://qiita.com/hiroweb/items/74867433ab5091713521)

###createアクションを編集
- 1ページ目で入力した情報のバリデーションチェック
- 1ページで入力した情報をsessionに保持させる
- 次の住所情報登録で使用するインスタンスを生成、当該ページへ遷移する
以上の3点がcreateアクションでやることになります。

```ruby:users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
  # 省略
  def create
    @user = User.new(sign_up_params)
    @user.build_profile(sign_up_params[:profile_attributes])
    unless @user.valid?
      flash.now[:alert] = @user.errors.full_messages
      render :new and return
    end
    session["devise.regist_data"] = {user: @user.attributes}
    session["devise.regist_data"][:user]["password"] = params[:user][:password]
    session[:profile_attributes] = sign_up_params[:profile_attributes]
    @sending_destination = @user.build_sending_destination
    render :new_sending_destination
  end
  # 省略
  protected
  # 省略
end

```

`valid?`メソッドでパラメータがバリデーションに違反しないかどうかチェックします。 
ページが遷移しても情報が消えることが無いように、クライアント側で保持をさせておく機能`session`を用いています。 
sessionにハッシュオブジェクトの形で情報を保持させるために、attributesメソッドを用いてデータを整形しています。また、paramsの中にはパスワードの情報は含まれていますが、attributesメソッドでデータ整形をした際にパスワードの情報は含まれていません。そこで、パスワードを再度sessionに代入する必要があります。 
`build_sending_destination`で今回生成したインスタンス`@user`に紐づく`sending_destination`モデルのインスタンスを生成します。 
そして、住所情報を登録させるページを表示する`new_sending_destination`アクションのビューへrenderします。

### new_sending_destinationアクションと対応するビューを編集する(2ページ目)

- 住所登録をするページを表示する`new_sending_destination`アクションのルーティングの設定
- 住所を登録する`create_sending_destination`アクションのルーティングの設定

```ruby:routes.rb
Rails.application.routes.draw do
  devise_for :users, controllers: { registrations: 'users/registrations' }
  devise_scope :user do
    get 'sending_destinations', to: 'users/registrations#new_sending_destination'
    post 'sending_destinations', to: 'users/registrations#create_sending_destination'
  end

  root to: 'items#index'
end
```

- 該当するビューファイルであるnew_sending_destination.html.hamlを作成

```ruby:devise/registrations/new_sending_destination.html.haml
= form_for @sending_destination do |f|
  = render "devise/shared/error_messages", resource: @sending_destination
  = f.text_field :family_name
  = f.text_field :first_name
  = f.text_field :family_name_kana
  = f.text_field :first_name_kana
  = f.text_field :post_code
  = f.text_field :prefecture
  = f.text_field :city
  = f.text_field :house_number
  = f.text_field :building_name
  = f.number_field :phone_number
  = f.submit "登録する"  
```

### create_sending_destinationアクションを編集する

- 2ページ目で入力した住所情報のバリデーションチェック
- バリデーションチェックが完了した情報と、sessionで保持していた情報とあわせ、ユーザー情報として保存する
- sessionを削除する
- ログインをする

```ruby:users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
  # 省略
  def create_sending_destination
    @user = User.new(session["devise.regist_data"]["user"])
    @profile = @user.build_profile(session[:profile_attributes])
    @sending_destination = SendingDestination.new(sending_destination_params)
    unless @sending_destination.valid?
      flash.now[:alert] = @sending_destination.errors.full_messages
      render :new_sending_destination and return
    end
    @user.build_sending_destination(@sending_destination.attributes)
    @user.save
    @profile.save
    session["devise.regist_data"]["user"].clear
    session[:profile_attributes].clear
    sign_in(:user, @user)

    redirect_to root_path
  end

  protected

  def sending_destination_params
    params.require(:sending_destination).permit(
      :first_name,
      :family_name,
      :first_name_kana, 
      :family_name_kana, 
      :post_code, 
      :prefecture, :city, 
      :house_number, 
      :building_name, 
      :phone_number
    )
  end
end
```

`@user`と`@prfile`それぞれのインスタンスにsessionで保持した情報を代入しています。　
2ページ目の住所情報を`valid?`でチェックします。　
build_sending_destinationを用いて送られてきたparamsを、保持していたsessionが含まれる`@user`に代入します。そして`save`メソッドを用いてテーブルに保存します。 
`clear`メソッドを用いてsessionを削除します。 
`sign_in(:user, @user)`でログインし、`redirect_to root_path`でトップページに遷移します。

## おわりに
以上の方法で実装できます。1ページ目で`fields_for`を用いたことでsessionへの代入にかなり四苦八苦しましたが、おかげでしっかり仕組みなどを考えることができました。細かな説明はしていませんが、アウトプットも兼ねて記事にしてみたので少しでも参考になれば幸いです。