14

I've got some issues with Symfony's form validation handling. I'd like to validate a form bound to an entity based on its data. There are quite a bunch of information how to dynamically modify the form fields using FormEvents. What I'm missing on this topic is how to control/modify the validation.

My simplified use case is:

  1. A user can add an event to a calendar.
  2. The validation checks if there's already an event.
  3. If there's a collision, the validation will throw an error.
  4. The user should now be able to ignore this error/warning.

The validation is implemented as a Validator with Constraint::CLASS_CONSTRAINT as the target (as it's taking some more stuff into account).

I tried to:

  • Hack around the validation groups, but couldn't find access to the entity wide validators.
  • Hack around the FormEvents and add an extra field like "Ignore date warning".
  • Hack around the submit button to change it to something like "Force submit".

... but never found a working solution. Even simpler hacks with a single property based validator didn't work out. :(

Is there a Symfony way to dynamically control the validation?

Edit: My code looks like this:

use Doctrine\ORM\Mapping as ORM;
use Acme\Bundle\Validator\Constraints as AcmeAssert;

/**
 * Appointment
 *
 * @ORM\Entity
 * @AcmeAssert\DateIsValid
 */
class Appointment
{
  /**
   * @ORM\Column(name="title", type="string", length=255)
   *
   * @var string
   */
  protected $title;

  /**
   * @ORM\Column(name="date", type="date")
   *
   * @var \DateTime
   */
  protected $date;
}

The validator used as a service:

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
 * Validates the date of an appointment.
 */
class DateIsValidValidator extends ConstraintValidator
{
    /**
     * {@inheritdoc}
     */
    public function validate($appointment, Constraint $constraint)
    {
        if (null === $date = $appointment->getDate()) {
            return;
        }

        /* Do some magic to validate date */
        if (!$valid) {
            $this->context->addViolationAt('date', $constraint->message);
        }
    }
}

The corresponding Constraint class is set to target the entity class.

use Symfony\Component\Validator\Constraint;

/**
 * @Annotation
 */
class DateIsValid extends Constraint
{
    public $message = 'The date is not valid!';

    /**
     * {@inheritdoc}
     */
    public function getTargets()
    {
        return self::CLASS_CONSTRAINT;
    }

    /**
     * {@inheritdoc}
     */
    public function validatedBy()
    {
        return 'acme.validator.appointment.date';
    }
}

Edit 2: Try with FormEvents... I also tried all the different events.

$form = $formFactory->createBuilder()
    ->add('title', 'text')
    ->add('date', 'date')
    ->addEventListener(FormEvents::WHICHONE?,  function(FormEvent $event) {
        $form = $event->getForm();

        // WHAT TO DO HERE?
        $form->getErrors(); // Is always empty as all events run before validation?

        // I need something like
        if (!$dateIsValid) {
            $form->setValidationGroup('ignoreWarning');
        }
    });

Edit 3: Constraint are correctly declared. That's not the issue:

services:
    validator.acme.date:
        class: AcmeBundle\Validator\Constraints\DateValidator
        arguments: ["@acme.other_service"]
        tags:
            - { name: validator.constraint_validator, alias: acme.validator.appointment.date }
althaus
  • 2,009
  • 2
  • 25
  • 33
  • Did you try modifying the forms `validation_groups` from inside your FormEvent? – Debreczeni András Jun 20 '14 at 11:55
  • For _do-it-fast-not-caring-about-bad-practices_ you can validate it against the desired constraint in a controller. – reafle Jun 20 '14 at 13:27
  • I would stick with the "ignore date warning" approach and make validation in callback. – tiriana Jun 20 '14 at 18:49
  • You can do exactly what you want with form events. What part do you not understand? I can help you explain or give you a basic example. – tomazahlin Jun 20 '14 at 19:10
  • @DebreczeniAndrás Yes, I tried to use the `FormEvents` in combination with the `validation_groups`. I was stuck at fetching the current validation error to somehow disable it. – althaus Jun 23 '14 at 09:45
  • @reafle I'd like to have a clean approach. The code is embedded in an automated CRUD handling, so there shouldn't be custom code in the controller. – althaus Jun 23 '14 at 09:46
  • @all I added my code basics and what I tried with the FormEvents. – althaus Jun 23 '14 at 09:54
  • But have you used the tags in the service definition? tags: - { name: validator.constraint_validator, alias: 'acme.validator.appointment.date' } – erlangb Jun 27 '14 at 11:06
  • @erlangb Yes, that's not the issue. – althaus Jun 27 '14 at 11:40
  • O sorry I didn't read that you wrote this on the question. – erlangb Jun 27 '14 at 13:26
  • Why don't you simply use a callback constraint? http://symfony.com/doc/current/reference/constraints/Callback.html – ggioffreda Jun 28 '14 at 14:22
  • @ggioffreda I'd be happy about a working example as I'm basically working with callbacks. The problem is that I cannot get around the quite strict workflow of forms and their validation. – althaus Jun 30 '14 at 09:00

4 Answers4

2

Validation is done on the entity, all Forms does is execute the Object's validations. You can choose groups based on submitted data

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'validation_groups' => function(FormInterface $form) {
            $data = $form->getData();
            if (Entity\Client::TYPE_PERSON == $data->getType()) {
                return array('person');
            } else {
                return array('company');
            }
        },
    ));
}

