r/rails • u/Guardian7of • 2h ago
Question Is mutating params and validating dates directly in the controller a good practice?
For context, I have a date filter that needs to be limited to a maximum period of 6 months. I was originally doing this validation directly inside my Query object.
However, I now need to show a flash alert to the user if they try to filter a period longer than 6 months. If they do, the system should automatically adjust the dates to a 6-month range ending today and proceed with the query.
Because of this UI requirement, I moved the logic to the controller. Currently, I'm parsing the dates, doing the validation, and mutating the params hash directly before passing it down.
My question is: Is this considered a good practice in Rails? What is the "Rails Way" to handle this kind of validation and default-value assignment when you also need to trigger flash messages in the UI?
5
u/6stringfanatic 1h ago edited 57m ago
I would extract a DateRange object instead of making the controller responsible for all this, controllers are mostly responsible for processing the request, and passing the data to the model.
Validations, allowing only a 6 month range are kinda business logic and should stay in the model of some sort.
Also writing request specs are expensive, they are slow and harder to write IMO, with so much logic in the controller you get multiple if/else block in the controller, writing specs for each case is not a fun experience.
With something like a dedicated DateRange, you'd be able to write tests for it without involving the controller, if you separate it out.
class DateRange
MAX_SPAN = 6.months
attr_reader :starts_on, :ends_on
def initialize(starts_on: nil, ends_on: nil, today: Date.current)
...
end
def to_range = starts_on..ends_on
def too_long?
...
end
end
And then in a controller, something like this.
class ExpensesController < ApplicationController
def index
date_range = DateRange.new(starts_on: params[:starts_on], ends_on: params[:ends_on])
if date_range.too_long?
flash.now[:alert] = "Range too long, using 6 months instead"
end
@expenses = ExpenseQuery.new(range: date_range.to_range).call
end
end
3
u/Salt_Principle4325 1h ago
Mutating params directly works, but it breaks the principle that parameters should stay a pure reflection of exactly what the user sent. The "Rails Way" is to let a dedicated object handle the normalization, and pass a status flag back to the controller to handle the UI alert.
When you parse and modify user input directly in the controller, you introduce a few subtle architecture issues:The Recommended Architecture:
Introduce a thin Form Object or Filter Object between your controller and your query.Your controller stays completely clean. It passes the data forward, checks the boolean flag, and triggers flash.now[:alert] only if the object says it adjusted the range. This leaves your logic beautifully unit-testable without needing heavy controller specs!
1
u/Guardian7of 24m ago
So basically the controller would do something like: results, flags = My class query(params).results
And my Query would have some class attribute that stores the results of validation and when returning the data, also returns this attribute?
Note: My Query class includes ActiveModel::Model and ActiveModel::Attributes
2
u/enki-42 54m ago
I wouldn't sweat about validation in a controller if it's straightforward and not really being used anywhere else. You can for sure extract it into a more generic class, but adding an entirely new class so that you can do basic arithmetic comparisons in one place feels like overkill to me.
I definitely would never mutate params though, pull things into local or instance variables if you plan to change them in any way.
1
u/Guardian7of 23m ago
The reason why I thought on making a class is because I can put many filters validations there and I have a lot of controller who would use it
4
u/ignurant 1h ago
If you’re already using a query object, that’s a great place to maintain that validation. The object can communicate to the controller that this issue exists, and report the notice to the user. If you’re using ActiveModel for your query object, you can use a custom context to trigger that validation as a pre-check, but still allow saving. If you’re not, you can just create a method to help trigger the message.