22

I'm creating a form in Symfony2. The form only contains one book field which allows the user to choose between a list of Books entities. I need to check whether the selected Book belongs to an Author I have in my controller.

public class MyFormType extends AbstractType
{
    protected $author;

    public function __construct(Author $author) {
        $this->author = $author;
    }

    public function buildForm(FormBuilderInterface $builder, array $options) {
        $builder->add('book', 'entity', array('class' => 'AcmeDemoBundle:Book', 'field' => 'title');
    }

    // ...
}

I want to check, after submitting the form, that the selected Book is written by the $author in my controller:

public class MyController
{
    public function doStuffAction() {
        $author = ...;
        $form = $this->createForm(new MyFormType($author));
        $form->bind($this->getRequest());

        // ...
    }
}

Unfortunately, I cannot find any way to do that. I tried creating a custom validator constraint as explained in The Cookbook, but while I can pass the EntityManager as parameter by defining the validator as a service, I cannot pass the $author from the controller to the validator constraint.

class HasValidAuthorConstraintValidator extends ConstraintValidator
{
    private $entityManager;

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

    public function validate($value, Constraint $constraint) {
        $book = $this->entityManager->getRepository('book')->findOneById($value);
        $author = ...; // That's the data I'm missing

        if(!$book->belongsTo($author))
        {
            $this->context->addViolation(...);
        }
    }
}

This solution could be exactly the one that I was looking for, but my form is not bound to an Entity and is not meant to be (I'm getting the data from the getData() method).

Is there a solution to my problem ? This must be a common case but I really don't know how to solve it.

4 Answers 4

32

I finally figured it out, with the help from Cerad. To inject custom parameters that need to be accessed from the ConstraintValidator::validate() method, you need to pass them as options in the Constraint.

public class HasValidAuthorConstraint extends Constraint
{
    protected $author;

    public function __construct($options)
    {
        if($options['author'] and $options['author'] instanceof Author)
        {
            $this->author = $options['author'];
        }
        else
        {
            throw new MissingOptionException("...");
        }
    }

    public function getAuthor()
    {
        return $this->author;
    }
}

And, in the ConstraintValidator:

class HasValidAuthorConstraintValidator extends ConstraintValidator
{
    private $entityManager;

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

    public function validate($value, Constraint $constraint) {
        $book = $this->entityManager->getRepository('book')->findOneById($value);
        $author = $this->constraint->getAuthor();

        if(!$book->isAuthor($author))
        {
            $this->context->addViolation(...);
        }
    }
}

Last but not least, you have to pass the parameter to the Validator:

public function buildForm(FormBuilderInterface $builder, array $options) {
    $builder->add('book', 'entity', array(
        'class' => 'AcmeDemoBundle:Book',
        'field' => 'title',
        'constraints' => array(
            new HasValidAuthorConstraint(array(
                'author' => $this->author
            ))
        )
    ));
}
Sign up to request clarification or add additional context in comments.

2 Comments

Note that in Symfony 2.7, the custom parameter is not found directly in the $options array, but it is inside a 'value' key. So for this example, you would get the author from $options['value']['author']. And if you declared the assert using annotations you would write something like: @CustomAssert\CustomDateTime({"dateFormat" : Enum::DATE_FORMAT})
You should define the $author property public so you can call parent::__construct($options); in the constructor.
3

Start by adding a setAuthor method to your constraint and then tweaking the validate method. The trick then is to determine the best place to call it.

It's not clear exactly how you are binding the validator to your book. Are you using validation.yml or doing something inside of the form?

1 Comment

Thank you for your answer, injecting the Author into the Constraint did the trick ! I was trying to inject the Author into the ValidatorConstraint which did not work !
1

Well, I'm not that familier with the Form/Validation component, but you can use a Hidden field with the name/id of the author and check if it's the same:

class MyFormType extends AbstractType
{
    protected $author;

    public function __construct(Author $author) {
        $this->author = $author;
    }

    public function buildForm(FormBuilderInterface $builder, array $options) {
        $builder
            ->add('book', 'entity', array('class' => 'AcmeDemoBundle:Book', 'field' => 'title');
            ->add('author_name', 'hidden', array(
                'data' => $this->author->getId(),
            ))
        ;
    }

    // ...
}

2 Comments

Thank you for your answer, but I don't think I would be able to do this for security reasons. Indeed, the user could change the author field in the form to match any other author, thus bypassing the restriction.
And thank you for editing my question, I did not know about Markdown syntax highlighting :)
-2

Accepted answer did not work for me using Symfony Framework version 2.1. This is how I solved it.

class CustomConstraint extends Constraint
{
    public $dependency;
    public $message = 'The error message.';
}

class CustomConstraintValidator extends ConstraintValidator
{
    public function validate($value, Constraint $constraint)
    {
        if (!$constraint->dependency->allows($value)) {
            $this->context->addViolation($constraint->message);
        }
    }
}

class CustomFormType extends AbstractType
{
    private $dependency;

    public function __construct(Dependency $dependency)
    {
        $this->dependency = $dependency;
    }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('field', 'type', array(
                'constraints' => array(
                    new CustomConstraint(array('dependency' => $this->dependency))
                )
        ));
    }
} 

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.