[Rails][Reform]Formオブジェクト使い方まとめ

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でエラー表示する。

```ruby:controllers/hoges_controller.rb
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クラス作るのが良いかと思います。

```ruby:usecases/hoges/create_from_premium_usecase.rb
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
```

```ruby:usecases/hoges/create_from_normal_usecase.rb
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のバリデーションに移譲できるようにします。

```ruby:models/hoge.rb
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タイプにしている)

```ruby:forms/hoges/create_from_premium_form.rb
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
```

```ruby:forms/hoges/create_from_normal_form.rb
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
```ruby
param = {
  name: "name",
  hoge_has_manies: [
    { description: "description1" },
    { description: "description2" }
  ]
}
```

```ruby:usecases/hoges/create_has_many_usecase.rb
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

```ruby:models/hoge_has_many.rb
class HogeHasMany < ApplicationRecord
  belongs_to :hoge
  
  validates :hoge, presence: true
  validates :description, length: { in: 1..65535 } # mysqlのtext型のmaxとする
end
```

## Form

```ruby:forms/hoges/create_has_many_form.rb
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
```

```ruby:forms/hoges/hoge_has_manies/create_form.rb
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
```ruby
param = {
  name: "name",
  hoge_has_one: { description: "description1" }
}
```

```ruby:usecases/hoges/create_has_one_usecase.rb
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

```ruby:models/hoge_has_one.rb
class HogeHasOne < ApplicationRecord
  belongs_to :hoge
  
  validates :hoge, presence: true
  validates :description, length: { in: 1..65535 } # mysqlのtext型のmaxとする
end
```

## Form

```ruby:forms/hoges/create_has_one_form.rb
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
```

```ruby:forms/hoges/hoge_has_ones/create_form.rb
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

```ruby:models/fuga.rb
class Fuga < ApplicationRecord
  validates :image, presence: true
end
```

```ruby:forms/hoge_fuga/create_form.rb
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など動的な値入れると現在時刻にならない

```ruby:forms/hoges/create_form.rb
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
```