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は威力を発揮する武器になります。