やり方はいろいろ考えられる
ここに示すのはうまく動作した一例
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
にエイリアスする
en:
activerecord: &activerecord
errors:
...
models:
...
attributes:
...
activemodel:
<<: *activerecord
参考
関連gem
reform - Form objects decoupled from models
gemに依存するコストがかかる可能性がある
model_name
の部分
How should I handle edit and update action when I use Form Object?