Form Object実装メモ

  • 19
    いいね
  • 0
    コメント

やり方はいろいろ考えられる
ここに示すのはうまく動作した一例
Railsとかのバージョンによっても書き方が変わる

7 Patterns to Refactor Fat ActiveRecord Models

ユースケース

例えばUserモデルのsaveのパターンがたくさんあり、それぞれに関連するcolumns, validationsなどの違いが多く複雑になった時に、関心を分けるために使える

  • パターン例
    • Signup
    • Sign In
    • Password Change
    • Update profile
    • Create users from importing
    • etc.
  • パターンが2つ程度でシンプルな時はvalidate :on/:ifなどで間に合う

@form = MyForm.new()があたかも@userであるかのようにform_forと一緒に使えるようにできる

実装

forms/my_form.rb
class MyForm
  include Virtus.model

  include ActiveModel::Model
  # 以下Rails3
  # include ActiveModel::Validations
  # include ActiveModel::Conversion
  # extend ActiveModel::Naming

  attr_reader :user

  attribute :email, String
  # Userに保存しないが、validationをかけたいattributeの例
  attribute :terms_of_use, Boolean

  # validations
  validates_acceptance_of :terms_of_use

  def initialize(user)
    @user = user
    self.attributes = @user.attributes
  end

  def persisted?
    false
  end

  # submit時のparamsのkeyに使われる -> params[:user] (無い場合params[:my_form])
  # form_forがURLを生成する時に使われる -> users_path (無い場合my_forms_pathなど)
  def self.model_name
    ActiveModel::Name.new(self, nil, "User")
  end

  # List attributes defined with virtus
  # controllerで @form.update(user_params) を呼ぶ時のpermitに使える
  #   user_params -> params.require(:user).permit(*MyForm.column_names)
  def self.column_names
    attribute_set.map(&:name)
  end

  def update(params)
    self.attributes = params.to_h # 実装によって変わる
    if valid?
      persist!
      true
    else
      false
    end
  end

  private

    def persist!
      @user.update!(self.attributes) # 実装によって変わる
      # userに含まれない一時的な値なら self.attributes.except(:terms_of_use) などとできる
      # 複数のmodelを扱うことも可能
    end
end

form + routes

routes.rb
resource :user, only: [:edit, :update]
edit.html.haml
= form_for @form, url: user_path, method: :put do |f|

I18n

ActiveRecord/ActiveModelの翻訳

activerecordのキーではうまく動作しなかった
activemodelにエイリアスする

en:
  activerecord: &activerecord
    errors:
      ...
    models:
      ...
    attributes:
      ...
  activemodel:
    <<: *activerecord

参考

RailsCasts Form Objects

関連gem
reform - Form objects decoupled from models
gemに依存するコストがかかる可能性がある

model_nameの部分
How should I handle edit and update action when I use Form Object?