Symfony2 - Validation not working for embedded Form Type

PhpSymfony

Php Problem Overview


I have a form that combines two entities (User and Profile).

Validation seems to work on the first part of the form that comes form the User Entity and is the basis of the form.

The ProfileType is included inside the UserType. The form renders correctly and displays the correct information, so it seems it is properly connected to the Profile entity. It's just the validation that is broken on the ProfileType.

Any idea as to why one part would validate and the other wouldn't?

Code below:

Validation.yml

DEMO\DemoBundle\Entity\User\Profile:
    properties:
        address1:
            - NotBlank: { groups: [profile] }
        name:
            - NotBlank: { groups: [profile] }
        companyName:
            - NotBlank: { groups: [profile] }

DEMO\DemoBundle\Entity\User\User:
    properties:
        username:
            - NotBlank:
                groups: profile
                message: Username cannot be left blank.
        email:
            - NotBlank:
                groups: profile
                message: Email cannot be left blank
            - Email:
                groups: profile
                message: The email "{{ value }}" is not a valid email.
                checkMX: true
        password:
            - MaxLength: { limit: 20, message: "Your password must not exceed {{ limit }} characters." }
            - MinLength: { limit: 4, message: "Your password must have at least {{ limit }} characters." }
            - NotBlank: ~

UserType.php

namespace DEMO\DemoBundle\Form\Type\User;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackValidator;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\FormError;

use DEMO\DemoBundle\Form\Type\User\ProfileType;

class UserType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder->add('username');
        $builder->add('email');
        $builder->add('profile', new ProfileType());
    }
    
    public function getDefaultOptions(array $options)
    {
        return array(
            'data_class' => 'DEMO\DemoBundle\Entity\User\User',
            'validation_groups' => array('profile')
        );
    }
    
    public function getName()
    {
        return 'user';
    }
}

ProfileType.php

namespace DEMO\DemoBundle\Form\Type\User;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackValidator;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\FormError;

class ProfileType extends AbstractType
{
    public function buildForm(FormBuilder $builder, array $options)
    {
        $builder->add('name');
        $builder->add('companyName', null, array('label' => 'Company Name'));
        $builder->add('address1', null, array('label' => 'Address 1'));
        $builder->add('address2', null, array('label' => 'Address 2'));
        $builder->add('city');
        $builder->add('county');
        $builder->add('postcode');
        $builder->add('telephone');
    }

    public function getDefaultOptions(array $options)
    {
        return array(
            'data_class' => 'DEMO\DemoBundle\Entity\User\Profile',
        );
    }

    public function getName()
    {
        return 'profile';
    }
}

Controller

$user = $this->get('security.context')->getToken()->getUser();

        $form = $this->createForm(new UserType(), $user);

        if ($request->getMethod() == 'POST') {
            $form->bindRequest($request);

            if ($form->isValid()) {
                // Get $_POST data and submit to DB
                $em = $this->getDoctrine()->getEntityManager();
                $em->persist($user);
                $em->flush();

                // Set "success" flash notification
                $this->get('session')->setFlash('success', 'Profile saved.');
            }

        }

        return $this->render('DEMODemoBundle:User\Dashboard:profile.html.twig', array('form' => $form->createView()));

Php Solutions


Solution 1 - Php

I spent an age searching and found that it was adding 'cascade_validation' => true to the setDefaults() array in my parent type's class that fixed it (as mentioned already in the thread). This causes the entity constraint validation to trigger in the child types shown in the form. e.g.

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(            
        ...
        'cascade_validation' => true,
    ));
}

For collections, also make sure to add 'cascade_validation' => true to the $options array for the collection field on the form. e.g.

$builder->add('children', 'collection', array(
    'type'         => new ChildType(),
    'cascade_validation' => true,
));

This will have the UniqueEntity validation take place as it should in the child entity used in the collection.

Solution 2 - Php

A note to those using Symfony 3.0 and up: the cascade_validation option has been removed. Instead, use the following for embedded forms:

$builder->add('embedded_data', CustomFormType::class, array(
    'constraints' => array(new Valid()),
));

Sorry for adding to this old thread with a slightly off-topic answer (Symfony 3 vs. 2), but finding this information here would have saved me a few hours today.

Solution 3 - Php

According to form type documentation you can also use Valid constraint instead of cascade_validation option.

$builder->add('children', 'collection', array(
    'type'        => new ChildType(),
    'constraints' => array(new Valid()),
));

Example from the owner entity:

/**
 * @var Collection
 *
 * @ORM\OneToMany(targetEntity="Child", ...)
 * @Assert\Valid()
 */
private $children

Solution 4 - Php

I was looking for the exact same thing and here is what I found

http://symfony.com/doc/master/book/forms.html#forms-embedding-single-object

You need to tell the main entity to validate it's sub entities like so:

