Railsを使って開発をしている時、1つのフォームで複数のモデルを操作したい場合やそのフォーム専用の処理が必要になることがあります。
Railsではフォームはモデルに依存しており、上記のようなケースが発生した場合通常のMVCだけでは処理が複雑になってしまいます。
そこで今回は、Form Objectを用いてモデルに依存しないフォームの書き方を解説していきたいと思います。

Form Objectを使うメリット

モデルとフォームの責務を切り分けられる事です。
Railsではフォームはモデルに依存しているため、通常のCRUD処理の実装は簡単に出来てしまいます。

しかし例えば、1つのフォームで親モデル・子モデル・孫モデルを操作するケースがあった場合、かなり複雑な処理をコントローラーに書くことになります。
また、ユーザーのログインログアウトなど、モデルの処理とは関係のない処理などもモデルに記述するケースも発生します。

そういった、特定のフォームでしか行わない処理を記述する場合、Form Objectはとても便利です。

ユースケース

  • 1つの記事に複数の画像を保存する処理(記事 has_many 画像 な関係)
  • ユーザーのログイン処理

今回はCarrierWaveを使って、1つの記事と複数(最大5枚)の画像を保存する処理を書いていきたいと思います。(CarrierWaveの設定については省略)
また、今回は記事1つにつき最低1枚は画像をつける必要があるとします。

実装

モデル

モデルは下記のようになります。

# app/models/post.rb

class Post < ApplicationRecord
  has_many :images

  validates_presence_of %i(
    title
    content
  )
end
# app/models/image.rb

class Image < ApplicationRecord
  mount_uploader :name, ImageUploader
  belongs_to :post

  validates_presence_of %i(
    name
  )
end

アップローダー

CarrierWaveのアップローダーを用意します。

# app/uploaders/image.rb

class DomUploader < CarrierWave::Uploader::Base
  if Rails.env.development? || Rails.env.test?
    storage :file
  else
    storage :fog
  end

  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  def extension_whitelist
    %w(jpg jpeg png)
  end

  def filename
    "#{secure_token}.#{file.extension}" if original_filename.present?
  end

  protected

  def secure_token
    var = :"@#{mounted_as}_secure_token"
    model.instance_variable_get(var) or model.instance_variable_set(var, SecureRandom.uuid)
  end
end

Gemfile

今回はデータの整形やキャストを行えるようにするために、Form Objectにvirtusというgemを導入します。

# Gemfile

....

gem 'virtus'

....

Form Objectの実装

それでは、Form Objectを実装していきます!
まずはappディレクトリ配下にformsディレクトリを作成し、そこに〇〇_form.rbという命名でファイルを作成します。

# app/forms/make_post_form.rb

class MakePostForm
  include Virtus.model
  include ActiveModel::Model # 通常のモデルのようにvalidationなどを使えるようにしたいのでActiveModel::Modelをinclude
  extend CarrierWave::Mount # モデル以外でCarrierWaveを使いたいときはこのModuleをextendする

  attribute :title, String
  attribute :content, String
  attribute :image1, String
  attribute :image2, String
  attribute :image3, String
  attribute :image4, String
  attribute :image5, String

  mount_uploader :image1, ImageUploader
  mount_uploader :image2, ImageUploader
  mount_uploader :image3, ImageUploader
  mount_uploader :image4, ImageUploader
  mount_uploader :image5, ImageUploader

  validates_presence_of %i(
    title
    content
    image1
  )

  def save_post!
    post = Post.new(title: title, content: content).save!
    post.images.build(name: image1).save!
    post.images.build(name: image2).save! if image2
    post.images.build(name: image3).save! if image3
    post.images.build(name: image4).save! if image4
    post.images.build(name: image5).save! if image5
    return post
  end
end

Controller & View

ここまで実装すれば、あとはController側で呼び出すだけです。

# app/controllers/posts_controller.rb

class PostsController < ApplicationController

  def new
    # フォームオブジェクトのインスタンス呼び出し
    @post_form = MakePostForm.new
  end
  
  def create
    @post_form = MakePostForm.new(post_params)
    if @post_form.save_post!
      redirect_to 'リダイレクト先'
    else
      render :new
    end
  end

  private

  def post_params
    params.require(:make_post_form).permit(
      :title, :content, :image1, :image2, :image3, :image4, :image5
    )
  end
end

Viewは下記のようになります。

# app/views/posts/new.html.slim

= form_with(model: @post_form, url: posts_path, local: true) do |form|
  = form.label :title
  = form.text_field :title
  = form.label :content
  = form.text_area :content
  = form.label :image1
  = form.file_field :image1
  = form.label :image2
  = form.file_field :image2
  = form.label :image3
  = form.file_field :image3
  = form.label :image4
  = form.file_field :image4
  = form.label :image5
  = form.file_field :image5
  = form.submit '保存する'

まとめ

いかがでしたでしょうか?かなり簡単にForm Objectを実装することが出来るのがお分り頂けたと思います。
今回の例では比較的簡単なパターンで解説したので、実際にはこの程度の処理はコントローラーに書くことが多いですが、実際の開発ではもっと複雑なフォームも出てきます。
そういった場合に、Form Objectは威力を発揮する武器になります。