Railsのparamsの入力値検証・文字列加工方法について考えてみたお話
Railsアプリケーションでよくあるparamsの処理。
save前はModelの中でvalid?すればよいですが、オブジェクトに入れる前に入力値検証や文字列の加工をしたかったり、
そもそも検索画面など、保存しないけど値を加工したりとか、よくあるパターンだと思います。
流れでControllerの中に書いていましたが、違うなあ…ともやもやしていました。
class BookingsController < ApplicationController def create @booking = current_user.bookings.new(processed_params) if @booking.save # 成功処理 else # 失敗処理 end end private def processed_params attrs = booking_params.to_h attrs[:room_id] = attrs[:room_id].to_i # date_selectを使ったとする booking_dates = ['booking_date(1i)', 'booking_date(2i)', 'booking_date(3i)'].map{|key| attrs[key].to_i} attrs[:booking_date] = Date.new(*booking_dates) rescue nil attrs end def booking_params params.require(:bookings).permit(:booking_date, :room_id) end end
単純にModel側に移せばいいかというと、さすがに微妙…。
class Booking < ApplicationRecord def initialize(data) data = process_data(data.to_h) super end private def process_data(params) # 処理は先程と同様 end end class BookingsController < ApplicationController def create @bookiing = current_user.bookings.new(booking_params) if @booking.save # 成功処理 else # 失敗処理 end end (略) end
Concernに移すのはかなりありな気がする。
ただ、Concernって共通化が目的な印象なので、繰り返し使うとはいえひとつずつのModelに対してこれを書くのも違和感を感じる。
module BookingParameter extend ActiveSupport::Concern private def processed_data(params) # 処理は先程と同様 end end class BookingsController < ApplicationController include BookingParameter def create @bookiing = current_user.bookings.new(processed_data(booking_params)) if @booking.save # 成功処理 else # 失敗処理 end end (略) end
神速さんが「個人的なリファクタリング原則で『引数が1つのメソッドは、その引数のインスタンスメソッドに書き換えられる』がある」とおっしゃっていたけれど、HashやActionContrller::Parametersにモンキーパッチをあてるのはいくらなんでも意味が違うと思う。
class Hash def processed_booking_data # 処理は先程と同様 end end
何がいいんだろう…!と唸りながら調べていたら「引数オブジェクト」という単語が目に飛び込んできて、
そうだjokerさんが前におっしゃってた…!と思ったら、まさしく回答者がjokerさんでした。
rails で params に対して複雑な処理をするときのベストプラクティスは? - QA@IT
こんな感じ…?
class BookingParameter attr_reader :booking_date, :room_id def initialize(attrs = {}) attrs.assert_valid_keys('day(1i)', 'day(2i)', 'day(3i)', 'room_id') booking_dates = ['booking_date(1i)', 'booking_date(2i)', 'booking_date(3i)'].map{|key| attrs[key].to_i} @booking_date = Date.new(*booking_dates) rescue nil
@room_id = attrs['room_id'].to_i freeze end def to_h {booking_date: @booking_date, room_id: @room_id} end end class BookingsController < ApplicationController def create @booking = current_user.bookings.new(BookingParameter.new(booking_params.to_h).to_h) if @booking.save # 成功処理 else # 失敗処理 end end (略) end
BookingParameter.new(booking_params.to_h).to_h というのがしっくりこないし
よりよい書き方があると思うのですが、今時点での結論はこんな感じです!