Makina Blog

Le blog Makina-corpus

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

Image
Symfony
11/04/2024

Access Control : une biblio­thèque PHP pour gérer des droits d’ac­cès

Nous avons récem­ment abouti un projet de gestion métier opéra­tion­nel, dont la durée de vie et la main­te­nance sont plani­fiées pour de nombreuses années. Dans ce contexte, nous avons expé­ri­menté un passage de celui-ci sur l’archi­tec­ture hexa­go­nale et la clean archi­tec­ture.

Voir l'article
Image
Encart blog DBToolsBundle
21/03/2024

L’ano­ny­mi­sa­tion sous stéroïdes avec le DBTools­Bundle

Le DbTools­Bundle permet d’ano­ny­mi­ser des tables d’un million de lignes en seule­ment quelques secondes. Cet article vous présente la métho­do­lo­gie mise en place pour arri­ver à ce résul­tat.

Voir l'article
Image
Encart article 2 : Itéra­tions vers le DDD et la clean archi­tec­ture avec Symfony
20/02/2024

Itéra­tions vers le DDD et la clean archi­tec­ture avec Symfony (2/2)

Quels virages avons-nous pris après un premier projet expé­ri­men­tal pour stabi­li­ser notre concep­tion logi­cielle, et que ferons-nous plus tard ?

Voir l'article

Inscription à la newsletter

Nous vous avons convaincus