In Symfony, I'm trying to create a Type based on EntityType. The idea is to have an EntityType field whose options are loaded via AJAX (using the Selectize library). However, once an option has been selected, I need to dynamically add it to my Type. To do this, I use an EventListener on FormEvents::POST_SUBMIT
to update my field with the option. Unfortunately, the field is no longer present in $form->getData()
after the submit.
My Type :
class ClientEntityType extends AbstractType
{
public function __construct(
private readonly ClientsRepository $clientsRepository,
private readonly RouterInterface $router,
) {
}
public function getParent(): string
{
return EntityType::class;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults($this->getOptions());
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event): void {
$data = $event->getData();
$form = $event->getForm();
$options = $this->getOptions();
if ($data) {
$client = $this->clientsRepository->find($data);
$choices = [];
$choices[(string) $client] = $client;
$options['choices'] = $choices;
$options['data'] = $client;
}
$form->getParent()->add(
$form->getName(),
self::class,
$options
);
});
}
private function getOptions(): array
{
return [
'class' => Clients::class,
'label' => 'Client',
'required' => false,
'attr' => [
'class' => 'clientSelect',
'data-search-url' => $this->router->generate('admin_client_search'),
],
'placeholder' => 'Choisir une société',
'choices' => [],
];
}
}
My form type :
class InvoiceListType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
// ...
->add('client', ClientEntityType::class);
}
}
My controller :
$form = $this->createForm(InvoiceListType::class, $data);
if (
$form->handleRequest($request)->isSubmitted()
&& $form->isValid()
) {
$data = $form->getData();
$data['client'] // Is missing!
// But I get the value with:
$form->get('client')->getData();
}
Thanks to Ali Rasouli, who pointed me in the direction of the custom ChoiceLoader. It was the solution I missed while trying to reinvent the wheel...
So, the final solution:
class ClientEntityType extends AbstractType
{
public function __construct(
private readonly RouterInterface $router,
) {
}
public function getParent(): string
{
return EntityType::class;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'label' => 'Client',
'class' => Clients::class,
'attr' => [
'class' => 'clientSelect form-control',
'data-search-url' => $this->router->generate('admin_client_search'),
],
'choice_loader' => function (Options $options, $loader) {
return new ExtraLazyChoiceLoader($loader);
},
'placeholder' => 'Choisir un client',
]);
}
}
And the new ExtraLazyChoiceLoader class that I found in the "symfony/ux-autocomplete" project:
class ExtraLazyChoiceLoader implements ChoiceLoaderInterface
{
private ?ChoiceListInterface $choiceList = null;
public function __construct(
private readonly ChoiceLoaderInterface $decorated,
) {
}
public function loadChoiceList(?callable $value = null): ChoiceListInterface
{
return $this->choiceList ??= new ArrayChoiceList([], $value);
}
public function loadChoicesForValues(array $values, ?callable $value = null): array
{
$choices = $this->decorated->loadChoicesForValues($values, $value);
$this->choiceList = new ArrayChoiceList($choices, $value);
return $choices;
}
public function loadValuesForChoices(array $choices, ?callable $value = null): array
{
$values = $this->decorated->loadValuesForChoices($choices, $value);
$this->loadChoicesForValues($values, $value);
return $values;
}
}
Note that when I upgrade the project to Symfony 7.4, I may be able to directly use the new `choice_lazy` option introduced in Symfony 7.2. https://symfony.com/doc/current/reference/forms/types/choice.html#choice-lazy