モデルが無いときのストロングパラメーターとバリデーション、解決策としての Form Object

モデルと関連のないフォーム、モデルに無いカラムのあるフォームを作ると、毎回「ストロングパラメータどうやって使うんだ?」。
「バリデーションをコントローラーに書くの気持ち悪い。」と感じてきました。
いままでは、params[:key]でアクセスして個別に確認することをしていたけれど、扱い方をしっかり認識できました。
最終的には、Form Object を使用することが解決策になると認識できるまでのまとめです。

目次

参考

確認 ストロングパラメータを使う

比較対象に、まずストロングパラメータを確認します。

苗字(last_name)と名前(first_name)を持つ User モデルへ登録することを想定します。
どちらも空でないことを条件にします。
edit 機能は省略します。

モデルの作成

app/models/user.rbを作成します。

app/models/user.rb
1
2
3
4
5
6
7
8
class User < ApplicationRecord
validates :last_name, presence: true
validates :first_name, presence: true

def full_name
"#{self.last_name} #{self.first_name}"
end
end

コントローラーの作成

app/controllers/users_controller.rbを作成します。

app/controllers/users_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class UsersController < ApplicationController
def index
@users = User.all
end

def new
@user = User.new
end

def create
#@user = User.new(params)
@user = User.new(user_params)

# モデルのバリデーションは、関数で実行できる
puts @user.validate

if @user.save
redirect_to action: :index
else
render :new
end
end

private
def user_params
params.require(:user).permit(:last_name, :first_name)
end
end

ビューの作成

app/views/users/index.html.erbapp/views/users/new.html.erbを作成します。

app/views/users/index.html.erb
1
2
3
4
5
6
7
8
9
10
<ul>
<% if @users.present? %>
<% @users.each do | user |%>
<li>
<%= user.full_name %>
</li>
<% end %>
<% end %>
</ul>
<%= link_to "追加", user_new_path %>
app/views/users/new.html.erb
1
2
3
4
5
<%= form_with model: @user,local: true  do |f|%>
<%= f.text_field :last_name %>
<%= f.text_field :first_name %>
<%= f.submit :submit %>
<% end %>

ルーティングの作成

config/routes.rb
1
2
3
4
5
Rails.application.routes.draw do
get 'users',to: "users#index"
get 'user/new',to: "users#new"
post 'users',to: "users#create"
end

動作確認

rails を起動して/usersにアクセスすれば、簡単な入力画面が現れます。
user.rbで、validates :last_name, presence: trueのようにpresence: trueを与えることで、空白の入力を防いでいます。

@user = User.new(user_params)@user = User.new(params)と記述します。
この場合、ActiveModel::ForbiddenAttributesErrorというエラーになります。
「ストロングパラメータをつかいなさい」というわけです。

params.require(:user).permit(:last_name, :first_name)に記述していないパラメータがあった時、user_paramsでは除かれています。
ビューに<%= f.text_field :middle_name %>を記述します。
そのうえでuser_params[:middle_name]を取得すると、Unpermitted parameter: :middle_nameがコンソールに表示されます。
「制限されたもの以外のパラメータがあるぞ」ということですね。

paramsuser_paramsは、どちらもActionController::Parametersクラスです。
判断の区分としては、permitted: trueが付与されているかが違いになりそうです。

1
2
3
4
5
params.inspest の結果
<ActionController::Parameters {"utf8"=>"?", "authenticity_token"=>"nK1vuzoWBldoYai4ibksEnXw1w0JusTswBMfF2AfLwuDk9tzIz36mOGPgalPRdtZqnC9duTAI1C8UsL6WU3Qaw==", "user"=>{"last_name"=>"", "first_name"=>""}, "commit"=>"submit", "controller"=>"users", "action"=>"create"} permitted: false>

user_params.inspest の結果
<ActionController::Parameters {"last_name"=>"", "first_name"=>""} permitted: true>

