Un concern réutilisable pour filtrer les modèles dans Ruby on Rails
Je voulais implémenter un système de filtrage générique pour des modèles dans Ruby on Rails, en procédant un peu différemment de ce que Justin Weiss et Fabio Pitino proposent.
Justin Weiss a jeté les bases d’un concern
en 2016 dans son article
Search and Filter Rails Models Without Bloating Your Controller (Chercher et filtrer parmi des modèles Rails sans alourdir les contrôleurs).
Fabio Pitino a poussé la technique plus loin grâce à une nouvelle macro nommée search_scope
dans Enhanced Filterable concern for Rails models.
Le nom indique clairement quels scope
s sont utilisés pour le filtrage, et simplifie la gestion des paramètres autorisés.
Cependant, le code de Fabio ne fonctionne plus avec Rails 6 (la macro search_scope
n’est pas initialisée à temps), et le nom ne refléte pas vraiment ce qui se passe.
Voici ce que j’ai fini par mettre en place, en m’appuyant sur le travail de Justin et Fabio.
-
Ajuster le nom et la syntaxe du
concern
pour que ce soit plus explicite :# app/models/concerns/filterable.rb module Filterable extend ActiveSupport::Concern class_methods do attr_reader :filter_scopes def filter_scope(name, *args) scope name, *args @filter_scopes ||= [] @filter_scopes << name end def filter_by(params) filtered = all params.permit(*filter_scopes).each do |key, value| filtered = filtered.public_send(key, value) if value.present? end filtered end end end
-
Inclure ce
concern
dans tous les modèles héritant d’ApplicationRecord
:# app/models/application_record.rb class ApplicationRecord include Filterable end
-
Ajouter la macro
filter_scope
dans tous les modèles qui doivent être filtrés :# app/models/widget.rb class Widget < ApplicationRecord filter_scope :foo, { where(foo: "bar") } end
-
Appliquer le filtre dans le contrôleur :
# app/controllers/widgets_controller.rb class WidgetsController < ApplicationRecord def index @widgets = Widget.filter_by(params) end end
Je ne suis toujours pas entièrement satisfait de cette solution car elle semble un peu grossière.
Dans la même veine, il existe la gemme has_scope
de HeartCombo mais je trouve dommage qu’il faille autoriser les scope
dans chaque contrôleur, même si c’est plus sécurisé.
Je vois plusieurs améliorations possibles :
- Automatiser le filtrage avec un
before_action
dans les contrôleurs, comme le faitCanCanCan
avecload_and_authorize_resource
. - Créer un
FormObject
queform_for
pourrait utiliser pour présélectionner les valeurs du formulaire à partir de la requête actuelle. - Renforcer la sécurité, ce que
has_scope
fait de manière plus préventive, pour empêcher les injections SQL. - Simplifier les requêtes sur les
scope
s booléens et ceux qui prennent plus d’un paramètre (date de début/fin par exemple).