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