I have had issues when using this approach on embedded forms && cascade-validation

Edit: using flash to determine if validation must take place.

// service definition
    <service id="app.form.type.callendar" class="%app.form.type.callendar.class%">
        <argument type="service" id="session" />
        <tag name="form.type" alias="my_callendar" />
    </service>


// some controller
public function somAvtion() 
{
    $form = $this->get('app.form.type.callendar');
    ...
}

// In the form
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'validation_groups' => function(FormInterface $form) {
            $session = $form->getSession();
            if ($session->getFlashBag()->get('callendar_warning', false)) {
                return array(false);
            } else {
                return array('Validate_callendar');
            }
        },
    ));
}
juanmf
  • 2,002
  • 2
  • 26
  • 28
  • I know yes, but this still doesn't help in my situation as I'd like to manipulate the validation based on user interaction. :/ – althaus Jul 14 '14 at 09:19
  • You might use session flash variables to bypass the validation if there's already a warning in last request? in that case you must use your `form as a service` injecting the container to get the flash. – juanmf Jul 14 '14 at 14:33
  • Just Edited this post to show how I'd do what I suggest in mi last comment above. – juanmf Jul 14 '14 at 14:48
  • Link is broken and for some reason "pear review" won't allow an edit.. Here's the correct link: http://symfony.com/doc/current/form/data_based_validation.html – amcastror Jul 19 '18 at 13:26
1

How does your user interact with the application to tell it to ignore the warning? Is there some kind of additional button? In that case you could simply check the button used for submitting the form or add some kind of hidden field (ignore_validation) etc. Wherever you end up getting that user input from (flash and dependency injection, based on submitted data etc.), I would then use validation groups and a closure to determine what to validate (just like juanmf explained in his answer).

RE your second approach (Form Events), you can add a priority to event listeners: As you can see in Symfony's Form Validation Event Listener, they use FormEvents::POST_SUBMIT for starting the validation process. So if you just add an event listener, it gets called before the validation listener and so no validation has happened yet. If you add a negative priority to your listener, you should be able to also access the form validation errors:

$builder->addEventListener(FormEvents::POST_SUBMIT, function(){...}, -900);
Community
  • 1
  • 1
Iris Schaffer
  • 804
  • 8
  • 18
  • I tried 3 approaches for UI: a) An additional button like "Force add". b) Just print a warning and pressing the regular "Add" button again. c) An additional check box like "[ ] Ignore warning. – althaus Dec 08 '14 at 10:20
0

Old question but...

I would first add a field (acceptCollision) in the form as suggested by you and other answers above.

Then you validator can do something like:

public function validate($appointment, Constraint $constraint)
    {
    if (null === $date = $appointment->getDate()) {
        return;
    }

    if ($appointment->getAcceptCollision()) {
       $valid = true;
       } elseif (

                 // Check Unicity of the date (no collision)
                 ) {
                    $valid = true;
       } else {
         $valid = false;
       }

    if (!$valid) {
        $this->context->addViolationAt('date', $constraint->message);
    }
}
curuba
  • 527
  • 1
  • 10
  • 29
0

I think you run into a problem because you are using the wrong concept. The decision which validation should be running belongs to the controller, not the validator.

So I would simply check in the controller which submit button is pressed (or weither there is a checkbox checked) and switch validation groups. However the form should be visually different, so I would probably create 2 forms for both states (both extend a base one or one form type that use options).

venimus
  • 5,907
  • 2
  • 28
  • 38
  • Hmm... that sounds like something I'm going to try. Have ignored the issue for the last months, but it still bothers us. – althaus Apr 17 '15 at 10:07