前書き
-
モノリシックな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内に収納できるのも便利です。