Edited at

Form Object実装メモ

More than 3 years have passed since last update.

やり方はいろいろ考えられる

ここに示すのはうまく動作した一例

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?