Ruby on Rails

【Rails】フォームオブジェクトの実装方法【超基本】

おはようございます。今回は実務で実装したいなと思い勉強した、フォームオブジェクトについて簡単にまとめたいと思います。

フォームオブジェクトとは

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を使えば簡単に実装することができました。実務のコードでモデルとコントローラーのボリュームがすごいことになっているので積極的に使っていきたいところです。
ここまで読んでいただきありがとうございました。

ABOUT ME
sakai
東京在住の30歳。元々は車部品メーカーで働いていてましたが、プログラミングに興味を持ちスクールに通ってエンジニアになりました。 そこからベンチャー → メガベンチャー → 個人事業主になりました。 最近は生成 AI 関連の業務を中心にやっています。 ヒカルチャンネル(Youtube)とワンピースが大好きです!