phpsymfony

Missing data after self-update of a custom Symfony sub Type


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();
}

Solution

  • 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