Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

symfony2 validate a form value using another form value

In Symfony2, I use custom constraints to validate some data on my form, but I wonder if I can bring in one value from the form to use in validating another value?

Here's my constraint...

<?php
// src\BizTV\ContainerManagementBundle\Validator\Constraints\ContainerExists.php
namespace BizTV\ContainerManagementBundle\Validator\Constraints;

use Symfony\Component\Validator\Constraint;

/**
 * @Annotation
 */
class ContainerExists extends Constraint
{
    public $message = 'Namnet är upptaget, vänligen välj ett annat.';

    public function validatedBy()
    {
        return 'containerExists';
    }

}

And my validator...

<?php 
// src\BizTV\ContainerManagementBundle\Validator\Constraints\ContainerExistsValidator.php
namespace BizTV\ContainerManagementBundle\Validator\Constraints;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

use Symfony\Component\DependencyInjection\ContainerInterface as Container;
use Doctrine\ORM\EntityManager as EntityManager;

class ContainerExistsValidator extends ConstraintValidator
{

    private $container;
    private $em;

    public function __construct(Container $container, EntityManager $em) {
        $this->container = $container;
        $this->em = $em;
    }   

    public function isValid($value, Constraint $constraint)
    {

        $em = $this->em;
        $container = $this->container;

        $company = $this->container->get('security.context')->getToken()->getUser()->getCompany();

        //Fetch entities with same name
        $repository = $em->getRepository('BizTVContainerManagementBundle:Container');
        $query = $repository->createQueryBuilder('c')
            ->where('c.company = :company')
            ->setParameter('company', $company)
            ->orderBy('c.name', 'ASC')
            ->getQuery();
        $containers = $query->getResult();      

        foreach ($containers as $g) {
            if ($g->getName() == $value) {
                $this->setMessage('Namnet '.$value.' är upptaget, vänligen välj ett annat', array('%string%' => $value));
                return false;
            }
        }

        return true;
    }
}

Used through a service...

services:
  biztv.validator.containerExists:
    class: BizTV\ContainerManagementBundle\Validator\Constraints\ContainerExistsValidator
    arguments: ['@service_container', '@doctrine.orm.entity_manager']      
    tags:
        - { name: validator.constraint_validator, alias: containerExists }

Here's how I apply it like this to my entity...

<?php

namespace BizTV\ContainerManagementBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

use BizTV\ContainerManagementBundle\Validator\Constraints as BizTVAssert;

use BizTV\UserBundle\Entity\User as user;
use BizTV\ContainerManagementBundle\Entity\Container as Container;

/**
 * BizTV\ContainerManagementBundle\Entity\Container
 *
 * @ORM\Table(name="container")
 * @ORM\Entity
 */
class Container
{

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

    /**
     * @var string $name
     * @Assert\NotBlank(message = "Du måste ange ett namn") 
     * @BizTVAssert\ContainerExists
     * @ORM\Column(name="name", type="string", length=255, nullable=true)
     */
    private $name;

In my validator I would like to be able to accomplish something like this instead

public function isValid($FORM->OTHERVALUE, $value, Constraint $constraint)
{

    $em = $this->em;
    $container = $this->container;

    $company = $this->container->get('security.context')->getToken()->getUser()->getCompany();

    //Fetch entities with same name
    $repository = $em->getRepository('BizTVContainerManagementBundle:Container');
    $query = $repository->createQueryBuilder('c')
        ->where('c.company = :company')
        ->setParameter('company', $company)
        ->orderBy('c.name', 'ASC')
        ->getQuery();
    $containers = $query->getResult();      


        if ($g->getName() == $FORM->OTHERVALUE) {
            $this->setMessage('Namnet '.$value.' är upptaget, vänligen välj ett annat', array('%string%' => $value));
            return false;
        }


    return true;
}
like image 285
Matt Welander Avatar asked May 10 '13 08:05

Matt Welander


3 Answers

You can create a Class Constraint Validator (http://symfony.com/doc/master/cookbook/validation/custom_constraint.html#class-constraint-validator) and with this you get the object itself.

First you need to create a Constraint class:

class ContainerExists extends Constraint
{
    public $message = 'Namnet är upptaget, vänligen välj ett annat.';

    public function validatedBy()
    {
        return 'containerExists';
    }

    public function getTargets()
{
    return self::CLASS_CONSTRAINT;
}
}

after that create the validator itself where you have access to the object and not only a single property.

class ContainerExistsValidator extends ConstraintValidator {

    private $em;
    private $security_context;

    public function __construct(EntityManager $entityManager, $security_context) {
        $this->security_context = $security_context;
        $this->em = $entityManager;
    }

    public function validate($object, Constraint $constraint) {

        // do whatever you want with the object, and if something is wrong add a violation

                $this->context->addViolation($constraint->message, array('%string%' => 'something'));

            }

        }

    }

then create the service to access entity manager and stuff:

validator.container_exists:
        class: YourBundleName\Validator\Constraints\ContainerExistsValidator
        arguments: ["@doctrine.orm.entity_manager", "@security.context"]
        tags:
            - { name: validator.constraint_validator, alias: containerExists }

In this case as the class constraint validator is applied to the class itself, and not to the property you need to add the following annotation to your class.

use YourBundleName\Validator\Constraints as BackendAssert;

/**
 * @BackendAssert\ContainerExists
 */
like image 159
João Alves Avatar answered Nov 17 '22 13:11

João Alves


This might not be the answer you are looking for, but one possible work around would be to use form events for validation:

    $builder->addEventListener(FormEvents::POST_BIND, function (DataEvent $event) {
        $form = $event->getForm();

        // You can access all form values with $form['value']
        if ($form['name'] == $form['OTHERVALUE']) {

          $form['name']->addError(new FormError('Your Message'));

        }

    });
like image 36
Thomas K Avatar answered Nov 17 '22 14:11

Thomas K


This can be done with an Assert\Callback. In the callback you have access to the form values and can perform checks with other values. Here is a general solution:

use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Validator\Context\ExecutionContextInterface;


public static function authenticate($val1, ExecutionContextInterface $context, $payload)
{
     $testVal = null;
     $root = $context->getRoot();
     if ($root instanceof \Symfony\Component\Form\Form) {
         $testVal = $root->getViewData()['testVal'];
         if ($testVal < 5){
             $context->buildViolation('Please enter a larger value')
                 ->atPath('val1')
                 ->addViolation()
             ;
         }
     }
}


public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
     ->add('val1', TextType::class, [
                'constraints' => [
                    new Assert\Callback([$this, 'authenticate']) //this calls the method above
                ]
            ])
     ->add('testVal', TextType::class)
}

This will associate the error with the specific input field.

like image 1
Marco Gaspari Avatar answered Nov 17 '22 13:11

Marco Gaspari