Validation spécifique qui me pose une colle

Dépendant du remplissage d'un champ, du fait que le formulaire est embarqué ou non ET AUSSI d'un rôle

Le problème exposé dans ce sujet a été résolu.

Bonjour à tous !

Aujourd’hui, je suis ici parce que j’ai une contrainte de validation qui me paraît un peu particulière. Je tiens à préciser que je suis sous Symfony 2.8, et que passer à une version plus récente (probablement 3) n’est pas considérable dans les mêmes délais.  :p

L’idée est de valider l’upload de scans de documents d’identité, et de renseigner en même temps certaines valeurs (code de sécurité. date d’expiration). Valider des champs, pas de souci, valider un upload, OK, valider une entité, pas de souci. Le problème est qu’il y a des cas spécifiques.

D’une part, on peut uploader un de ces documents pour une personne existante, donc formulaire "principal".
D’autre part, à la création d’une nouvelle personne, celui qui remplit peut fournir le scan et saisir ces informations, donc formulaire embarqué.

Mais indépendamment de cela, certaines personnes peuvent ne pas avoir à remplir les champs texte/date/autres, en clair ne fournir que le scan.

Le scénario est donc le suivant :

  1. Si j’ai les droits d’uploader des fichiers sans faire plus
    1. et que je créé une nouvelle personne : je n’ai aucune contrainte.
    2. et que j’ajoute un document à une personne existante : le champ du fichier est obligatoire
  2. Si je dois remplir les champs
    1. et que je créé une nouvelle personne
      1. et que je n’ai pas de document à ce moment : je n’ai aucune contrainte sur les champs du document
      2. et que je renseigne le document : les autres champs du document "deviennent" obligatoires
    2. et que j’ajoute un document à une personne existante :
      • tous les champs sont obligatoires

Du coup, je tente de voir comment spécifier les contraintes serveur en fonction de cela.

J’ai déjà deux formulaires, un pour embarquer et un autre "standalone". Pour l’instant, le second hérite du premier (principalement parce que le second contient le bouton de soumission dans sa déclaration des champs).

J’avais pensé à une validation par callback, mais je ne vois pas comment je peux passer le service security.authorization_checker à ma méthode (qui réside dans l’entité).
J’ai aussi réfléchi aux événements de formulaire pour ajouter le champ avec les contraintes correctes, mais avec PRE_SET_DATA, POST_SET_DATA et PRE_SUBMIT, je n’ai pas la propriété qui est remplie. Je l’aurais avec POST_SUBMIT, mais évidemment une fois le formulaire soumis au sens Symfony, plus moyen d’y ajouter un champ…

Je pourrais me créer mon propre validateur, mais ça me paraît relativement overkill

Mon souci est donc soit d’accéder au service security.authorization_checker, soit de savoir si un fichier a bien été envoyé.

Est-ce que quelqu’un aurait déjà rencontré un cas similaire ?

Merci d’avance  :)

Edit

En fait, j’étais resté bloqué sur @Assert\Callback(), qui forcément est dans l’entité. Mais si je définis la contrainte dans la classe de fomrmulaire, j’ai accès à plus de choses, notamment les valeurs "brutes" du formulaire. Le seul hic maintenant est que j’ai une "double" validation, une avec les annotations dans l’entité, et une autre de par les contraintes dans la classe de formulaire. Je soupçonne une différence entre l’appel à handleRequest() et submit(), l’une impliquant la validation des mappings et pas l’autre.

+0 -0

Je rouvre le sujet parce que je m’y perds encore. Je pensais avoir une idée du souci, mais visiblement non.

tl;dr : qu’est-ce qui peut faire que les contraintes d’annotations ne sont pas lues ?

Avec Symfony 2.8.28 au moins, les groupes de validation peuvent être différents pour la validation définie dans le formulaire et celle dans l’entité en cas d’entités embarquées. Donc groupe Default pour les annotations, mais le groupe défini dans la classe de formulaire pour celles qui y sont définies.

Je vous fournis mon formulaire actuel ainsi que les annotations de l’entité.

<?php

namespace A\WebBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\Validator\Constraints as Assert;
use Vich\UploaderBundle\Mapping\Annotation as Vich;

/**
 * Identity
 *
 * @ORM\Table()
 * @ORM\Entity(repositoryClass="A\WebBundle\Repository\IdentityRepository")
 *
 * @Vich\Uploadable
 */
class Identity
{
    const STATE_PENDING = 'pending';
    const STATE_VALIDATED = 'validated';
    const STATE_REJECTED = 'rejected';
    const STATE_ARCHIVED = 'archived';
    const STATE_EXPIRED = 'expired';

    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * Identity number
     * @var string
     *
     * @Assert\NotBlank(groups={"create", "edit"})
     *
     * @ORM\Column(type="string", length=255, nullable=true)
     */
    private $number;

