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 というのがしっくりこないし
よりよい書き方があると思うのですが、今時点での結論はこんな感じです!