また、モデルを使えるときの利点として、.validateメソッドでモデルに記述したバリデーションを実行できます。
個別にパラメータをチェックする実装をコントローラーを書かなくていいのはとても利点だと感じます。

一通り確認しましたが、フォームの入力にストロングパラメータとモデルが使えることで、2 つの利点が享受できると感じます。

  • コントローラ側で制限したパラメータだけを受け取ることができる。
  • モデルのバリデーションを利用できる。

パラメータの制限を試みる

params.require(:user).permit(:last_name, :first_name)のようにすることで、制限ができるのは確認しました。
では、モデルが無いときどうするのか?
require(:model)ができないではないか!

対応方法としてparams.permit(:key1, :key2, ...)とすることで制限できました。
.require(:user)を除きます。

具体的な実装は以下の通りです。

コントローラーの修正

app/controllers/users_controller.rbを修正します。

app/controllers/users_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class UsersController < ApplicationController
def index
@users = User.all
end

def new
@user = User.new
end

def create
@user = User.new(user_params)

# モデルのバリデーションは、関数で実行できる
puts @user.validate

if @user.save
redirect_to action: :index
else
render :new
end
end

private
def user_params
params.permit(:last_name, :first_name)
end
end

ビューの作成

app/views/users/new.html.erbを修正します。

app/views/users/new.html.erb
1
2
3
4
5
6
<%= form_with url: users_path ,methods: :post, local: true  do |f|%>
<%= f.text_field :last_name %>
<%= f.text_field :first_name %>
<%= f.text_field :middle_name %>
<%= f.submit :submit %>
<% end %>

モデルとは関連の無いフォームになりました。

比較確認

rails を起動して/user/newにアクセスすれば、入力画面が現れます。

見た目には差はありませんが、ここでこれまで違うのはレンダリングされた html です。

例えば、last_name を比較します。

  • モデルを与えたフォームの場合
    <input type="text" name="user[last_name]" id="user_last_name">
  • モデルを与えないフォームの場合
    <input type="text" name="last_name" id="last_name">

nameidが異なっています。

試しに、モデルと関連のないフォームのまま、params.require(:user).permit(:last_name, :first_name)にて、パラメータをチェックします。
すると、param is missing or the value is empty: userというエラーになります。

逆に、モデルと関連づけたフォームで、params.permit(:last_name, :first_name)を行います。
Unpermitted parameters: :utf8, :authenticity_token, :user, :commitとなり、こちらもエラーとなります。

ここでわかるのは、以下の 2 点です。

  • モデルと関連のあるフォームは、**params.require(:model).permit(:key1, :key2)**を使用する。
  • モデルと関連の無いフォームは、**params.permit(:key1, :key2)**を使用する。

実はこの点をあいまいにしたままだったので、はっきりして良かったです。
(この記事書きながら、認識誤ってたことを確認できました。)

パラメーターの制限はできましたが、バリデーションがモデルに依存しています。
次は、モデルとの依存を切ってみます。

Form Object を使う

Form の入力をデータベースを保存するのではなく、入力内容を Slack に通知する場合を考えてみます。
「モデル無いじゃん。どうするんだ?」ということが発生します。
コントローラーでバリデーションを書くこともありました。
Form Object を使うことで、そういったことを防ぐことができます。

今回は、メッセージとメンション先を指定する@~~~の 2 つを入力するフォームを用意してみます。

バリデーションは、以下の 2 つを用意します。

  • メッセージは、空白禁止
  • メンション先指定は@の入力を指定する

比較対象 Form Object を使わない

一旦比較対象として、Form Object を使わないで実装してみます。

コントローラーの作成

app/controllers/slack_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class SlackController < ApplicationController
def create

# バリデーションチェック
if user_params[:message].blank? || !user_params[:mention].include?("@")
flash[:alert] = "入力にエラーがあります。" # 本来はどのパラメータがエラーなのかなど詳細な実装はできるが省略
return render :new
end

