Reputation: 157
I have ran into, at least for me, quite strange Symfony behaviour. I have form type with nested form. I need to validate this nested form only in some circumstances.
That's why I added validation groups to Valid() constraint. If I use same group for eg. TextType, it works as expected. If I remove groups from Valid() it also works. So it means Valid() constraint itself works, it just don't work when validation groups are used.
$builder
->add('test', TextType::class, [
'mapped' => false,
'constraints' => [new NotBlank(['groups' => 'SenderAddress'])],
])
->add('senderAddress', AddressType::class, [
'label' => false,
'constraints' => [new Valid(['groups' => 'SenderAddress'])],
])
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'validation_groups' => function (FormInterface $form) {
if (!$form->get('senderAddresses')->getData()) {
return ['Default', 'SenderAddress'];
}
return ['Default'];
},
]);
}
So if nothing is filled in senderAddresses and
But when I try to remove group validation for senderAddress it works as expected. What am I doing wrong?
->add('senderAddress', AddressType::class, [
'label' => false,
'constraints' => [new Valid()], //but this is not how I want it!
])
EDIT: These are the objects I work with
Entity Order for CalculationType
<?php
namespace App\Entity;
use App\Repository\OrderRepository;
use App\Traits\IdentifierTrait;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity(repositoryClass=OrderRepository::class)
* @ORM\Table(name="`order`")
*/
class Order
{
use IdentifierTrait;
public const STATUSES = [
'order.statuses.calculation' => self::STATUS_CALCULATION,
'order.statuses.created' => self::STATUS_CREATED,
];
public const STATUS_CALCULATION = 1;
public const STATUS_CREATED = 2;
/**
* @ORM\ManyToOne(targetEntity="Address", cascade={"persist"})
*
* @var Address
*/
private Address $senderAddress;
/**
* @ORM\ManyToOne(targetEntity="Address", cascade={"persist"})
*
* @var Address
*/
private Address $recipientAddress;
/**
* @ORM\ManyToOne(targetEntity="Package", cascade={"persist"})
* @Assert\Valid()
*
* @var Package
*/
private Package $package;
/**
* @ORM\Column(type="datetime")
*
* @var DateTime
*/
private DateTime $createdAt;
/**
* @ORM\Column(type="integer", length=1, options={"unsigned": true})
*
* @var int
*/
protected int $status = self::STATUS_CALCULATION;
/**
* @ORM\Column(type="text", length=255, nullable=TRUE)
*
* @var null|string
*/
private ?string $serviceType = null;
/**
* @ORM\ManyToOne(targetEntity=User::class)
*
* @var User
*/
private User $user;
/**
* Order constructor.
*/
public function __construct()
{
$this->createdAt = new DateTime('now');
}
/**
* @return Address
*/
public function getSenderAddress(): Address
{
return $this->senderAddress;
}
/**
* @param Address $senderAddress
*/
public function setSenderAddress(Address $senderAddress): void
{
$this->senderAddress = $senderAddress;
}
/**
* @return Address
*/
public function getRecipientAddress(): Address
{
return $this->recipientAddress;
}
/**
* @param Address $recipientAddress
*/
public function setRecipientAddress(Address $recipientAddress): void
{
$this->recipientAddress = $recipientAddress;
}
/**
* @return Package
*/
public function getPackage(): Package
{
return $this->package;
}
/**
* @param Package $package
*/
public function setPackage(Package $package): void
{
$this->package = $package;
}
/**
* @return string|null
*/
public function getServiceType(): ?string
{
return $this->serviceType;
}
/**
* @param string|null $serviceType
*/
public function setServiceType(?string $serviceType): void
{
$this->serviceType = $serviceType;
}
/**
* @return DateTime
*/
public function getCreatedAt(): DateTime
{
return $this->createdAt;
}
/**
* @param DateTime $createdAt
*/
public function setCreatedAt(DateTime $createdAt): void
{
$this->createdAt = $createdAt;
}
/**
* @return int
*/
public function getStatus(): int
{
return $this->status;
}
/**
* @param int $status
*/
public function setStatus(int $status): void
{
$this->status = $status;
}
/**
* @return User
*/
public function getUser(): User
{
return $this->user;
}
/**
* @param User $user
*/
public function setUser(User $user): void
{
$this->user = $user;
}
}
Entity Address for AddressType
<?php
namespace App\Entity;
use App\Repository\AddressRepository;
use App\Traits\IdentifierTrait;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Intl\Countries;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity(repositoryClass=AddressRepository::class)
*/
class Address
{
use IdentifierTrait;
public const TYPES = [
'address.types.sender' => self::TYPE_SENDER,
'address.types.recipient' => self::TYPE_RECIPIENT,
];
public const TYPE_SENDER = 1;
public const TYPE_RECIPIENT = 2;
/**
* @ORM\ManyToOne(targetEntity=User::class, inversedBy="addresses")
*
* @var User|null
*/
private ?User $user;
/**
* @ORM\Column(type="string", length=100, nullable=true)
* @Assert\Length(
* max="100"
* )
*
* @var string|null
*/
private ?string $name = null;
/**
* @ORM\Column(type="string", length=150)
* @Assert\NotBlank()
* @Assert\Length(
* max="150"
* )
*
* @var string|null
*/
private ?string $street = null;
/**
* @ORM\Column(type="string", length=100)
* @Assert\NotBlank()
* @Assert\Length(
* max="100"
* )
*
* @var string|null
*/
private ?string $city = null;
/**
* @ORM\Column(type="string", length=15)
* @Assert\NotBlank()
* @Assert\Length(
* max="15"
* )
*
* @var string|null
*/
private ?string $postalCode = null;
/**
* @ORM\Column(type="string", length=3)
* @Assert\NotBlank()
* @Assert\Length(
* max="3"
* )
*
* @var string|null
*/
private ?string $country = null;
/**
* @ORM\Column(type="string", length=3, nullable=true)
*
* @var string|null
*/
private ?string $province = null;
/**
* @ORM\Column(type="integer", length=1, options={"unsigned": true})
*
* @var int
*/
protected int $type = self::TYPE_SENDER;
/**
* Address constructor.
*
* @param int $type
* @param User|null $user
*/
public function __construct(int $type = self::TYPE_SENDER, User $user = null)
{
$this->type = $type;
$this->user = $user;
}
/**
* @return User|null
*/
public function getUser(): ?User
{
return $this->user;
}
/**
* @param User|null $user
*/
public function setUser(?User $user): void
{
$this->user = $user;
}
/**
* @return string|null
*/
public function getName(): ?string
{
return $this->name;
}
/**
* @param string|null $name
*/
public function setName(?string $name): void
{
$this->name = $name;
}
/**
* @return string|null
*/
public function getStreet(): ?string
{
return $this->street;
}
/**
* @param string|null $street
*/
public function setStreet(?string $street): void
{
$this->street = $street;
}
/**
* @return string|null
*/
public function getCity(): ?string
{
return $this->city;
}
/**
* @param string|null $city
*/
public function setCity(?string $city): void
{
$this->city = $city;
}
/**
* @return string|null
*/
public function getPostalCode(): ?string
{
return $this->postalCode;
}
/**
* @param string|null $postalCode
*/
public function setPostalCode(?string $postalCode): void
{
$this->postalCode = $postalCode;
}
/**
* @return string|null
*/
public function getCountry(): ?string
{
return $this->country;
}
/**
* @param string|null $country
*/
public function setCountry(?string $country): void
{
$this->country = $country;
}
/**
* Get country name
*/
public function getCountryName(): string
{
return Countries::getName($this->country);
}
/**
* @return string|null
*/
public function getProvince(): ?string
{
return $this->province;
}
/**
* @param string|null $province
*/
public function setProvince(?string $province): void
{
$this->province = $province;
}
/**
* Get full description
*
* @return string
*/
public function getFullDescription(): string
{
return ($this->name ? ($this->name . ' - ') : '') . $this->street . ', ' . $this->postalCode . ' ' . $this->city . ', ' . $this->getCountryName();
}
/**
* @return int
*/
public function getType(): int
{
return $this->type;
}
/**
* @param int $type
*/
public function setType(int $type): void
{
$this->type = $type;
}
}
CalculationType
<?php
namespace App\Form;
use App\Entity\Address;
use App\Entity\User;
use Doctrine\ORM\EntityRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints\Valid;
class CalculationType extends AbstractType
{
/**
* @var UserInterface|User|null
*/
private ?UserInterface $user;
/**
* CalculationFormType constructor.
*
* @param Security $security
*/
public function __construct(Security $security)
{
$this->user = $security->getUser();
}
/**
* @param FormBuilderInterface $builder
* @param array $options
*
*/
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('senderAddress', AddressType::class, [
'label' => false,
'constraints' => [new Valid(['groups' => 'NotNullAddress'])], //todo doesnt work
])
->add('recipientAddress', AddressType::class, [
'label' => false,
'constraints' => [new Valid(['groups' => 'NotNullAddress'])], //todo doesnt work
'show_name' => false,
])
->add('package', PackageType::class, ['label' => false])
->add('calculate', SubmitType::class, [
'label' => 'order.calculate',
]);
$this->addressesField($builder, 'senderAddresses', Address::TYPE_SENDER);
$this->addressesField($builder, 'recipientAddresses', Address::TYPE_RECIPIENT);
}
/**
* @param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'validation_groups' => static function (FormInterface $form) {
if (!$form->get('senderAddresses')->getData()) {
return ['Default', 'NotNullAddress'];
}
return ['Default'];
},
]);
}
/**
* Addresses field
*
* @param FormBuilderInterface $builder
* @param string $childName
* @param int $type
*/
private function addressesField(FormBuilderInterface $builder, string $childName, int $type): void
{
$builder->add($childName, EntityType::class, [
'class' => Address::class,
'mapped' => false,
'required' => false,
'placeholder' => 'order.newAddress',
'choice_label' => 'fullDescription',
'query_builder' => function (EntityRepository $er) use ($type) {
return $er->createQueryBuilder('address')
->andWhere('address.user = :user')
->andWhere('address.type = ' . $type)
->setParameter('user', $this->user);
},
'attr' => [
'class' => 'select-existing-address',
],
]);
}
}
AddressType
<?php
namespace App\Form;
use App\Entity\Address;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CountryType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class AddressType extends AbstractType
{
/**
* @param FormBuilderInterface $builder
* @param array $options
*
* @throws \JsonException
*/
public function buildForm(FormBuilderInterface $builder, array $options): void
{
if ($options['show_name']) {
$builder->add('name');
}
$builder->add('street')
->add('city')
->add('postalCode')
->add('country', CountryType::class, [
'preferred_choices' => ['CZ', 'DE', 'IT', 'SK'],
])
->add('province');
}
/**
* @param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Address::class,
'show_name' => true,
]);
}
}
Upvotes: 1
Views: 2129
Reputation: 111
I just solved this problem for a form that I am building. I am adding this answer here for future users that might need a solution to this problem. When validating an embedded object, you have to add the validation group to that object's properties. I use annotations for this, so the Address entity should look like this (notice the groups option added to each property that needs validation):
<?php
namespace App\Entity;
use App\Repository\AddressRepository;
use App\Traits\IdentifierTrait;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Intl\Countries;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity(repositoryClass=AddressRepository::class)
*/
class Address
{
use IdentifierTrait;
public const TYPES = [
'address.types.sender' => self::TYPE_SENDER,
'address.types.recipient' => self::TYPE_RECIPIENT,
];
public const TYPE_SENDER = 1;
public const TYPE_RECIPIENT = 2;
/**
* @ORM\ManyToOne(targetEntity=User::class, inversedBy="addresses")
*
* @var User|null
*/
private ?User $user;
/**
* @ORM\Column(type="string", length=100, nullable=true)
* @Assert\Length(
* max="100",
* groups={"SenderAddress"}
* )
*
* @var string|null
*/
private ?string $name = null;
/**
* @ORM\Column(type="string", length=150)
* @Assert\NotBlank(groups={"SenderAddress"})
* @Assert\Length(
* max="150",
* groups={"SenderAddress"},
* )
*
* @var string|null
*/
private ?string $street = null;
/**
* @ORM\Column(type="string", length=100)
* @Assert\NotBlank(groups={"SenderAddress"})
* @Assert\Length(
* max="100",
* groups={"SenderAddress"}
* )
*
* @var string|null
*/
private ?string $city = null;
/**
* @ORM\Column(type="string", length=15)
* @Assert\NotBlank(groups={"SenderAddress"})
* @Assert\Length(
* max="15"
* groups={"SenderAddress"}
* )
*
* @var string|null
*/
private ?string $postalCode = null;
/**
* @ORM\Column(type="string", length=3)
* @Assert\NotBlank(groups={"SenderAddress"})
* @Assert\Length(
* max="3"
* groups={"SenderAddress"}
* )
*
* @var string|null
*/
private ?string $country = null;
/**
* @ORM\Column(type="string", length=3, nullable=true)
*
* @var string|null
*/
private ?string $province = null;
/**
* @ORM\Column(type="integer", length=1, options={"unsigned": true})
*
* @var int
*/
protected int $type = self::TYPE_SENDER;
// the rest of the entity....
}
The Order entity should only have the Valid annotation for the sender address:
/**
* @ORM\Entity(repositoryClass=OrderRepository::class)
* @ORM\Table(name="`order`")
*/
class Order
{
use IdentifierTrait;
public const STATUSES = [
'order.statuses.calculation' => self::STATUS_CALCULATION,
'order.statuses.created' => self::STATUS_CREATED,
];
public const STATUS_CALCULATION = 1;
public const STATUS_CREATED = 2;
/**
* @Assert\Valid()
* @ORM\ManyToOne(targetEntity="Address", cascade={"persist"})
* @var Address
*/
private Address $senderAddress;
// the rest of the entity
The form should work with the modifications. More details here
Upvotes: 0