    /**
     * Password Nationality (not only limited to cio countries)
     *
     * @var string
     *
     * @Assert\NotBlank(groups={"create", "edit"})
     *
     * @ORM\Column(type="string", length=2, nullable=true)
     */
    private $nationality;

    /**
     * @var string
     *
     * @ORM\Column(name="filename", type="string", length=255)
     */
    private $filename;

    /**
     * @var File
     *
     * @Assert\File(maxSize="10M", mimeTypes={"image/jpeg", "image/png", "application/pdf"}, groups={"create", "createEmpty", "edit", "editEmpty"})
     * @Assert\NotBlank(groups={"create", "createEmpty"})
     *
     * @Vich\UploadableField(mapping="person_identity", fileNameProperty="filename")
     */
    private $file;

    /**
     * @var \DateTime
     *
     * @Assert\NotBlank(groups={"create", "edit"})
     *
     * @ORM\Column(name="expiry_date", type="datetime", nullable=true)
     */
    private $expiryDate;

    /**
     * @var string
     *
     * @ORM\Column(name="validation_state", type="string", length=255)
     */
    private $validationState = self::STATE_PENDING;

    /**
     * @var string
     *
     * @ORM\Column(type="text", nullable=true)
     */
    private $rejectionReason;

    /**
     * @var \DateTime
     *
     * @ORM\Column(name="validated_at", type="datetime", nullable=true)
     */
    private $validatedAt;

    /**
     * @var \DateTime
     *
     * @ORM\Column(name="rejected_at", type="datetime", nullable=true)
     */
    private $rejectedAt;

    /**
     * @var Person
     *
     * @ORM\ManyToOne(targetEntity="Person", inversedBy="identities", fetch="EAGER")
     */
    private $person;

    /* Les accesseurs et mutateurs ne me semblent pas nécessaires, ils sont "standard",
     * pas de manipulations particulières
}
Identity.php
<?php
namespace A\WebBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CountryType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Context\ExecutionContextInterface;

/**
 * Form used by Staff to add a new Identity
 *
 */
class CreateEmbeddedIdentityType extends AbstractType
{
    protected $securityContext;

    public function __construct(SecurityContextInterface $securityContext)
    {
        $this->securityContext = $securityContext;
    }

    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $futureYear = new \DateTime('+80 years');
        $futureYear = (int) $futureYear->format('Y');
        $pastYear = new \DateTime('-20 years');
        $pastYear = (int) $pastYear->format('Y');

        $builder
            ->add('file', FileType::class)
            ->add('number', null, array(
                'required' => !$this->securityContext->isGranted('ROLE_IDENTITY_SUBMIT_EMPTY'),
            ))
            ->add('expiryDate', DateType::class, array(
                'years' => range($futureYear, $pastYear),
                'required' => !$this->securityContext->isGranted('ROLE_IDENTITY_SUBMIT_EMPTY'),
            ))
            ->add('nationality', CountryType::class, array(
                'placeholder' => 'person.identity.form.nationality_empty',
                // Required server-side, but as the field is handled with JavaScript, the HTML 5 popup doesn't show up
                'required' => false,
                'attr' => array(
                    'data-type' => 'chosen'
                ),
            ))
        ;

        $securityContext = $this->securityContext;
        $builder->addEventListener(
            FormEvents::PRE_SET_DATA,
            function (FormEvent $event) use ($securityContext, $futureYear, $pastYear) {
                $form = $event->getForm();
                $form
                    ->add('number', null, array(
                        'required'    => !$securityContext->isGranted('ROLE_IDENTITY_SUBMIT_EMPTY'),
                        'constraints' => array(
                            new Callback(array(
                                'groups'   => array('create', 'edit'),
                                'callback' => function ($value, ExecutionContextInterface $context) use ($form, $securityContext) {
                                    $file = $form->get('file')->getData();

                                    if (empty($value) && null != $file && !$securityContext->isGranted('ROLE_IDENTITY_SUBMIT_EMPTY') && $context->getViolations()->count() < 1) {
                                        $context->addViolation('This value should not be blank.');
                                    }
                                }
                            )),
                        ),
                    ))
                    ->add('expiryDate', DateType::class, array(
                        'years' => range($futureYear, $pastYear),
                        'required' => !$this->securityContext->isGranted('ROLE_IDENTITY_SUBMIT_EMPTY'),
                        'constraints' => array(
                            new Callback(array(
                                'groups'   => array('create', 'edit'),
                                'callback' => function ($value, ExecutionContextInterface $context) use ($form, $securityContext) {
                                    $file = $form->get('file')->getData();

                                    if (empty($value) && null != $file && !$securityContext->isGranted('ROLE_IDENTITY_SUBMIT_EMPTY') && $context->getViolations()->count() < 2) {
                                        $context->addViolation('This value should not be blank.');
                                    }
                                }
                            )),
                        ),
                    ))
                    ->add('nationality', CountryType::class, array(
                        'placeholder' => 'person.identity.form.nationality_empty',
                        // Required server-side, but as the field is handled with JavaScript, the HTML 5 popup doesn't show up
                        'required' => false,
                        'attr' => array(
                            'data-type' => 'chosen'
                        ),
                        'constraints' => array(
                            new Callback(array(
                                'groups'   => array('create', 'edit'),
                                'callback' => function ($value, ExecutionContextInterface $context) use ($form, $securityContext) {
                                    $file = $form->get('file')->getData();

                                    if (empty($value) && null != $file && !$securityContext->isGranted('ROLE_IDENTITY_SUBMIT_EMPTY') && $context->getViolations()->count() < 3) {
                                        $context->addViolation('This value should not be blank.');
                                    }
                                }
                            )),
                        ),
                    ))
                ;
            }
        );
    }