/**
 * @Assert\Type(type="AppBundle\Entity\Category")
 * @Assert\Valid()
 */
 private $subentity;

I've tested this on symfony 2.8 and it works.

Solution 5 - Php

are you using YML or Annotations?

I tried applying the cascade_validation option on my parent form class, but validation was still not occurring. After reading a little documentation, I went to app/config/config.yml and found that enable_annotations under framework->validation was set to true. Apparently, if this is true, the validation service no loner reads any validation.yml files. So I just changed it to false, and now the form is validating fine.

Solution 6 - Php

Belongs to Symfony 2.3

Working with embedded forms and validation groups could be quite painful: Annotation @Assert\Valid() doesen't work for me (without groups it is ok). Insert 'cascade_validation' => true on the DefaultOptions is the key. You do not need to repeat this on the ->add(). Take care: HTML 5 validation do not work together with validation groups.

Example:

A Collection of 2 Addresses. Both 1:1 unidirectional. Each with a different (!) validation group.

  class TestCollection{

//(...)

/**
 * @var string
 * @Assert\NotBlank(groups={"parentValGroup"})
 * @ORM\Column(name="name", type="string", length=255, nullable=true)
 */
protected $name;

/**
 * @var \Demo\Bundle\Entity\TestAddress	 
 * @Assert\Type(type="Demo\Bundle\Entity\TestAddress")
 * @ORM\OneToOne(targetEntity="TestAddress",cascade={"persist","remove"},orphanRemoval=true)
 * @ORM\JoinColumn(name="billing_address__id", referencedColumnName="id")
 */
protected $billingAddress;

/**
 * @var \Demo\Bundle\Entity\TestAddress
 * @Assert\Type(type="Demo\Bundle\Entity\TestAddress")
 * @ORM\OneToOne(targetEntity="TestAddress",cascade={"persist","remove"}, orphanRemoval=true)
 * @ORM\JoinColumn(name="shipping_address__id", referencedColumnName="id")
 */	
protected $shippingAddress;

//(...)
}

Address Entity

class TestAddress {
/**
 * @var string
 * @Assert\NotBlank(groups={"firstname"})
 * @ORM\Column(name="firstname", type="string", length=255, nullable=true)
 */
private $firstname;

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

/**
 * @var string
 * @Assert\Email(groups={"firstname","lastname"}) 
 * @ORM\Column(name="email", type="string", length=255, nullable=true)
 */
private $email;

Address type - ability to change validation group

class TestAddressType extends AbstractType {	
protected $validation_group=['lastname'];//switch group

public function __construct($validation_group=null) {
	if($validation_group!=null) $this->validation_group=$validation_group;
}

public function buildForm(FormBuilderInterface $builder, array $options)
{
	//disable html5 validation: it suchs with groups 
	
    $builder
        ->add('firstname',null,array('required'=>false))
        ->add('lastname',null,array('required'=>false))
        ->add('email',null,array('required'=>false))
	;
}

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'data_class' => 'Demo\Bundle\Entity\TestAddress',			
		'validation_groups' => $this->validation_group,
    ));
}
(...)

And last the CollectionType

class TestCollectionType extends AbstractType {	

public function buildForm(FormBuilderInterface $builder, array $options)
{	$builder
		->add('name')           
		->add('billingAddress', new TestAddressType(['lastname','firstname']))
		->add('shippingAddress', new TestAddressType(['firstname']))            
    ;
}

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'data_class' => 'Demo\Bundle\Entity\TestCollection',
		'validation_groups' => array('parentValGroup'),			
		'cascade_validation' => true
    ));
}

//(...)    

Hope it helps..

Solution 7 - Php

You have to add validation_groups in your ProfiletType also. Validation is done in each form type separately based on their data_class if exists.

Solution 8 - Php

From my controller:

$form = $this->get('form.factory')
        ->createNamedBuilder('form_data', 'form', $item, array('cascade_validation' => true))
        ->add('data', new ItemDataType())
        ->add('assets', new ItemAssetsType($this->locale))
        ->add('contact', new ItemContactType())
        ->add('save', 'submit',
            array(
                'label' => 'Save',
                'attr' => array('class' => 'btn')
            )
        )
        ->getForm();

Fourth parametr in ::createNamedBuilder - array('cascade_validation' => true))

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionMr PabloView Question on Stackoverflow
Solution 1 - Phpdaftv4derView Answer on Stackoverflow
Solution 2 - Phpcg.View Answer on Stackoverflow
Solution 3 - PhpsupernovaView Answer on Stackoverflow
Solution 4 - PhpnacholibreView Answer on Stackoverflow
Solution 5 - PhptargnationView Answer on Stackoverflow
Solution 6 - PhpHaukeView Answer on Stackoverflow
Solution 7 - PhpMun Mun DasView Answer on Stackoverflow
Solution 8 - Phpd3uterView Answer on Stackoverflow