Makina Blog
Symfony : utiliser une contrainte de type Callback dans un formulaire pour de la validation spécifique
Vous devez développer une contrainte pour un formulaire métier ? La déclarer à l'aide du composant Validation de Symfony est peut-être excessif : il est aussi possible de le faire en passant par une assertion de type Callback.
La plupart du temps, quand on développe un formulaire avec Symfony, les contraintes offertes par le core suffisent, mais parfois, des contraintes plus métier nécessitent des développements spécifiques. Symfony offre la possibilité de créer ses propres contraintes de validation en implémentant les classes Constraint et ConstraintValidator. Mais cette méthodologie peut s'avérer lourde lorsque l'on développe une contrainte métier simple qui ne sera utilisée qu'une fois.
Dans ce cas, on peut implémenter notre contrainte à l'intérieur d'une assertion de type Callback.
Note : si le formulaire est rattaché à une entité Doctrine, les contraintes de l'entité sont automatiquement prises en compte.
L'assertion de type Callback s'utilise comme une assertion classique, la principale option à renseigner au constructeur est 'callback
', qui peut prendre comme valeur un callable :
- Le nom d'une méthode à appeler ;
- Un array callable au format
['<Class>', '<method>']
; - Une closure.
Exemple d'utilisation avec une closure :
<?php
$maContrainte = new Callback([
// la variable $data prend la valeur de l'élément sur lequel on applique la contrainte
'callback' => static function ($data, ExecutionContextInterface $context) {
if (/* Ma condition */) {
$context
->buildViolation("Vous n'avez pas fait ce qu'il fallait !")
->addViolation()
;
}
}
]);
Pour mieux comprendre son utilisation, un premier exemple de contrainte rattachée directement au formulaire va vous être présenté, puis un autre où la validation se fait au niveau d'un champ spécifique.
Ajouter une contrainte sur le formulaire
Lorsqu'une contrainte est multi-champs, elle peut être renseignée directement sur le formulaire. Si la contrainte n'est pas vérifiée à la soumission du formulaire, le message d'erreur apparaît en haut du formulaire et non sur un champ en particulier.
Pour illustrer, imaginons un formulaire permettant de créer un rectangle, possédant les champs suivants :
- Longueur : entier, doit être supérieure à 0 ;
- Largeur : entier, doit être supérieure à 0.
Seulement, nous souhaitons que les rectangles créés via ce formulaire soient harmonieux : le ratio longueur/largeur doit être égal au nombre d'or ( 1,62), avec une marge de 5% (on est quand même un peu tolérant).
La contrainte ici n'est pas spécifique au champ « Longueur » ou « Largeur » seul, mais à l'ensemble de ces deux champs.
Voici à quoi ressemblerait le code de création de ce formulaire :
<?php
use Symfony\Component\Form\Extension\Core\Type as Form;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
...
$form = $this->createFormBuilder(null, [
'constraints' => [
new Assert\Callback(
// l'array $data contient les valeurs des différents champs du formulaire
['callback' => static function (array $data, ExecutionContextInterface $context) {
$nombreOr = 1.62;
$ratio = $data['longueur'] / $data['largeur'];
if ($ratio > ($nombreOr * 1.05) || $ratio < ($nombreOr * 0.95)) {
$context
->buildViolation("Votre rectangle ne sera pas élégant, essayez autre chose !")
->addViolation()
;
}
}]
)
]])
->add('longueur', Form\IntegerType::class, [
'label' => "Longueur",
'required' => true,
'constraints' => [
new Assert\GreaterThan([
'value' => 0,
'message' => "La longueur doit être supérieure à 0."
]),
]
])
->add('largeur', Form\IntegerType::class, [
'label' => "Largeur",
'required' => true,
'constraints' => [
new Assert\GreaterThan([
'value' => 0,
'message' => "La largeur doit être supérieure à 0."
]),
]
])
->add('save', Form\SubmitType::class, [
'label' => "Enregistrer ",
])
;
Nous avons ainsi ajouté notre contrainte d'une manière relativement rapide et peu verbeuse.
Ajouter une contrainte sur un élément spécifique du formulaire
De la même manière, on peut vérifier une contrainte sur un champ particulier.
Par exemple, prenons le formulaire suivant qui permet de renseigner une question/réponse :
- Question : chaîne de caractères, doit faire moins de 120 caractères, commencer par une majuscule et finir par un point d'interrogation ;
- Réponse : chaîne de caractères, doit faire moins de 500 caractères.
Ici, il semble assez compliqué de réaliser les contraintes du champ « Question » avec ce que nous propose le core de Symfony : utilisons une assertion de type Callback pour les exprimer :
<?php
use Symfony\Component\Form\Extension\Core\Type as Form;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
...
$form = $this->createFormBuilder()
->add('question', Form\TextType::class, [
'label' => "Question",
'required' => true,
'constraints' => [
new Assert\Length([
'min' => 0,
'minMessage' => "La question ne peut pas être vide.",
'max' => 120,
'maxMessage' => "La question doit faire moins de 120 caractères.",
]),
new Assert\Callback([
// Ici $value prend la valeur du champs que l'on est en train de valider,
// ainsi, pour un champs de type TextType, elle sera de type string.
'callback' => static function (?string $value, ExecutionContextInterface $context) {
if (!$value) {
return;
}
if (!\preg_match('~^\p{Lu}~u', $value)) {
$context
->buildViolation('La question doit commencer par une majuscule.')
->atPath('[question]')
->addViolation()
;
}
if (\substr($value, \strlen($value) - 1, 1) !== '?') {
$context
->buildViolation("La question doit finir par un point d'interrogation.")
->atPath('[question]')
->addViolation()
;
}
},
]),
]
])
->add('reponse', Form\TextareaType::class, [
'label' => "Réponse",
'required' => true,
'constraints' => [
new Assert\Length([
'min' => 0,
'minMessage' => "La réponse ne peut pas être vide.",
'max' => 500,
'maxMessage' => "La réponse doit faire moins de 500 caractères.",
]),
]
])
->add('save', Form\SubmitType::class, [
'label' => "Enregistrer ",
])
;
Finalement
En résumé, l'utilisation d'une assertion de type Callback apporte les avantages suivants :
- C'est peu verbeux, il n'est pas nécessaire de déclarer des classes Constraint et ConstraintValidator ;
- Le métier lié à la validation du formulaire est au même endroit que le formulaire lui-même.
Mais il peut vite atteindre ses limites :
- La déclaration du formulaire peut devenir très grosse si on a beaucoup de contraintes de ce type ;
- Si nous utilisons une closure la contrainte n'est pas du tout réutilisable.
Lorsque ces limites sont atteintes, une solution simple consiste à ne plus utiliser de closure mais à déclarer la méthode de callback à l'extérieur de la création du formulaire.
Ainsi, la déclaration du formulaire en elle-même est plus légère et la méthode de validation peut être réutilisée.
Pour aller plus loin
Venez assister à l'une de nos sessions de formation Symfony
Actualités en lien
DbToolsBundle : sortie de la version 1.2
Découvrez les nouveautés de cette nouvelle version ainsi que les fonctionnalités à venir de la prochaine version majeure.
Access Control, bibliothèque PHP pour gérer des droits d’accès
Suite à un projet de gestion métier opérationnel dont la durée de vie et la maintenance sont à long termes, nous avons expérimenté un passage de celui-ci sur l’architecture hexagonale et la clean architecture.
L’anonymisation sous stéroïdes avec le DBToolsBundle
Le DbToolsBundle permet d’anonymiser des tables d’un million de lignes en seulement quelques secondes. Cet article vous présente la méthodologie mise en place pour arriver à ce résultat.