    /**
     * {@inheritdoc}
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $validationGroups = array('create');

        if ($this->securityContext->isGranted('ROLE_IDENTITY_SUBMIT_EMPTY')) {
            $validationGroups = array('createEmpty');
        }

        $resolver->setDefaults(array(
            'validation_groups' => $validationGroups,
            'data_class' => 'A\WebBundle\Entity\Identity',
        ));
    }

    /**
     * {@inheritdoc}
     */
    public function getBlockPrefix()
    {
        return 'create_identity';
    }
}
CreateEmbeddedIdentityType

Comme vous pouvez le constater, j’ai donc des contraintes dans l’entité, et des contraintes dans le formulaire (afin d’avoir accès au contexte de sécurité).

Le problème que je tente de résoudre actuellement, c’est que les contraintes au niveau de l’entité semblent ne pas être prises en compte quand le formulaire est embarqué dans mon PersonType. Dans ce PersonType, j’ai pourtant bien mis que les objets Identity devaient être valides (contrainte Valid, qui par défaut boucle sur les éléments d’une collection). Mais apparemment cela ne déclenche que la validation formulaire, à en croire la liste des validations (avant d’avoir ajouté les tests sur le nombre de contraintes dans les fonctions de rappel, évidemment), tandis que le formulaire d’ajout "standalone" prend en compte les deux sources de contrainte (les tests sur le nombre de contraintes est là pour éviter d’avoir deux fois le même message, une fois du formulaire et une fois de l’annotation).

Comme dit précédemment, je pensais qu’il s’agissait d’une différence entre handleRequest() et submit(), mais après avoir testé, c’est une mauvaise piste.

Evidemment, je pourrais redéfinir le champ d’upload au même moment que les autres et voir s’il est vide et pas les autres (ce qui est un peu le pendant logique de ce que je fais avec eux), mais je trouve cela vraiment hacky

J’ai aussi tenté d’ajouter le groupe de validation 'Default' (avec et sans majuscule), mais apparemment ça n’apporte rien.
Evidemment, un nouveau validateur ne serait pas très utile, étant donné que c’est comme si les annotations de validation n’étaient pas prises en compte. Et le mettre au niveau du formulaire ne serait pas non plus pratique…

Je complète mon scénario (les points 3 en italique) :

  1. Si j’ai les droits d’uploader des fichiers sans faire plus

    1. et que je créé une nouvelle personne : je n’ai aucune contrainte.
    2. et que j’ajoute un document à une personne existante : le champ du fichier est obligatoire
    3. et que j’ajoute une des données du document sans ledit document, on doit le demander
  2. Si je dois remplir les champs

    1. et que je créé une nouvelle personne

      1. et que je n’ai pas de document à ce moment : je n’ai aucune contrainte sur les champs du document
      2. et que je renseigne le document : les autres champs du document "deviennent" obligatoires
      3. et que je renseigne un des champs du document, mais pas celui-ci, j’aimerais que la contrainte existante s’applique (ce qui n’est actuellement pas le cas)
    2. et que j’ajoute un document à une personne existante :

      • tous les champs sont obligatoires

Est-ce que quelqu’un voit ce autour de quoi je tourne sans m’en rendre compte ?

Merci

Edit

Alors avec Symfony 2.8.28, les groupes de validation peuvent ne pas être les mêmes pour le formulaire et pour l’entité. Donc dans l’exemple fourni ici, les annotations n’étaient pas prises en compte dans le formulaire embarqué parce que le groupe "cascadé" est Default. En revanche, lors de la même soumission, dans le formulaire c’est le groupe déclaré dans CreateEmbeddedIdentityType::configureOptions qui est utilisé… Donc deux groupes dans le cas d’un formulaire embarqué.

Du coup, maintenant que j’ai compris comment lancer la validation avec les annotations, je peux créer un validateur un peu plus complexe. Voilà qui sera plus propre et souple.

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte