【FormObject】使ってみた

##環境
Ruby 3.0.2
Rails 6.1.4.1

##状況

userとemployeeテーブルの作成と更新を一つの画面で行いたいとき、
accepts_nested_attributes_forは非推奨なのでFormObjectを使ってリファクタリングしてみた。

```app/models/employee.rb
class Employee < ApplicationRecord
...
  accepts_nested_attributes_for :user
...
end
```


##メリット
コントローラーがすっきりする


##EmployeeForm

```app/forms/employee_form.rb
class EmployeeForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :firstname, :string
  attribute :lastname, :string
  attribute :number, :integer
  attribute :email, :string
  attribute :role, :string
  attr_reader :employee

  with_options presence: true do
    validates :firstname
    validates :lastname
  end

  delegate :persisted?, to: :employee

  def initialize(attributes = nil, employee: Employee.new)
    @employee = employee
    attributes ||= default_attributes
    super(attributes)
  end

  def save
    return false if invalid?
    ActiveRecord::Base.transaction do
      employee.update!(attributes.except('role'))
      employee.user.update!(role: role) unless employee.user.nil?
      employee
    end
  rescue => e
    Rails.logger.error e.message
    false
  end

  def to_model
    employee
  end
  
  private

  def default_attributes
    {
      firstname: employee.firstname,
      lastname: employee.lastname,
      number: employee.number,
      email: employee.email,
      role: employee.user.try(:role)
    }
  end
end
```

- ```include ActiveModel::Model```→モデルと同じようにバリデーションを書ける
- ```include ActiveModel::Attributes```→クラスメソッドattributeに属性名と型を渡すと、attr_accessorと同じように属性が使えるようになる
- ```a ||= xxx``` : aが偽か未定義ならaにxxxを代入する
- ```#initialize```ではFormオブジェクトの値を初期化している。 ```#super```は```ActiveModel::Model```の```#initialize```を呼び出しており、書き込みメソッド（#firstname=など）を用いて値を代入している。（super は現在のメソッドがオーバーライドしているメソッドを呼び出す）
- ```#persisted?```と```#to_model```はビューの表示（#form_with）に必要なメソッド。```#persisted?```は作成・更新に応じてフォームのアクションをPOST・PATCHに切り替えてくれる。 また```#to_model```はアクションのURLを適切な場所に切り替えてくれる。
- ```employee.update!(attributes.except('role'))```で属性を一つ一つ指定しているやり方が多かったが、できるだけ少ないコードで書きたかったため無理やりexceptしている。



##Controller
```app/controllers/manage/employees_controller.rb
class Manage::EmployeesController < ApplicationController
  before_action :set_employee, only: [:edit, :update, :destroy]

  def index
    @employees = Employee.all
  end

  def new
    @employee_form = EmployeeForm.new
  end

  def create
    @employee_form = EmployeeForm.new(employee_form_params)
    if @employee_form.save
      redirect_to new_manage_employee_path, notice: '設定の追加が完了しました'
    else
      redirect_to new_manage_employee_path, alert: '設定の追加に失敗しました'
    end
  end

  def edit
    @employee_form = EmployeeForm.new(employee: @employee)
  end

  def update
    @employee_form = EmployeeForm.new(employee_form_params, employee: @employee)
    if @employee_form.save
      redirect_to edit_manage_employee_path(@employee), notice: '設定の更新が完了しました'
    else
      redirect_to edit_manage_employee_path(@employee), alert: '設定の更新に失敗しました'
    end
  end

  def destroy
  end

  private

  def set_employee
    @employee = Employee.find(params[:id])
  end

  def employee_form_params
    params.require(:employee).permit(
      :firstname, 
      :lastname, 
      :number, 
      :email, 
      :role
    )
  end
end
```

##View
```new.html.slim
= form_with model: @employee_form, url: manage_preferences_employee_path, local: true do |f|
  = render 'form', form: @employee_form, f: f
```

- new.html.slimとedit.html.slimから```@employee_form```をformとして渡す想定



##参考

https://applis.io/posts/rails-design-pattern-form-objects

https://qiita.com/kazutosato/items/91c5c989f98981d06cd4

https://docs.ruby-lang.org/ja/latest/doc/spec=2fcall.html#super

https://post-output.com/252-2/#toc9

https://qiita.com/tanutanu/items/5ff5f1e653d802929f0b