RailsのFormオブジェクトをついに実装した

## 前書き

- `モノリシックなRailsアプリで、ActiveRecordに紐づかないモデルに対するフォームは、Form Objectを使えば綺麗に書ける`らしい。
- というのは前から知っていたが、実際自分の手で実装したことはなかった。
- この度、自分の手で実装する機会が業務で得られたので、記録を残す。
    - 公表して問題ない程度に実装をぼやかしているので、その過程でタイポなどがあるかもしれないです。ご了承ください。


## 作りたいもの

- アプリへのpush通知を送ってくれるページを管理者用のページとして追加したい。


## Formオブジェクトを使わない実装

- まずはFormオブジェクトを使わずに以下のように実装した。

```config.rb
namespace :admin do
  resources :push_notifications, only: [:new, :create]
end
```

```app/controllers/admin/push_notifications_controller.rb

class Admin::PushNotificationsController < ApplicationController
  def new
    render 'new'
  end

  def create
    # user_idがintに変換できない場合エラーが吐かれるが、formでf.number_fieldを指定しているので良いとする
    self.user_id = Integer(user_id)
    user_id = Integer(push_notification_params[:user_id])
    user = User.find_by(id: user_id)

    unless user
      flash.now[:alert] = '指定されたユーザーIDは無効です。'
      return render
    end

    title = push_notification_params[:title]

    if title.blank?
      flash.now[:alert] = "titleは空文字ではいけません。"
      return render :new
    end

    # Push通知を送るサービスクラス
    PushNotificationService.new.notify(
      user_id: user_id,
      title: title,
      body: push_notification_params[:body],
      url: push_notification_params[:url],
    )
    flash[:notice] = "ユーザー#{user_id}へのプッシュ通知が送信されました。"
    redirect_to new_admin_push_notification_path
  end

  private
  def push_notification_params
    params.permit(:user_id, :title, :body, :url)
  end
end
```


```app/views/admin/new.html.haml

%h1= "push通知のテスト送信"

= form_with(url: admin_push_notifications_path, local: true) do |f|
  .div
    = f.label :user_id, 'User ID (必須)'
    = f.number_field :user_id, autofocus: true, required: true
  .div
    = f.label :title, 'Title: (必須)'
    = f.text_area :title, autofocus: true, required: true
  .div
    = f.label :body, 'Body'
    = f.text_area :body, autofocus: true
  .div
    = f.label :url, 'URL'
    = f.text_field :url, autofocus: true
  .div
    = f.submit("送信")

```


- ご覧の通り、やりたい処理``以前に、Controller内で行われているバリデーションの連発が悪い意味で印象的です。


## これをFormオブジェクトを使って実装する

```config.rb
namespace :admin do
  resources :push_notifications, only: [:new, :create]
end
```

```app/controllers/admin/push_notifications_controller.rb
class Admin::PushNotificationsController < ApplicationController
  def new
    @demo_form = Admin::PushNotificationDemoForm.new
    render 'new'
  end

  def create
    @demo_form = Admin::PushNotificationDemoForm.new(demo_form_params)

    if @demo_form.save
      flash[:notice] = "ユーザー#{@demo_form.user_id}のへプッシュ通知が送信されました。"
      redirect_to new_admin_push_notification_path
    else
      flash.now[:alert] = @demo_form.errors.full_messages.join(" / ")
      return render :new
    end
  end

  private
  def demo_form_params
    params.require(:admin_push_notification_demo_form).permit(:user_id, :title, :body, :url)
  end
end
```

```app/forms/push_notification_demo_form.rb
class Admin::PushNotificationDemoForm
  include ActiveModel::Model

  validates :user_id, presence: true
  validates :title, presence: true
  validate :is_valid_user_id_and_has_individual_job

  def save
    return false if invalid?

    VisitNotificationService.new.notify(
      user_job_id: @job.id,
      user_id: @user_id,
      push_notification_content: VisitNotificationService.build_push_notification(
        title: @title,
        body: @body,
        url: @url,
      )
    )
    return true
  end

  private

  def is_valid_user_id_and_has_individual_job
    user = User.find_by(id: @user_id)
    unless user
      errors.add(:user_id, 'が無効です。')
      return false
    end

    @job = user.individual_job
    unless @job
      errors.add(:base, "指定されたUser(#{@user_id})に紐づくIndividualJobが存在しません。")
      return false
    end

    true
  end
end
```

```app/views/admin/new.html.haml

%h1= "push通知のテスト送信"

= form_for(@demo_form, url: admin_push_notifications_path, local: true) do |f|
  .div
    = f.label :user_id, 'User ID (必須)'
    = f.number_field :user_id, autofocus: true, required: true
  .div
    = f.label :title, 'Title: (必須)'
    = f.text_area :title, autofocus: true, required: true
  .div
    = f.label :body, 'Body'
    = f.text_area :body, autofocus: true
  .div
    = f.label :url, 'URL'
    = f.text_field :url, autofocus: true
  .div
    = f.submit("送信")

```


- ご覧の通り、バリデーション(要はビジネスロジック)がformオブジェクトの中に押し込めれたために非常に綺麗に実装できます。

- 自分の今回の実装では、form_withで指定されるモデルとurlが異なっています。
- また、エラーメッセージを(active record同様に)formオブジェクトのインスタンス変数であるerrors内に収納できるのも便利です。


## 参考

- https://product-development.io/posts/rails-design-pattern-form-objects
- https://tech.medpeer.co.jp/entry/2017/05/09/070758