【Rails】Deviseをカスタマイズして使いたい！！(コントローラ、ビュー編)

# どうした?

前回まででモデルとルーティングを設定し、コントローラとビューファイルを作成しました。
今回はコントローラとビューファイルを作り込んで、

* ユーザー作成
* ユーザー編集
* ログイン
* ログアウト

ができるようにします。

https://qiita.com/ysk91_engineer/items/35d71e25261286a52b7b

# 今回の目標

ユーザー情報を操作するため、`users/resistrations_controller`に

* new
* create
* edit
* update

アクションを実装します。

ログイン情報を操作するため、`users/sessions_controller`に

* new
* create
* delete

アクションを実装します。


# 手順

## ユーザー作成

### ビューファイルの作成

まずはユーザー情報を入力するビューファイルを作成していきましょう。
ユーザーの作成と編集は同じレイアウトなので、部分テンプレートを使用します。
`views/users/resistrations`フォルダ内に`_user_form.html.erb`ファイルを作成します。

前回作成した空の`new.html.erb`ファイルで部分テンプレートを呼び出します。

```erb
<%= render 'user_form' %>
```

`_user_form.html.erb`を以下のように編集します。

```erb
<%= form_with model: @user do |f| %>
  <div class="my-3">
    <%= f.label :name, class: 'form-label fw-bold' %>
    <%= f.text_field :name, class: 'container-fluid' %>
  </div>
  <div class="my-3">
    <%= f.label :email, class: 'form-label fw-bold' %>
    <%= f.text_field :email, class: 'container-fluid' %>
  </div>
  <div class="my-3">
    <%= f.label :password, class: 'form-label fw-bold' %>
    <%= f.password_field :password, class: 'container-fluid' %>
  </div>
  <div class="my-3">
    <%= f.label :password_confirmation, class: 'form-label fw-bold' %>
    <%= f.password_field :password_confirmation, class: 'container-fluid' %>
  </div>
  <div class="my-3">
    <%= f.submit '登録', class: "btn btn-primary" %>
  </div>
<% end %>
```

### コントローラの作成

Deviseで自動作成したコントローラには基本的なCRUDアクションといくつかのメソッドが定義されています。
まずはアクションの中身を見てみましょう。

```ruby
# GET /resource/sign_up
def new
  super
end
```

全てのアクションが`super`、つまり親クラスである`Devise::RegistrationsController`を継承していますね。
この親クラスを見てみると、、

https://github.com/heartcombo/devise/blob/main/app/controllers/devise/registrations_controller.rb

うーん、、、分かるようで分からないような、、、
何だかあちこちから変数を引っ張って来て便利なようにしてくれている風ですが、僕がしたいのはもっとシンプルなものです。
`new`アクションはギリギリ理解できたとしても、`create`や`update`などを完全に乗りこなせる自信がありません。
なので、**`super`は消してしまいましょう。**
僕が`new`アクションでしたいことは、ただ１つ。これだけです。

```ruby
def new
  @user = User.new
end
```

