None of the above.
I would say that the cleanest way is to neither burdon your controller or User model further. Instead create a separate object which can be tested in isolation.
# Virtual model which represents a search query.
class UserQuery
include ActiveModel::Model
include ActiveModel::Attributes
attribute :first_name
attribute :last_name
attribute :email
attribute :project_name
# Loops through the attributes of the object and contructs a query
# will call 'filter_by_attribute_name' if present.
# @param [ActiveRecord::Relation] base_scope - is not mutated
# @return [ActiveRecord::Relation]
def resolve(base_scope = User.all)
valid_attributes.inject(base_scope) do |scope, key|
if self.respond_to?("filter_by_#{key}")
scope.merge(self.send("filter_by_#{key}"))
else
scope.where(key => self.send(key))
end
end
end
private
def filter_by_project_name
User.joins(:projects)
.where(projects: { name: project_name })
end
# Using compact_blank is admittedly a pretty naive solution for testing
# if attributes should be used in the query - but you get the idea.
def valid_attributes
attributes.compact_blank.keys
end
end
This is especially relevant when you're talking about a User class which usually is the grand-daddy of all god classes in a Rails application.
The key to the elegance here is using Enumerable#inject
which lets you iterate accross the attributes and add more and more filters successively and ActiveRecord::SpawnMethods#merge
which lets you mosh scopes together. You can think of this kind of like calling .where(first_name: first_name).where(last_name: last_name)...
except in a loop.
Usage:
@users = UserQuery.new(
params.permit(:first_name, :last_name, :email, :project_name)
).resolve
Having a model means that you can use it for form bindings:
<%= form_with(model: @user_query, url: '/users/search') do |f| %>
# ...
<% end %>
And add validations and other features without making a mess.
与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…