Task: registration form, among other things, there are three drop-down lists. In the first country is selected, then in the second Ajax'om loaded regions and, when choosing a region, in the third loaded cities. All three fields load data from three models: Country, Region, City.

buildForm code:

 $builder ->add('country', EntityType::class, ['class' => 'AppBundle:Country', 'choice_label' => 'name', 'placeholder' => '--- Выберите страну ---']) ->add('region', RegionSelectorType::class, [ 'required' => false]) ->add('city', EntityType::class, ['class' => 'AppBundle:City', 'choice_label' => 'name', 'placeholder' => '--- Выберите город ---', 'required' => false]) ->add('post_code', null, ['required' => false]) ->add('address', null, ['required' => false]) 

The country is loaded immediately from the entity, there are few of them. But for the regions and cities, I wanted to make a dynamic load so as not to waste a lot of data.

While stalled in the region, the city will be on the same principle. The problem is that when the form is submitted, the field does not transform, by ID , into the essence. Tried and Using Transformer , Creating a Reusable issue_selector Field , is written in the profiler

Unable to reverse value for property path "region": The choice of "15"

The code for my RegionSelectorType type RegionSelectorType :

 namespace AppBundle\Form; use AppBundle\Form\DataTransformer\RegionToNumberTransformer; use Doctrine\Common\Persistence\ObjectManager; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; class RegionSelectorType extends AbstractType { private $manager; public function __construct(ObjectManager $manager) { $this->manager = $manager; } public function buildForm(FormBuilderInterface $builder, array $options) { $transformer = new RegionToNumberTransformer($this->manager); $builder->addModelTransformer($transformer); } public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults(array( 'invalid_message' => 'Выбранный регион не существует', )); } public function getParent() { return ChoiceType::class; } } 

Transformer code:

 namespace AppBundle\Form\DataTransformer; use AppBundle\Entity\Region; use Doctrine\Common\Persistence\ObjectManager; use Symfony\Component\Form\DataTransformerInterface; use Symfony\Component\Form\Exception\TransformationFailedException; class RegionToNumberTransformer implements DataTransformerInterface { private $manager; public function __construct(ObjectManager $manager) { $this->manager = $manager; } /** * Transforms an object (region) to a string (number). * * @param Region|null $region * @return string */ public function transform($region) { if (null === $region) { return ''; } return $region->getId(); } /** * Transforms a string (number) to an object (region). * * @param string $regionNumber * @return Region|null * @throws TransformationFailedException if object (region) is not found. */ public function reverseTransform($regionNumber) { // no region number? It's optional, so that's ok if (!$regionNumber) { return; } $region = $this->manager ->getRepository('AppBundle:Region') // query for the issue with this id ->find($regionNumber) ; if (null === $region) { // causes a validation error // this message is not shown to the user // see the invalid_message option throw new TransformationFailedException(sprintf( 'An region with number "%s" does not exist!', $regionNumber )); } return $region; } } 

Service is registered as:

 services: app.form.type.region_selector: class: AppBundle\Form\RegionSelectorType arguments: ['@doctrine.orm.entity_manager'] tags: - { name: form.type, alias: region_selector } 

In general, all the manual. The reverseTransform() function does not work at all. It is only ChoiceType::class to replace ChoiceType::class with TextType::class in getParent() , as in the example everything works: reverseTransform() works, the entity is created. With EntityType instead of my type, everything also works. I create a region of type EntityType , in the choices an empty array, - an error. That is, the EntityType must be present in the form field, which will then be returned by the submit.

The first thing that comes to mind is to create an EventListеner , catch the value transmitted by ChoiceType and create an entity on the fly, but why then DataTransformer ?

I use 3 symfony, but I think the second one will have roughly the same problems and solution.

  • In Symfony2, there were problems with the fact that you can’t just use the choiceType for objects without crutches. Have you tried to return from getParent EntityType :: class? Maybe this will not be the solution to your problem, but if the form works , it will be a small advance - AmsTaFFix

1 answer 1

but then why DataTransformers?

DataTransformer have nothing to do with it.

The first thing that comes to mind is creating an EventListener

As you yourself noticed, you can create an EventListеner . But you don't have to use ChoiceType , you can also use EntityType

Below are some code examples:

Registration form:

 namespace AppBundle\Form; use AppBundle\Entity\City; use AppBundle\Entity\Country; use AppBundle\Entity\Region; use Doctrine\ORM\EntityRepository; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormInterface; class RegistrationForm extends AbstractType { public function buildForm( FormBuilderInterface $builder, array $options ) { $builder ->add('country', EntityType::class, [ 'class' => Country::class, ]) ->add('region', EntityType::class, [ 'class' => Region::class, 'query_builder' => function(EntityRepository $er) { $qb = $er->createQueryBuilder('r') ->where('r.country IS NULL') ; return $qb; }, ]) ->add('city', EntityType::class, [ 'class' => City::class, 'query_builder' => function(EntityRepository $er) { $qb = $er->createQueryBuilder('c') ->where('c.region IS NULL') ; return $qb; }, ]) ; $builder->addEventListener(FormEvents::POST_SET_DATA, function(FormEvent $event) { $data = $event->getData(); $form = $event->getForm(); /** @var $country Country */ $country = $data['country']; $country = $country ? $country->getId() : false; /** @var $region Region */ $region = $data['region']; $region = $region ? $region->getId() : false; $this->modifyForm($form, $country, $region); }); $builder->addEventListener(FormEvents::POST_SUBMIT, function(FormEvent $event) { $data = $event->getData(); $form = $event->getForm(); /** @var $country integer */ $country = (int)$data['country']; $region = (int)$data['region']; $this->modifyForm($form, $country, $region); }); } protected function modifyForm(FormInterface $form, $country, $region = null) { if ( $country ) { $form->add('region', EntityType::class, [ 'class' => Region::class, 'query_builder' => function(EntityRepository $er) use ( $country ) { $qb = $er->createQueryBuilder('r') ->where('r.country = :country') ->setParameter('country', $country) ->orderBy('r.name') ; return $qb; } ]); if ( $region ) { $form->add('city', EntityType::class, [ 'class' => City::class, 'query_builder' => function(EntityRepository $er) use ( $region ) { $qb = $er->createQueryBuilder('c') ->where('c.region = :region') ->setParameter('region', $region) ->orderBy('r.name') ; return $qb; } ]); } } } } 

By default, the regions and cities will be empty, achieved this through the parameter quiery_builder .

If a country is specified, then we catch it in the eventListener , and change the query_builder to get the regions of this country. For cities the same.

I think you can handle the rest.