V
V
Vitaly Sorokin2018-05-06 16:43:20
symfony
Vitaly Sorokin, 2018-05-06 16:43:20

How to create Symfony 2 cascading form?

Good day everyone. There is a task: to make a symfony form with three fields Country, Region, City.
These data are selected from drop-down lists. Accordingly, the options in the lists Region and City. appear only after selection in the previous field.
From the JS side, everything is clear and works, but when submitting the form, there is a gag, namely:
Everything is implemented as indicated here: symfony.com.ua/doc/current/form/dynamic_form_modif... Section "Dynamic generation for the submitted form". And with the "Region" field, there are no questions - everything works, but the POST_SUBMIT event for the region does not work.
Here is the fom code:

<?php

class MyInputType extends AbstractType
{

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        /** @var MyInput $data */
        $data = $builder->getData();
        $builder->add('country', 'entity', [
            'class' => Country::class,
            'choices' => $data->getAvailableCountries(),
            'required' => false,
        ]);

        $this->addRegionPole($builder,$data->getAvailableRegions($data->getCountry()));
        $this->addCityPole($builder,$data->getAvailableCities($data->getCountry(), $data->getRegion()));

        $this->iniPOST_SUBMIT_event($builder);
    }

    private function iniPOST_SUBMIT_event(FormBuilderInterface $builder)
    {
        $context = $this;
        $builder->get('country')->addEventListener(
            FormEvents::POST_SUBMIT,
            function (FormEvent $event) use ($context) {
                /** @var Country $data */
                $data = $event->getForm()->getData();
                $form = $event->getForm()->getParent();
                $context->addRegionPole($form,$form->getData()->getAvailableRegions($data));
            }
        );

        $builder->get('region')->addEventListener(
            FormEvents::POST_SUBMIT,
            function (FormEvent $event) use ($context) {
                /** @var Region $data */
                $region = $event->getForm()->getData();
                $form = $event->getForm()->getParent();
                /** @var Country $country */
                $country = $form->get('country')->getData();

                /** @var MyInput $MyInput */
                $MyInput = $form->getData();

                $context->addCityPole($form,$MyInput->getAvailableCities($country,$region));
            }
        );
    }

    /**
     * @param FormInterface $form
     * @param array $choices
     */
    protected function addRegionPole($form, array $choices)
    {
        $form->add('region', 'entity', [
      'class' => Region::class,
            'choices' => $choices,
            'required' => false,
        ]);
    }

    /**
     * @param FormInterface | FormBuilderInterface $form
     * @param array $choices
     */
    protected function addCityPole($form, array $choices)
    {
        $form->add('city', 'entity', [
            'class' => City::class,
            'choices' => $choices,
            'required' => false,
        ]);
    }

    /**
     * @param OptionsResolverInterface $resolver
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults([
            'data_class' => MyInput::class,
            'csrf_protection' => false,
        ]);
    }

    /**
     * Returns the name of this type.
     *
     * @return string The name of this type
     */
    public function getName()
    {
        return 'MyInputType';
    }
}

Answer the question

In order to leave comments, you need to log in

2 answer(s)
D
Denis, 2018-05-06
@SorokinWS

Everything is much simpler and of course there are a lot of approaches.
The classic approach does NOT imply js and adds new fields after each form submission, the solution is extremely simple:

<?php

interface ObjectInterface {
    public function getCountry(): ?Country;
    public function getRegion(): ?Region;
}

class AppType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('country', EntityType::class, [
                'class' => \App\Entity\Country::class,
            ])
        ;

        $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
            /** @var ObjectInterface $object */
            if (null === $object = $event->getData()) {
                return;
            }
            $form = $event->getForm();

            if ($object->getCountry()) {
                $form->add('region', EntityType::class, [
                    'class' => \App\Entity\Region::class,
                    'query_builder' => function (EntityRepository $repository) use ($object) {
                        return $repository->createQueryBuilder('e')->where('e.country = :country')->setParameter('country', $object->getCountry());
                    }
                ]);
            }

            if ($object->getRegion()) {
                $form->add('city', EntityType::class, [
                    'class' => \App\Entity\City::class,
                    'query_builder' => function (EntityRepository $repository) use ($object) {
                        return $repository->createQueryBuilder('e')->where('e.region = :region')->setParameter('region', $object->getRegion());
                    }
                ]);
            }
        });
    }
}

Question price 3 POST requests
But this approach is not convenient and only confuses.
How to decide?
One of the ways will be the following:
- Create a controller with methods for obtaining regions, cities.
- On the front, draw the input by any means
- Dynamically (on the front) do anything with these forms
- Submit the form
In this case, the symfony form will look primitive
It will only be necessary to set a validation group, where you will calmly validate the entire tree, starting with the country.
Question price - 2 ajax requests, 1 POST
Or...
Draw the form
Where region and city - ChoiceType are empty.
With Ajax we take regions from the server, and then cities.
In the form event (SUBMIT and POST_SUBMIT with a lower validator priority), we manually reset this data.
Personally, I prefer the first approach (classic without js), on which buns are then hung.
There are more requests, but the object is collected, both on the front and on the back, sequentially, and there are no problems with editing or creating.
For understanding. PRE_SET_DATA is a GET request, prefix before data resolution (this is why the code checks for null). PRE_SUBMIT is a POST request. In both cases, you have an object in the event.
Accordingly, data_class is responsible for the sequence, a DTO object (or entity) in which there will be a country, region and city.
Bonus (having dealt with this heresy) - validators, all sorts of rests and serializers are screwed to such forms once or twice as needed.
> Tried - does not work. It does not include the values ​​of dynamic fields.
I will answer your comment here.
Firstly, the PRE_SET_DATA event with checking the data for null
Secondly, the PRE_SUBMIT event (optionally) for data binding (if your form does not use entities)
symfony.com/doc/current/form/dynamic_form_modifica...

V
voronkovich, 2018-05-06
@voronkovich

You are dynamically redefining the field for the region, thereby "overwriting" the event listener. Those. you need to add the listener again:

$builder->get('country')->addEventListener(
            FormEvents::POST_SUBMIT,
            function (FormEvent $event) use ($context) {
                /** @var Country $data */
                $data = $event->getForm()->getData();
                $form = $event->getForm()->getParent();

                // !!! Здесь слушатель события для поля 'region' удаляется !!!
                $context->addRegionPole($form,$form->getData()->getAvailableRegions($data));
            }
        );

Perhaps you should move the code with hooking handlers to the addRegionPole and addCityPole methods:

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question