# slack通知処理
notifier = Slack::Notifier.new('Webhook URL')
notifier.ping("<#{user_params[:mention]}> #{user_params[:message]}")

flash[:notice] = "通知を送りました"

redirect_to action: :index
end

private
def user_params
params.permit(:message, :mention)
end
end

ビューの作成

app/views/slack/index.html.erb
1
2
<p class="notice"><%= notice %></p>
<%= link_to "Slack通知", slack_new_path %>
app/views/slack/new.html.erb
1
2
3
4
5
6
7
8
<!--エラーメッセージ表示部分-->
<p class="notice"><%= alert %></p>

<%= form_with url: slack_path ,methods: :post, local: true do |f|%>
<%= f.text_field :message %>
<%= f.text_field :mention %>
<%= f.submit :submit %>
<% end %>

ルーティングの作成

config/routes.rb
1
2
3
4
5
Rails.application.routes.draw do
get 'slack',to: "slack#index"
get 'slack/new',to: "slack#new"
post 'slack',to: "slack#create"
end

ここまでの実装で、入力された内容に基づいて slack への通知ができます。
バリデーションは、以下の部分です。

1
return render :new if user_params[:message].blank? || !user_params[:mention].include?("@")

今回は 2 変数に対して、それぞれ 1 つずつの制約しか無いので簡単です。
しかし、変数が増える。制約の内容が詳細かつ複雑になるとこのようにシンプルには済まないでしょう。

Form Object を導入する

Form Object を導入してみます。
参考にしたものを見ると、appディレクトリの下に、formsディレクトリを作ってその中にファイルを作る例が多いようなので、それに従います。

Form Object を作成

app/forms/notifier_form.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class NotifierForm
# Form Objectを使うにあたってActiveModel::Modelを読み込むこと
include ActiveModel::Model

attr_accessor :message, :mention

#入力の空文字を禁止する
validates :message, presence:true
#'@'を含むように制限する
validate :include_atmark

def include_atmark
unless self.mention.include? '@'
errors.add(:mention, "@を含めてください。")
end
end

def save
return false if invalid?

# slack通知処理
notifier = Slack::Notifier.new('Webhook URL')
notifier.ping("<#{mention}> #{message}")
true
end
end

コントローラを修正

app/controllers/slack_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class SlackController < ApplicationController

def new
@form = NotifierForm.new()
end

def create
@form = NotifierForm.new(user_params)

if @form.save
flash[:notice] = "通知を送りました"
redirect_to action: :index
else
return render :new
end
end

private
def user_params
params.require(:notifier_form).permit(:message, :mention)
end
end

ビューを修正

app/views/slack/new.html.erbを以下のように修正します。

app/views/slack/new.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<ul>
<!--エラーメッセージ表示部分-->
<% @form.errors.full_messages.each do |msg| %>
<li>
<%= msg %>
</li>
<% end %>
</ul>

<%= form_with model: @form , url: slack_path ,methods: :post, local: true do |f|%>
<%= f.text_field :message %>
<%= f.text_field :mention %>
<%= f.submit :submit %>
<% end %>

比較

Form Object を使用することで、コントローラー特に create の処理が非常にシンプルになりました。
コントローラの中でバリデーションを書きたくない、書くのは気持ち悪い。という場合に非常に有効だと感じます。

コントローラで、用意した@formform_withに渡すことで、生成された html の name 要素 はnotifier_form[message]のようになります。
このことで ストロングパラメータの拘束に.require(:notifier_form)が使用できるようになりました。

また、エラーが出た時の入力をフォームへ返せるようになりました。
エラーメッセージが Form Object の中にあるので、メッセージの表示方法もフラッシュメッセージから変更しています。
カラムごとどのような制約に引っかかったのか、見てわかりやすいのがイイです。

Form Object を間に噛ませてデータの保存をする

