Formオブジェクト使ってますか?
Formオブジェクトを使うケースとしては
- パラメータに前処理やデフォルト値を入れたいとき
- コンテクストに応じたバリデーションを書きたいとき
- モデルに紐付かないパラメータのバリデーションしたいとき
- 複数のモデルに関連したパラメータをバリデーションしたいとき
などがあるかなと思います。
Formオブジェクト作る際にActiveModel::ModelやVirtusを使う例が多い印象ですが、最近はReformに落ち着きました。
理由としては
- Modelのバリデーションを共通で使うことができつつ、コンテクストに応じてバリデーションをFormオブジェクトに書ける
- 複数のModelのバリデーションを共通のerrorsによしなに統合してくれる
- viewのform_for(form_with)もmodelに紐付いて表示してくれる
- プロパティにhookする関数があり、デフォルト値や前処理が可能
反面、reform固有の実装でハマることも多くそこが懸念で導入しない人も多いのではないかと思います。
基本的な使い方
まずは基本的なreformのパターン。ユーザーのステータスがノーマルかプレミアムでバリデーションが別れるパターンを検討します。 ちなみにバリデーション自体は適当で、ノーマルの場合は140文字しかテキストを入力できないが、プレミアムの場合は1000文字入力できるとかにします。
controller
ユーザーのステータスがノーマルかプレミアムで呼び出すusecase(サービス層)を変えて実行する。
エラーがあればnewを再度呼び出し、formでエラー表示する。
class HogesController < ApplicationController
before_action :authenticate_user!
def new
@form = new_form
end
def create
@form = create_usecase.execute
if @form.errors.empty?
render :done
else
render :new
end
end
private
def new_form
if current_user.normal?
return Hoges::CreateFromNormalForm.new(Hoge.new)
elsif current_user.premium?
return Hoges::CreateFromPremiumForm.new(Hoge.new)
end
raise "user status is invalid"
end
def create_usecase
if current_user.normal?
return Hoges::CreateFromNormalUsecase.new(current_user, hoge_params)
elsif current_user.premium?
return Hoges::CreateFromPremiumUsecase.new(current_user, hoge_params)
end
raise "user status is invalid"
end
def hoge_params
params
.require(:hoge)
.permit(:text)
end
end
Usecase(サービス層)
paramと関連モデルを受け取り、formを返すのが通常パターン。
バリデーションチェックしてfalseならそのままformを返却。trueならmodelをsave!してデータベースエラーになるならそのままraiseする(formで全てのチェックを完了させておく)
こういったUsecaseの場合は、baseクラス作るのが良いかと思います。
module Hoges
class CreateFromPremiumUsecase
attr_reader :form, :params
def initialize(current_user, param)
@param = param.merge(from_user_id: current_user.id)
@form = CreateFromPremiumForm.new(Hoge.new)
end
def execute
return form unless form.validate(params)
ActiveRecord::Base.transaction do
form.sync
form.model.save!
end
form
end
end
end
module Hoges
class CreateFromNormalUsecase
attr_reader :form, :params
def initialize(current_user, param)
@param = param.merge(user: current_user)
@form = CreateFromNormalForm.new(Hoge.new)
end
def execute
return form unless form.validate(params)
ActiveRecord::Base.transaction do
form.sync
form.model.save!
end
form
end
end
end
Model
Modelのバリデーションはデータベース制約(データ型やuniqueness、not null、enumチェックなど)やどのコンテクストでも必ずチェックするもの(ひらがなしか受け付けないやメールアドレスの型など)を書き、reformはmodelのバリデーションに移譲できるようにします。
class Hoge < ApplicationRecord
belongs_to :user
validates :user, presence: true
validates :text, length: { in: 1..65535 } # mysqlのtext型のmaxとする
end
Form
copy_validations_fromでpropertyが定義してあるModelのバリデーションをコピーしてエラーがあればまとめてerrorsに突っ込まれる。
今回はuserがあるか必ずチェックされる。
(dry-validationの方が推奨だが、copy_validations_fromが使いたいのでActiveModelタイプにしている)
module Hoges
class CreateFromPremiumForm < Reform::Form
include Reform::Form::ActiveModel
model :hoge
property :user
property :text
validates :text, length: { in: 1..1000 }
extend ActiveModel::ModelValidations
copy_validations_from Hoge
end
end
module Hoges
class CreateFromNormalForm < Reform::Form
include Reform::Form::ActiveModel
model :hoge
property :user
property :text
validates :text, length: { in: 1..140 }
extend ActiveModel::ModelValidations
copy_validations_from Hoge
end
end
以上が基本パターン
has_manyで複数モデルを作成するパターン
accepts_nested_attributes_forで作ることも多いかと思いますが、それの代替手段となります。
Usecase
param = {
name: "name",
hoge_has_manies: [
{ description: "description1" },
{ description: "description2" }
]
}
module Hoges
class CreateHasManyUsecase
attr_reader :form, :params
def initialize(current_user, param)
@param = param.merge(from_user_id: current_user.id)
@form = CreateHasManyForm.new(Hoge.new)
end
def execute
return form unless form.validate(params)
ActiveRecord::Base.transaction do
form.sync
form.model.save!
form.hoge_has_manies.each do |hoge_has_many|
hoge_has_many.sync
hoge_has_many.model.hoge = form.model
hoge_has_many.model.save!
end
end
form
end
end
end
Model
class HogeHasMany < ApplicationRecord
belongs_to :hoge
validates :hoge, presence: true
validates :description, length: { in: 1..65535 } # mysqlのtext型のmaxとする
end
Form
module Hoges
class CreateHasManyForm < Reform::Form
include Reform::Form::ActiveModel
model :hoge
property :user
property :text
collection :hoge_has_manies
form: HogeHasManies::CreateForm,
populate_if_empty: HogeHasMany,
default: [],
virtual: true,
save: false
extend ActiveModel::ModelValidations
copy_validations_from Hoge
end
end
module Hoges
module HogeHasManies
class CreateForm < Reform::Form
include Reform::Form::ActiveModel
model :hoge_has_many
property :description
extend ActiveModel::ModelValidations
copy_validations_from HogeHasMany
end
end
end
has_oneで複数モデルを作成するパターン
Usecase
param = {
name: "name",
hoge_has_one: { description: "description1" }
}
module Hoges
class CreateHasOneUsecase
attr_reader :form, :params
def initialize(current_user, param)
@param = param.merge(from_user_id: current_user.id)
@form = CreateHasOneForm.new(Hoge.new)
end
def execute
return form unless form.validate(params)
ActiveRecord::Base.transaction do
form.sync
form.model.save!
form.hoge_has_one.sync
form.hoge_has_one.hoge = form.model
form.hoge_has_one.save!
end
form
end
end
end
Model
class HogeHasOne < ApplicationRecord
belongs_to :hoge
validates :hoge, presence: true
validates :description, length: { in: 1..65535 } # mysqlのtext型のmaxとする
end
Form
module Hoges
class CreateHasOneForm < Reform::Form
include Reform::Form::ActiveModel
model :hoge
property :user
property :text
collection :hoge_has_one
form: HogeHasOnes::CreateForm,
populate_if_empty: HogeHasOne,
virtual: true,
save: false
extend ActiveModel::ModelValidations
copy_validations_from Hoge
end
end
module Hoges
module HogeHasOnes
class CreateForm < Reform::Form
include Reform::Form::ActiveModel
model :hoge_has_one
property :description
extend ActiveModel::ModelValidations
copy_validations_from HogeHasOne
end
end
end
リレーションのない複数モデルの更新
Model
class Fuga < ApplicationRecord
validates :image, presence: true
end
module HogeFuga
class CreateForm < Reform::Form
include Reform::Form::ActiveModel
property :user, on: :hoge
property :text, on: :hoge
property :image, on: :fuga
extend ActiveModel::ModelValidations
copy_validations_from hoge: Hoge, fuga: Fuga
end
end
#その他Tips
defaultはキャッシュされるっぽいのでTime.currentなど動的な値入れると現在時刻にならない
module Hoges
class CreateForm < Reform::Form
include Reform::Form::ActiveModel
property :user
property :text, default: Time.current # NG
extend ActiveModel::ModelValidations
copy_validations_from Hoge
end
end