Vous êtes en colère à chaque fois que vous voyez des variables d'instance mutantes dans un contrôleur rails pour filtrer des données ? Cet article est fait pour vous 🙂 .
Filtres
Vous avez probablement déjà vu cela :
# app/controllers/api/v1/things_controller.rb
module API
module V1
classe ThingsController < BaseController
def index
@things = Thing.all
@things = @things.where(size : params[:size]) if params[:size]
@things = @things.where('name ILIKE ?', "%#{params[:name_contains]}%") if params[:name_contains]
render json : @choses
end
fin
fin
fin
Pourquoi est-ce que je considère qu'il s'agit d'une mauvaise code? Parce qu'il fait tout simplement grossir notre contrôleur. Selon moi, nous devrions extraire autant de logique que possible des contrôleurs et utiliser un système de contrôle de l'utilisation. ou des services. Dans ce cas, nous mettrons en place un filtre générique qui nous permettra de à utiliser sur plusieurs contrôleurs.
Mais attendez, analysons d'abord le code actuel. Il peut être mauvais mais fonctionne. Nous disposons d'un champ d'application initial (Chose.tout) et la limitent si l'utilisateur a passé le cap de la paramètre lié à un filtre. Pour chaque filtre, nous vérifions si le paramètre a été transmis et si c'est le cas, nous appliquons un filtre. La deuxième chose est que nous n'avons pas besoin d'utiliser l'ivar, nous pouvons utiliser de simples variables locales.
D'accord, alors. Ne pourrions-nous pas utiliser un objet de service pour modifier la portée initiale ? L'exécution peut se présenter comme suit :
# app/controllers/api/v1/things_controller.rb
module API
module V1
classe ThingsController < BaseController
def index
scope = Thing.all
things = Things::IndexFilter.new.call(scope, params)
render json : things
end
end
end
fin
Le résultat est bien meilleur, mais nous devons encore mettre en place le filtre. Notez que la signature de l'appel sera la même pour toutes les ressources, nous pouvons donc avoir une classe générique pour cette tâche.
# app/services/generic/index_filter.rb
module Generic
classe IndexFilter
EMPTY_HASH = {}.freeze
def self.filters
EMPTY_HASH
end
def call(scope, params)
apply_filters !(self.class.filters.keys, scope, params)
end
private
def apply_filters !(filter_keys, scope, params)
filter_keys.inject(scope.dup) do |current_scope, filter_key|
apply_filter !(filter_key, current_scope, params)
end
fin
def apply_filter !(filter_key, scope, params)
filter = fetch_filter(filter_key)
return scope unless apply_filter ?(filter, params)
filter[:apply].call(scope, params)
fin
def apply_filter ?(filter, params)
filter[:apply ?].call(params)
end
def fetch_filter(filter_key)
self.class.filters.fetch(filter_key) { raise ArgumentError, 'unknown filter' }
end
end
end
Cela semble compliqué ? Pas vraiment - toute la magie se passe dans #apply_filters !. Nous prenons le duplicata de l'étendue initiale et lui appliquons chaque filtre.
Lorsque nous appliquons le champ d'application, cela signifie que nous mutons le duplicata de notre champ d'application initial. Nous nous attendons à ce que les filtres soient mis en œuvre sous la forme d'un hachage dans le fichier self.filters méthode d'une classe enfantine. C'est ce que nous allons faire.
# app/services/things/index_filter.rb
module Things
class IndexFilter (params) {
params[:size].is_a ?(String)
},
apply : ->(scope, params) {
scope.where(size : params[:size])
}
}.freeze,
name_contains_filter : {
apply? : ->(params) {
params[:name_contains].is_a ?(String)
},
apply : ->(scope, params) {
scope.where('name ILIKE ?', "%#{params[:name_contains]}%")
}
}.freeze
}.freeze
def self.filters
FILTERS
fin
end
end
Voilà, c'est fait ! Nous avons écrit plus de code, mais les filtres simples se présentent de la même manière pour toutes les ressources. Nous avons nettoyé le contrôleur du code responsable de filtrage et a prévu une classe "spécialisée" à cet effet, qui suit de très près le processus de filtrage. une convention claire.