おはようございます。今回は実務で実装したいなと思い勉強した、フォームオブジェクトについて簡単にまとめたいと思います。
フォームオブジェクトとは
Ruby on Railsのデザインパターン「設計手法」の1つで、フォームの責務(バリデーションや整形など)をカプセル化し、コントローラやモデルを肥大化させないために使うデザインパターンです。
フォームオブジェクトの導入
今回は単純にユーザを登録するというケースを例に実際に導入してみます。
まずは、ユーザ登録のロジックをモデルに実装するとどうなるのかを見ていきます。
class User < ApplicationRecord
has_secure_password # パスワードをハッシュ化して保存する機能が提供される。
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :password, format: { with: /\A[\p{ascii}&&[^\x20]]{8,72}\z/ }
validates: terms_of_service, acceptance: true
end
class UserRegistrationController < ApplicationController
def new
@user = User.new
end
def create
@user = User.new(user_params)
if @user.save
sign_in(@user)
redirect_to action: :completed
else
render :new
end
def completed: end
private
def user_params
params.require(:user).permit(:email, :password, :password_confirmation, :terms_of_service)
end
end
このようなケースでは、フォームオブジェクトを用いなくても問題ありません。しかし、次のような要件が出てきたことを考えると、フォームオブジェクトの導入が検討されます。
- ユーザ登録の後にメールを送信したい。
- 通常のユーザ登録の他に、管理者がCSVファイルからユーザを一斉に登録できるようにしたい。
- その場合、メールの内容を変更したい。
Userモデルのバリデーションやコールバックを特定の条件のみ実行するようにすることで、2つのユースケースのロジックをモデルやコントローラーに書くことで実装できますが、UserモデルやUsersコントローラーが肥大化していきます。こういったことを避けるためにフォームオブジェクトを導入します。
class UserRegistrationsController < ApplicationController
def new
@user_registration_form = UserRegistrationForm.new
end
def create
@user_registration_form = UserRegistrationForm.new(user_registration_form_params)
if @user_registration_form.save
# sign_in(@user_registration_form.user)
redirect_to action: :completed
else
render :new
end
end
def completed; end
private
def user_registration_form_params
params.require(:user_registration_form).permit(
:email,
:password,
:password_confirmation,
:terms_of_service
)
end
end
new
メソッドではUser
クラスのインスタンスではなく、UserRegistrationForm
クラスのインスタンスを作成しています。create
メソッドではsave
メソッドが呼ばれており、これはUserRegistrationForm
クラスでオーバーライドされたメソッドになります。ここで、バリデーションが実行され、問題なければデータベースに保存します。
では、UserRegistrationForm
クラスを詳しく見ていきます。
class UserRegistrationForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :email, :string
attribute :password, :string
attribute :password_confirmation, :string
validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP, allow_blank: true}
validate :email_is_not_taken_another
validates :password, format: { with: /\A[\p{ascii}&&[^\x20]]{8,72}\z/ },
confirmation: { allow_blank: false }
validates :terms_of_service, acceptance: true
def save
return false if invalid?
user.save
end
def user
@user ||= User.new(email: email, password: password)
end
private
def email_is_not_taken_another
errors.add(:email, :taken, value: email) if User.exists?(email: email)
end
end
詳しく見ていきます。
ActiveModel::Modelをインクルード
ActiveModel::Model
をインクルードすると以下のようなモジュールもまとめてミックスインすることとなります。
extend ActiveSupport::Concern
include ActiveModel::AttributeAssignment
include ActiveModel::Validations
include ActiveModel::Conversion
included do
extend ActiveModel::Naming
extend ActiveModel::Translation
end
https://github.com/rails/rails/blob/v5.2.2/activemodel/lib/active_model/model.rb#L60-L68
ActiveModel::AttributeAssignment
キーが属性名と一致するハッシュを渡すことで、属性を一括で設定することができる機能を提供します。
class Cat
include ActiveModel::AttributeAssignment
attr_accessor :name, :status
end
cat = Cat.new
cat.assign_attributes(name: "Gorby", status: "yawning")
cat.name # => 'Gorby'
cat.status # => 'yawning'
ActiveModel::Validations
バリデーションの機能を提供します。
ActiveModel::Attributesをインクルード
ActiveModel::Attributesをインクルードするとattr_accesorの代わりに属性を指定することができます。こちらを使うメリットとしては型を指定できることかなと思っています。また、デフォルト値の指定だったり、細かい設定を行うことができます。
これらモジュールをインクルードすることで属性の定義やバリデーションをつけることができ、データベースと直接紐付かないクラスを作成することができます。また、コールバックなどの処理も持たせることも可能です。
フォームに関するロジックをフォームオブジェクトに分けたので、Userクラスがすっきりしました。
class User < ApplicationRecord
has_secure_password
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :password, format: { with: /\A[\p{ascii}&&[^\x20]]{8,72}\z/ }
end
要件を思い出してみます。
- ユーザ登録の後にメールを送信したい。
- 通常のユーザ登録の他に、管理者がCSVファイルからユーザを一斉に登録できるようにしたい。
- その場合、メールの内容を変更したい。
メールを送信する機能はフォームオブジェクトに足せます。また、CSVファイルから一斉登録する機能はそれ用のフォームオブジェクトを作成することで、コードの見通しがよくなり、メールの内容を通常の登録と変えることもできます。
さいごに
ActiveModel::Model
を使えば簡単に実装することができました。実務のコードでモデルとコントローラーのボリュームがすごいことになっているので積極的に使っていきたいところです。
ここまで読んでいただきありがとうございました。