ここまで、データを保存しないフォームでの Form Object を使いましたが、もちろんデータの保存を行うもできます。

Slack への通知履歴をデータベースに保存してみます。

slack_notifier_logsテーブルを作ってあるものとします。

モデルの作成

app/models/slack_notifier_log.rb
1
2
class SlackNotifierLogs < ApplicationRecord
end

Form Object を修正

app/forms/notifier_form.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class NotifierForm
# Form Objectを使うにあたってActiveModel::Modelを読み込むこと
include ActiveModel::Model

attr_accessor :message, :mention

#入力の空文字を禁止する
validates :message, presence:true
#'@'を含むように制限する
validate :include_atmark

def include_atmark
unless self.mention.include? '@'
errors.add(:mention, "「@」を含めてください。")
end
end

# パラメータからSlackNotifierLogモデルを作る(オーバーライド)
def to_model
SlackNotifierLog.new(message: message, mention: mention)
end

def save
return false if invalid?

# slack通知処理
notifier = Slack::Notifier.new('Webhook URL')
notifier.ping("<#{mention}> #{message}")

# データベースへの保存
to_model.save

true
end
end

コントローラの修正

to_modelをオーバーライドしたことで、コントローラの修正が必要になりました。

app/controllers/slack_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class SlackController < ApplicationController

def new
@form = NotifierForm.new()
end

def create
@form = NotifierForm.new(user_params)

if @form.save
flash[:notice] = "通知を送りました"
redirect_to action: :index
else
return render :new
end
end

private
def user_params
params.require(:slack_notifier_log).permit(:message, :mention)
end
end

to_model をオーバーライドしたことで、ビューで記述したform_with model:@form ~~~によるパラメータの拘束名が変わります。
生成された html の name 要素 はnotifier_form[message]からslack_notifier_log[message]のようになります。
ストロングパラメータ側でも、.require(:slack_notifier_log)に書き換える必要がありました。

参考にした記事から、オーバーライドしていますが「別の関数のほうが、いいのでは?」感じます。

実行すると、slack への通知と、データの保存がされるようになります。

Form Object を間に噛ませてデータの保存をする(別パターン)

一応、オーバーライドしない場合の Form Object とコントローラと、次のようになります。

app/forms/notifier_form.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class NotifierForm
# Form Objectを使うにあたってActiveModel::Modelを読み込むこと
include ActiveModel::Model

attr_accessor :message, :mention

#入力の空文字を禁止する
validates :message, presence:true
#'@'を含むように制限する
validate :include_atmark

def include_atmark
unless self.mention.include? '@'
errors.add(:mention, "「@」を含めてください。")
end
end

# フォームの入力をモデルに渡す。
def set_model
SlackNotifierLog.new(message: message, mention: mention)
end

def save
return false if invalid?

# slack通知処理
notifier = Slack::Notifier.new('Webhook URL')
notifier.ping("<#{mention}> #{message}")

set_model.save

true
end
end
app/controllers/slack_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class SlackController < ApplicationController

def new
@form = NotifierForm.new()
end

def create
@form = NotifierForm.new(user_params)

if @form.save
flash[:notice] = "通知を送りました"
redirect_to action: :index
else
return render :new
end
end

private
def user_params
params.require(:notifier_form).permit(:message, :mention)
end
end

ビューでform_withに、NotifierFormのオブジェクトを渡して、notifier_formで拘束されたパラメータを受け取るほうが素直だと感じました。


今回は、モデルがないときのストロングパラメータと Form Object を試しました。
結果として、コントローラに詳細なバリデーションを書くようなことを避けられるのが、非常に良いと感じました。
最終的なコードの見通しが良くなったと感じます。

また、最終的にデータ保存するときでもフォームへの入力の拘束の実装とモデル自体の実装が分離できること。
Form Object を介して複数モデルのバリデーションを行って、データの保存ができるのもいいなと感じました。

ではでは。