コントローラ内の`protected`以下を見てみると、いくつかのメソッドが定義されています。
これらは[ストロングパラメータ](https://github.com/heartcombo/devise#strong-parameters)の設定に必要なようです。
モデルを作成したとき、Deviseが初めから用意していたカラムを思い出してください。
emailとpassword関連ですね。
それ以外のカラムをストロングパラメータで送信する(今回は`name`)場合に`keys: [:attribute]`の部分を編集するようです。
、、、う〜ん、どこかでエラーが起きそう。
**なので消します。**

結局、コントローラは以下のようになりました。

```ruby
class Users::RegistrationsController < Devise::RegistrationsController
  # before_actionも削除

  def new
    @user = User.new
  end

  def create
    @user = User.new(create_users_params)
    if @user.save
      sign_in(@user)
      redirect_to # 任意のパス
    else
      render 'new'
    end
  end

# edit, updateは後述

  private

  def create_users_params
    params.require(:user).permit(:name, :email, :password)
  end
end
```

Deviseに慣れたプロフェッショナルは`super`やメソッドを使いこなすのかもしれませんが、現在の僕はこれで精一杯です。

`create`アクション内で`sign_in`メソッドを使用しています。
これは[Deviseで用意されているメソッド](https://github.com/heartcombo/devise/blob/main/lib/devise/controllers/sign_in_out.rb#L33)で、認証済のユーザーをログインさせるのに便利です。
`sign_in(@user)`を与えることで`@user`に対して

* ログイン
* セッションの保持
* `/sign_up`, `/login`へのアクセス不可

をしてくれます。

試しに、画面上でサンプルユーザーを1人登録してみましょう。
登録後、コンソールでユーザーが増えていることが確認できたら実装成功です！

## ユーザー編集

続いて、ユーザー情報を編集できるようにします。

### ビューファイルの作成

`edit.html.erb`ファイルにて先程作成した部分テンプレートを呼び出します。

```erb
<%= render 'user_form' %>
```

### コントローラの設定

まずは、アクション内でやりたいことを整理します。

**`edit`アクション**
`new`と同じように、`super`を削除し、現在のユーザーを取得します。

**`update`アクション**
`edit`と同じように、`super`を削除し、現在のユーザーを取得します。
現在のユーザーに対して`update`メソッドに渡された`params`情報で更新するようにします。

ユーザー情報の編集では、パスワードの入力を要求しないようにします。
Deviseはデフォルトでパスワードを更新するとログアウトする仕様になっています。
これをカスタマイズするために

* `update_resource`メソッドをオーバーライドする
* Userモデルに`update_without_current_password`を定義する
* `update_users_params`をストロングパラメータに設定する

という手順をとります。

#### `update_resource`メソッドをオーバーライドする

Deviseはデフォルトで`update_resource`というメソッドを持ち合わせています。
このメソッドはユーザー情報の更新の際にパスワードを更新するとログアウトする仕様となっています。
そのため、コントローラにてメソッドをオーバーライドする必要があります。

```ruby:users/resistrations_controller.rb
private

def update_resource(resource, params)
  resource.update_without_current_password(params)
end
```

#### Userモデルに`update_without_current_password`を定義する

上でオーバーライドしたコードの中で`update_without_current_password`を使用しているので、これをUserモデルで定義します。

```ruby:user.rb
def update_without_current_password(params)
  if params[:password].blank? && params[:password_confirmation].blank?
    params.delete(:password)
    params.delete(:password_confirmation)
  end
  update(params)
end
```

if文内では`password`と`password_confirmation`の両方が空欄のとき、送信するparamsハッシュからそれぞれの存在を削除(nil)しています。
`password`と`password_confirmation`を空欄(blank)ではなく、そもそもそんなキーはない(nil)ことにしてしまうという手法です。
(blankだとバリデーションエラーになるが、nilならストロングパラメータに引っかからない限りparamsとして通すイメージ)

#### `update_users_params`をストロングパラメータに設定する

`create`と`update`では送信するカラムが異なるため、それぞれに対してストロングパラメータを定義します。
`update_users_params`は以下のようになります。

```ruby
def update_users_params
  params.require(:user).permit(:name, :email)
end
```

以上をまとめると、ユーザー情報の更新は以下のようになります。

```ruby:users/resistrations_controller.rb
def edit
  @user = User.find(params[:id])
end

def update
  @user = User.find(params[:id])
  if @user.update(update_users_params)
    redirect_to categories_path, notice: "ユーザー「#{@user.name}」を更新しました。"
  else
    render 'edit'
  end
end

private

# create_users_paramsは先述

def update_users_params
  params.require(:user).permit(:name, :email)
end

def update_resource(resource, params)
  resource.update_without_current_password(params) # update_without_current_passwordはUserモデルで定義
end
```

## ログイン

続いて、ログイン機能です。

### ビューファイルの作成

`views/users/sessions`フォルダ内に`new.html.erb`ファイルを以下のように作成しました。

```erb
<%= form_with url: login_path, scope: :session do |f| %>
  <div class="my-3">
    <%= f.label :email, class: 'form-label fw-bold' %>
    <%= f.text_field :email, class: 'container-fluid' %>
  </div>
  <div class="my-3">
    <%= f.label :password, class: 'form-label fw-bold' %>
    <%= f.password_field :password, class: 'container-fluid' %>
  </div>
  <div class="my-3">
    <%= f.submit 'ログイン', class: "btn btn-primary" %>
  </div>
<% end %>
```

### コントローラの作成

`users/sessions_controller#create`では、ユーザー作成時に使用した`sign_in`メソッドを使用します。

```ruby:users/sessions_controller.rb
def new
end

def create
  @user = User.find_by(email: params[:session][:email])
  if @user.valid_password?(params[:session][:password])
    sign_in(@user)
    redirect_to # 任意のパス
  else
    render 'new', notice: "ログインに失敗しました"
  end
end
```

ここでは、まず入力された`email`を持つユーザーを探し、
`valid_password?`というDevise標準のメソッドを使用してユーザーが持つパスワードと入力されたパスワード情報が一致するかを調べています。
`email`と`password`が一致したら`sign_in`メソッドでユーザーをログインさせます。

ログイン機能はこれで実装完了です。

## ログアウト

### ログアウトリンクの作成

ログアウトリンクは以下のように作成しました。

```erb
<%= link_to('ログアウト', logout_path, method: :delete) if user_signed_in? %>
```

ここでは、`user_signed_in?`というメソッドを使用して条件分岐させています。
このメソッドはDeviseで標準で用意されているメソッドで、セッション情報にユーザーがいるかどうかを調べます。
ちなみにDeviseは`current_user`も標準で用意しており、`current_user.name`なども使用可能です。

### コントローラの作成

ログアウト機能は、セッションから現在のユーザーを削除する機能です。
これはDeviseでも同様におこなっているので、`super`を利用してしまいましょう。

```ruby
def destroy
  super
end
```

ログアウト機能はこれで実装完了です。


# まとめ

以上で、Deviseをカスタマイズして使いたい!!シリーズを完結したいと思います。
Deviseは便利(に思える)一方、中身を分からずに使用する恐怖感もあると思います。
本記事が少しでもDeviseの導入の参考になれば嬉しいです！


# 参考

https://zenn.dev/kitabatake/articles/start-to-like-the-devise

https://easyramble.com/user-account-update-without-password-on-devise.html

## スペシャルサンクス

本記事を書きながら

* Deviseのカスタマイズなんて需要あるんかな、、？
* スクールに通ってる人はみんな勉強済みなのでは、、？

と思っていた僕に、「未経験でソースコードを深堀りするなんて凄いね」と言って下さった現役エンジニアの一言でやる気が起きました。
ありがとうございました！

また、`users/resistrations_controller#update`の実装に沼った際にQiitaの質問記事で沢山の方にお助けいただきました。
ありがとうございました！

https://qiita.com/ysk91_engineer/questions/2b5ccb2244ac39d6cbe6