Skip to content

Instantly share code, notes, and snippets.

@tasiot
Last active May 28, 2023 19:32
Show Gist options
  • Save tasiot/387c623078f6aa393f0dff51115c0543 to your computer and use it in GitHub Desktop.
Save tasiot/387c623078f6aa393f0dff51115c0543 to your computer and use it in GitHub Desktop.
Apply related entity constraints in a symfony form unmapped field

Apply the entity field constraints in a unmapped field (eg. in a DTO object)

Sometimes, you need to create a DTO object to make a form which relates many entities.

Your entity constraints are not applied in your registration form because you must declare these fields are unmapped (or mapped to an DTO object instead of your real entity).

Example

You have an entity to store username / password and another entity to store the user informations like firstName, lastName, address…

You create a form which relates your DTO object or which has some unmapped fields.

You only have to add the "constraints_from_entity" option to declare the "real" related entity and the "constraints_from_property" option to declare the "real" related entity field. If constraints_from_property is not set, it finds the same property name.

When your form is submitted, these options are read and related constraints will be applied on your field.

<?php
// App/Form/Extension/ConstraintsFromExtension.php
declare(strict_types=1);
namespace App\Form\Extension;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
class ConstraintsFromExtension extends AbstractTypeExtension
{
public function __construct(
private ValidatorInterface $validator
) {
}
public static function getExtendedTypes(): iterable
{
return [FormType::class];
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$entity = $options['constraints_from_entity'] ?? null;
$property = $options['constraints_from_property'] ?? $builder->getName();
if (null !== $entity && '' !== $property) {
$builder->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) use ($entity, $property): void {
$form = $event->getForm();
$violations = $this->validator->validatePropertyValue($entity, $property, $event->getData());
/** @var ConstraintViolationInterface $violation */
foreach ($violations as $violation) {
$form->addError(
new FormError(
$violation->getMessage(),
$violation->getMessageTemplate(),
$violation->getParameters(),
$violation->getPlural(),
$violation->getCause()
)
);
}
}
);
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefault('constraints_from_entity', null);
$resolver->setDefault('constraints_from_property', null);
$resolver->addAllowedTypes('constraints_from_entity', ['string', 'null']);
$resolver->addAllowedTypes('constraints_from_property', ['string', 'null']);
}
}
<?php
// App/Form/Model/UserRegistration.php
declare(strict_types=1);
namespace App\Form\Model;
class UserRegistration
{
public ?string $username = null;
public ?string $fullname = null;
}
<?php
// App/Form/UserRegistrationType.php
namespace App\Form;
use App\Entity\User;
use App\Entity\UserData;
use App\Form\Model\UserRegistration;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class PlayerAccountType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('username', TextType::class, [
'label' => 'Your username',
'constraints_from_entity' => User::class,
'constraints_from_property' => 'username' // property "username" in User entity
])
->add('fullname', TextType::class, [
'label' => 'Your name',
'constraints_from_entity' => UserData::class,
'constraints_from_property' => 'fullname' // property "fullname" in UserData entity
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => UserRegistration::class
]);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment