Accueil / Blog / Métier / 2020 / Symfony : utiliser une contrainte de type Callback dans un formulaire pour de la validation spécifique

Symfony : utiliser une contrainte de type Callback dans un formulaire pour de la validation spécifique

Par Simon Mellerin — publié 26/02/2020, édité le 31/03/2020
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.
Symfony : utiliser une contrainte de type Callback dans un formulaire pour de la validation spécifique

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

ABONNEZ-VOUS À LA NEWSLETTER !
Voir aussi
Le framework Symfony, un choix réfléchi 05/10/2016

Alliant souplesse, performance et efficacité, Symfony devient un incontournable dans nos ...

Comment démarrer un projet Symfony 4 en 5 minutes Comment démarrer un projet Symfony 4 en 5 minutes 24/05/2018

Les nouveautés Symfony 4 permettent de démarrer un projet vraiment simplement.

Drupal 8 : Dynamiser vos contenus à l'aide des formulaires AJAX Drupal 8 : Dynamiser vos contenus à l'aide des formulaires AJAX 18/12/2018

Utiliser les AjaxCommands de l'API de Drupal 8 pour agir sur le Markup

Makina Corpus, premier pas en éco-conception 05/11/2018

Makina Corpus va être accompagnée pour intégrer les principes de l’éco-conception dans le ...

Utiliser des bundles Symfony dans Drupal 7 Utiliser des bundles Symfony dans Drupal 7 19/12/2016

Notre module Drupal Symfony DIC permet d'apporter la puissance de Symfony, dans Drupal, ...

Nos formations
Formation Symfony