
Nous avons souvent besoin d'une validation contextuelle dans les modèles ActiveRecord et nous ne trouvons pas de moyen idéal pour le faire. Cet article présente une solution élégante, légère et sans dépendance.Tout d'abord, gardons à l'esprit deux études de cas qui illustrent parfaitement cette problématique :
Quels sont les outils fournis par ActiveRecord pour faire face à ce problème ?
Vous pouvez également utiliser d'autres outils tels que la validation sèche.
Rien de ce qui précède ne correspond à mes attentes : une solution modulaire, légère, qui ne nécessite aucune dépendance supplémentaire et qui repose uniquement sur ActiveModel::Validations.

Construisez un objet avec uniquement le comportement requis pour le contexte en question.
Autrement dit : au lieu d'avoir un modèle avec toutes les validations et de n'exécuter que celles qui sont nécessaires, ayez un modèle avec seulement les validations toujours nécessaires et étendez-le avec des validations spécifiques au contexte/état.

Avantages:
Inconvénients:

Supposons que nous ayons un modèle Article avec un title, un subtitle, un content et un state (valeurs : %i[draft published]).
Bien que l'article soit un draft, il suffit qu'un titre ou un sous-titre soit présent.
Pour être published, l'article doit avoir un titre, un sous-titre et un contenu présent. Le titre doit également être unique parmi les articles publiés.
app/models/article.rb
class Article < ApplicationRecord
validates :state, inclusion: { in: %i[draft published] }
endapp/validations/articles/draft_validation.rb
module Articles::DraftValidation
def self.extended(obj)
obj.class_eval do
validates :title, presence: true, unless: ->{ subtitle.present? }
end
end
endapp/validations/articles/published_validation.rb
module Articles::PublishedValidation
def self.extended(obj)
obj.class_eval do
validates :title, presence: true, uniqueness: { conditions: ->{ where(status: 'published') } }
validates :subtitle, presence: true
validates :content, presence: true
end
end
endMaintenant, voici comment valider un article en fonction de son état :
class ArticlesController < ApplicationController
def update
@article.assign_attributes(article_params)
case @article.state
when :draft
@article.extend(Articles::DraftValidation)
when :published
@article.extend(Articles::PublishedValidation)
end
if @article.save
redirect_to @article, notice: 'Article was successfully updated.'
else
render :edit
end
end
endIl s'agit d'une manière simplifiée à l'extrême, mais fonctionnelle, de construire un article avec juste les bonnes validations pour son état actuel.
Supposons que nous ayons un modèle de Profile et que, pour des raisons d'ergonomie, nous ayons décidé de diviser le processus d'enregistrement en 4 étapes :
Puisque j'ai démontré précédemment comment écrire des modules qui contiennent des validations spécifiques au contexte et à l'état, nous supposerons que les 4 modules suivants existent :
Pour pouvoir remplir le formulaire à l'étape N, il faut d'abord que toutes les étapes précédentes soient correctement remplies.Voici un exemple de la manière dont on pourrait construire un remplissage progressif de formulaire :
class ProfilesController < ApplicationController
STEPS = %w[personal_information family_situation work_expectations financial_situation].freeze
def update
@profile = Profile.find(params[:id])
previous_incomplete_step = find_previous_incomplete_step
if previous_incomplete_step
redirect_to edit_profiles_controller(id: @profile.id, current_step: previous_incomplete_step), notice: "Please complete this form first"
return
end
@profile.assign_attributes(profile_params)
@profile.extend(step_validation_module(current_step))
if @profile.save
redirect_to_next_step
else
render :edit
end
end
private
def current_step
params[:current_step]
end
def redirect_to_next_step
if current_step == STEPS.last
redirect_to root_path, notice: "Profile successfully completed!"
else
next_step = STEPS[STEPS.find_index(current_step) + 1]
edit_profiles_controller(id: @profile.id, current_step: next_step)
end
end
def find_previous_incomplete_step
STEPS.each do |step|
break if step == current_step
@profile.extend(step_validation_module(step))
return step unless @profile.valid?
end
nil
end
def step_validation_module(step)
"Profiles::#{step.to_s.camelize}Validation".constantize
end
endC'est à dessein que je n'écris pas toutes les méthodes et contrôles de sécurité nécessaires dans cet exemple de code, afin de le rendre aussi facile à lire que possible. Le but ici est évidemment d'illustrer une technique, et non d'écrire du code prêt à la production.Une dernière chose à propos de cette approche : vous pouvez composer une validation faite d'autres validations.
Supposons que vous ayez besoin, à plusieurs endroits, de valider que le profil est complet. Allez-vous continuer à étendre votre enregistrement avec les validations des 4 étapes distinctes à chaque fois ? Non, vous écrirez simplement un autre module de validation qui sera lui-même composé des validations des 4 étapes. En tant que tel :
module Profiles::FullValidation
def self.extended(obj)
obj.class_eval do
extend Profiles::PersonalInformationValidation
extend Profiles::FamilySituationValidation
extend Profiles::WorkExpectationsValidation
extend Profiles::FinancialSituationValidation
end
end
endMerci d'avoir lu!