モデルと関連のないフォーム、モデルに無いカラムのあるフォームを作ると、毎回「ストロングパラメータどうやって使うんだ?」。
「バリデーションをコントローラーに書くの気持ち悪い。」と感じてきました。
いままでは、params[:key]
でアクセスして個別に確認することをしていたけれど、扱い方をしっかり認識できました。
最終的には、Form Object を使用することが解決策になると認識できるまでのまとめです。
目次
参考
- 7.3.2 Strong Parameters
- strong_parameters in rails for multiple keys
- メドピア開発者ブログ - form object を使ってみよう
- Qiita - 【Rails】FormObject を使ってほしい
- Form Object という選択肢を検討してみる
- Rails: Form Object と
#to_model
を使ってバリデーションをモデルから分離する(翻訳)
確認 ストロングパラメータを使う
比較対象に、まずストロングパラメータを確認します。
苗字(last_name
)と名前(first_name
)を持つ User モデルへ登録することを想定します。
どちらも空でないことを条件にします。
edit 機能は省略します。
モデルの作成
app/models/user.rb
を作成します。
1 | class User < ApplicationRecord |
コントローラーの作成
app/controllers/users_controller.rb
を作成します。
1 | class UsersController < ApplicationController |
ビューの作成
app/views/users/index.html.erb
とapp/views/users/new.html.erb
を作成します。
1 | <ul> |
1 | <%= form_with model: @user,local: true do |f|%> |
ルーティングの作成
1 | Rails.application.routes.draw do |
動作確認
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
がコンソールに表示されます。
「制限されたもの以外のパラメータがあるぞ」ということですね。
params
とuser_params
は、どちらもActionController::Parameters
クラスです。
判断の区分としては、permitted: true
が付与されているかが違いになりそうです。
1 | params.inspest の結果 |
また、モデルを使えるときの利点として、.validate
メソッドでモデルに記述したバリデーションを実行できます。
個別にパラメータをチェックする実装をコントローラーを書かなくていいのはとても利点だと感じます。
一通り確認しましたが、フォームの入力にストロングパラメータとモデルが使えることで、2 つの利点が享受できると感じます。
- コントローラ側で制限したパラメータだけを受け取ることができる。
- モデルのバリデーションを利用できる。
パラメータの制限を試みる
params.require(:user).permit(:last_name, :first_name)
のようにすることで、制限ができるのは確認しました。
では、モデルが無いときどうするのか?require(:model)
ができないではないか!
対応方法としてparams.permit(:key1, :key2, ...)
とすることで制限できました。.require(:user)
を除きます。
具体的な実装は以下の通りです。
コントローラーの修正
app/controllers/users_controller.rb
を修正します。
1 | class UsersController < ApplicationController |
ビューの作成
app/views/users/new.html.erb
を修正します。
1 | <%= form_with url: users_path ,methods: :post, local: true do |f|%> |
モデルとは関連の無いフォームになりました。
比較確認
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">
name
とid
が異なっています。
試しに、モデルと関連のないフォームのまま、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 を使わないで実装してみます。
コントローラーの作成
1 | class SlackController < ApplicationController |
ビューの作成
1 | <p class="notice"><%= notice %></p> |
1 | <!--エラーメッセージ表示部分--> |
ルーティングの作成
1 | Rails.application.routes.draw do |
ここまでの実装で、入力された内容に基づいて slack への通知ができます。
バリデーションは、以下の部分です。
1 | return render :new if user_params[:message].blank? || !user_params[:mention].include?("@") |
今回は 2 変数に対して、それぞれ 1 つずつの制約しか無いので簡単です。
しかし、変数が増える。制約の内容が詳細かつ複雑になるとこのようにシンプルには済まないでしょう。
Form Object を導入する
Form Object を導入してみます。
参考にしたものを見ると、app
ディレクトリの下に、forms
ディレクトリを作ってその中にファイルを作る例が多いようなので、それに従います。
Form Object を作成
1 | class NotifierForm |
コントローラを修正
1 | class SlackController < ApplicationController |
ビューを修正
app/views/slack/new.html.erb
を以下のように修正します。
1 | <ul> |
比較
Form Object を使用することで、コントローラー特に create の処理が非常にシンプルになりました。
コントローラの中でバリデーションを書きたくない、書くのは気持ち悪い。という場合に非常に有効だと感じます。
コントローラで、用意した@form
をform_with
に渡すことで、生成された html の name 要素 はnotifier_form[message]
のようになります。
このことで ストロングパラメータの拘束に.require(:notifier_form)
が使用できるようになりました。
また、エラーが出た時の入力をフォームへ返せるようになりました。
エラーメッセージが Form Object の中にあるので、メッセージの表示方法もフラッシュメッセージから変更しています。
カラムごとどのような制約に引っかかったのか、見てわかりやすいのがイイです。
Form Object を間に噛ませてデータの保存をする
ここまで、データを保存しないフォームでの Form Object を使いましたが、もちろんデータの保存を行うもできます。
Slack への通知履歴をデータベースに保存してみます。
slack_notifier_logs
テーブルを作ってあるものとします。
モデルの作成
1 | class SlackNotifierLogs < ApplicationRecord |
Form Object を修正
1 | class NotifierForm |
コントローラの修正
to_model
をオーバーライドしたことで、コントローラの修正が必要になりました。
1 | class SlackController < ApplicationController |
to_model をオーバーライドしたことで、ビューで記述したform_with model:@form ~~~
によるパラメータの拘束名が変わります。
生成された html の name 要素 はnotifier_form[message]
からslack_notifier_log[message]
のようになります。
ストロングパラメータ側でも、.require(:slack_notifier_log)
に書き換える必要がありました。
参考にした記事から、オーバーライドしていますが「別の関数のほうが、いいのでは?」感じます。
実行すると、slack への通知と、データの保存がされるようになります。
Form Object を間に噛ませてデータの保存をする(別パターン)
一応、オーバーライドしない場合の Form Object とコントローラと、次のようになります。
1 | class NotifierForm |
1 | class SlackController < ApplicationController |
ビューでform_with
に、NotifierForm
のオブジェクトを渡して、notifier_form
で拘束されたパラメータを受け取るほうが素直だと感じました。
今回は、モデルがないときのストロングパラメータと Form Object を試しました。
結果として、コントローラに詳細なバリデーションを書くようなことを避けられるのが、非常に良いと感じました。
最終的なコードの見通しが良くなったと感じます。
また、最終的にデータ保存するときでもフォームへの入力の拘束の実装とモデル自体の実装が分離できること。
Form Object を介して複数モデルのバリデーションを行って、データの保存ができるのもいいなと感じました。
ではでは。