I had to upgrade one of my projects from symfony 2.8 to symfony 3.4, and I noticed a huge change in the validation process.
To simplify let's say I have a User entity, with many Addresses entities. When I create / update my User I want to be able to add / remove / update any number of addresses. So in symfony 2.8 I had this kind of situation
User
I use annotation validators
src/AppBundle/Entity/User.php
//...
class User
{
//...
/**
* @Assert\Count(min=1, max=10)
* @ORM\OneToMany(targetEntity="AppBundle\Entity\Address", mappedBy="user", cascade={"persist", "remove"})
*/
protected $addresses;
//...
}
UserForm
src/AppBundle/Form/UserForm.php
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// ...
->add('addresses', CollectionType::class, [
'type' => AddressType::class,
'cascade_validation' => true,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
])
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults([
'data_class' => User::class,
'cascade_validation' => true,
'validation_groups' => // User's logic
]);
}
Address
src/AppBundle/Entity/Address.php
//...
class Address
{
//...
/**
* @ORM\ManyToOne(targetEntity="AppBundle\Entity\User", inversedBy="user")
*/
protected $user;
/**
* @Assert\NotBlank(groups={"zipRequired"})
* @ORM\Column(type="text", nullable="true")
*/
protected $zipCode;
//...
}
AddressForm
src/AppBundle/Form/AddressForm.php
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
// ...
->add('zipCode', TextType::class)
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults([
'data_class' => Address::class,
'cascade_validation' => true,
'validation_groups' => function(FormInterface $form) {
/** @var Address $data */
$data = $form->getData();
$validation_groups = [];
// Simplified here, it's a service call with heavy logic
if ($data->doesRequireZip()) {
$validation_groups[] = 'zipRequired';
}
return $validation_groups;
},
]);
}
In symfony 2.8
On 3 addresses added, two have to valid the zipRequired group, one not. I works !
In symfony 3.4
I added @Assert\Valid()
to User::$zipCode declaration and removed the 'cascade_validation' => true
(not in method configureOptions but it seems unused) as it's deprecated.
But now on 3 addresses added, two have should to valid the zipRequired group, and one not : Only User's class validator_groups are used, so I can valid a form with incoherent data !
I checked with xdebug and the validator_groups
callback in AddressForm
is called but validators are not.
I tested the solutions decribed here : Specify different validation groups for each item of a collection in Symfony 2? but it can't work anymore as in symfony 3.4 cascade_validation
on a property throws an error
In my situation the logic involved is too heavy to use a solution ad described here Specify different validation groups for each item of a collection in Symfony 3? as it is very inneficient to rewrite the whole validation_groups
callback in individual methods and it apply the groups on all child entities.
The Behaviour of @Assert\Valid
and cascade_validation
are different, is there a way to handle embed form individual entity validation_groups in symfony 3.4 or the feature is definitely gone ?
As this is an expected behavior (the entire form should be validated from the root validation_groups, as you can see the explanation here: https://github.com/symfony/symfony/issues/31441) the only way i could find to solve this problem is using an callback validation inside the collection item (entity):
src/AppBundle/Entity/Address.php
//...
class Address
{
//...
/**
* @ORM\ManyToOne(targetEntity="AppBundle\Entity\User", inversedBy="user")
*/
protected $user;
/**
* @ORM\Column(type="text", nullable="true")
*/
protected $zipCode;
//...
/**
* @Assert\Callback()
*/
public function validate(): void {
if ($data->doesRequireZip()) {
// validate if $zipCode isn't null
// other validations ...
}
}
}
more about symfony callback assert: https://symfony.com/doc/current/reference/constraints/Callback.html
this solution was made in symfony 5.1, but this probably works from 2.8+
just to add my final solution was adding the validation throug the validator, forcing each property against an specific group (also you dont need to force Default if your form already did it).
/**
* @Assert\Callback()
*/
public function validate(ExecutionContext $context): void
{
$validator = $context->getValidator();
$groups = $this->doesRequireZip() ? ['requiredZipGroup'] : [];
$violations = $validator->validate($this, null, $groups);
/** @var \Symfony\Component\Validator\ConstraintViolationInterface $violation */
foreach ($violations as $violation) {
$context->buildViolation($violation->getMessage(), $violation->getParameters())
->atPath($violation->getPropertyPath())
->addViolation();
}
}
passing null
to second argument on $validator->validate()
will force the Assert\Valid
to run through the object $this
, so, all constraints and also callbacks within the $groups
will run
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With