Extending EntityType to allow extra choices set with AJAX calls
Asked Answered
U

3

5

I try to create a Symfony Custom type extending the core "entity" type.

But I want to use it with Select2 version 4.0.0 (ajax now works with "select" html element and not with hidden "input" like before).

  • This type should create an empty select instead of the full list of entities by the extended "entity" type.

This works by setting the option (see configureOption):

'choices'=>array()
  • By editing the object attached to the form it should populate the select with the current data of the object. I solved this problem but just for the view with the following buildView method ...

Select2 recognize the content of the html "select", and does its work with ajax. But when the form is posted back, Symfony doesn't recognize the selected choices, (because there were not allowed ?)

Symfony\Component\Form\Exception\TransformationFailedException

    Unable to reverse value for property path "user": The choice "28" does not exist or is not unique

I tried several methods using EventListeners or Subscribers but I can't find a working configuration.

With Select2 3.5.* I solved the problem with form events and overriding the hidden formtype, but here extending the entitytype is much more difficult.

How can I build my type to let it manage the reverse transformation of my entites ?

Custom type :

<?php
namespace AppBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

use Symfony\Component\Form\ChoiceList\View\ChoiceView;

class AjaxEntityType extends AbstractType
{
    protected $router;

    public function __construct($router)
    {
        $this->router = $router;
    }

   /**
    * {@inheritdoc}
    */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {   
        $builder->setAttribute('attr',array_merge($options['attr'],array('class'=>'select2','data-ajax--url'=>$this->router->generate($options['route']))));
    }

    /**
    * {@inheritdoc}
    */
    public function buildView(FormView $view, FormInterface $form, array $options)
    {
        $view->vars['attr'] = $form->getConfig()->getAttribute('attr');
        $choices = array();
        $data=$form->getData();
        if($data instanceOf \Doctrine\ORM\PersistentCollection){$data = $data->toArray();}
        $values='';
        if($data != null){
            if(is_array($data)){
                foreach($data as $entity){
                    $choices[] = new ChoiceView($entity->getAjaxName(),$entity->getId(),$entity,array('selected'=>true));
                }
            }
            else{
                $choices[] = new ChoiceView($data->getAjaxName(),$data->getId(),$data,array('selected'=>true));
            }
        }

        $view->vars['choices']=$choices;
    }

   /**
    * {@inheritdoc}
    */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setRequired(array('route'));
        $resolver->setDefaults(array('choices'=>array(),'choices_as_value'=>true));
    }

    public function getParent() {
        return 'entity';
    }

    public function getName() {
        return 'ajax_entity';
    }
}

Parent form

<?php
namespace AppBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class AlarmsType extends AbstractType
{
   /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name','text',array('required'=>false))
            ->add('user','ajax_entity',array("class"=>"AppBundle:Users","route"=>"ajax_users"))
            ->add('submit','submit');
    }

    /**
     * @param OptionsResolver $resolver
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array('data_class' => 'AppBundle\Entity\Alarms','validation_groups'=>array('Default','form_user')));
    }

    /**
     * @return string
     */
    public function getName()
    {
        return 'alarms';
    }
}
Understudy answered 11/5, 2015 at 22:6 Comment(0)
U
8

Problem solved.

The solution is to recreate the form field with 'choices'=>$selectedChoices in both PRE_SET_DATA and PRE_SUBMIT FormEvents.

Selected choices can be retrived from the event with $event->getData()

Have a look on the bundle I created, it implements this method :

Alsatian/FormBundle - ExtensibleSubscriber

Understudy answered 29/5, 2015 at 19:46 Comment(0)
A
1

Here is my working code which adds to users (EntityType) related to tag (TagType) ability to fill with options from AJAX calls (jQuery Select2).

class TagType extends AbstractType
{
    //...
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $modifyForm = function ($form, $users) {
            $form->add('users', EntityType::class, [
                'class' => User::class,
                'multiple' => true,
                'expanded' => false,
                'choices' => $users,
            ]);
        };
        $builder->addEventListener(
            FormEvents::PRE_SET_DATA,
            function (FormEvent $event) use ($modifyForm) {
                $modifyForm($event->getForm(), $event->getData()->getUsers());
            }
        );
        $userRepo = $this->userRepo; // constructor injection
        $builder->addEventListener(
            FormEvents::PRE_SUBMIT,
            function (FormEvent $event) use ($modifyForm, $userRepo) {
                $userIds = $event->getData()['users'] ?? null;
                $users = $userIds ? $userRepo->createQueryBuilder('user')
                    ->where('user.id IN (:userIds)')->setParameter('userIds', $userIds)
                    ->getQuery()->getResult() : [];
                $modifyForm($event->getForm(), $users);
            }
        );
    }
    //...
}
Abstain answered 11/2, 2021 at 0:58 Comment(0)
M
0

here's my approach based on Your bundle just for entity type in one formtype. Usage is

MyType extends ExtensibleEntityType

(dont forget parent calls on build form and configure options)

and the class itself

abstract class ExtensibleEntityType extends AbstractType
{
    /**
     * @var EntityManagerInterface
     */
    private EntityManagerInterface $entityManager;

    /**
     * ExtensibleEntityType constructor.
     * @param EntityManagerInterface $entityManager
     */
    public function __construct(EntityManagerInterface $entityManager)
    {
        $this->entityManager = $entityManager;
    }

    public function getParent()
    {
        return EntityType::class;
    }

    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        parent::buildForm($builder, $options);
        $builder->addEventListener(FormEvents::PRE_SET_DATA, [$this, 'preSetData']);
        $builder->addEventListener(FormEvents::PRE_SUBMIT, [$this, 'preSubmit'], 50);
    }

    /**
     * @param FormEvent $event
     */
    public function preSetData(FormEvent $event)
    {
        $form = $event->getForm();
        $parent = $event->getForm()->getParent();
        $options = $form->getConfig()->getOptions();
        if (!$options['pre_set_called']) {
            $options['pre_set_called'] = true;
            $options['choices'] = $this->getChoices($options, $event->getData());
            $parent->add($form->getName(), get_class($this), $options);
        }
    }

    /**
     * @param FormEvent $event
     */
    public function preSubmit(FormEvent $event)
    {
        $form = $event->getForm();
        $parent = $event->getForm()->getParent();
        $options = $form->getConfig()->getOptions();
        if (!$options['pre_submit_called']) {
            $options['pre_submit_called'] = true;
            $options['choices'] = $this->getChoices($options, $event->getData());
            $parent->add($form->getName(), get_class($this), $options);
            $newForm = $parent->get($form->getName());
            $newForm->submit($event->getData());
        }
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        parent::configureOptions($resolver);
        $resolver->setDefaults([
            'multiple' => true,
            'expanded' => true,
            'choices' => [],
            'required' => false,
            'pre_set_called' => false,
            'pre_submit_called' => false,
            'validation_groups' => false,
        ]);
    }

    /**
     * @param array $options
     * @param $data
     * @return mixed
     */
    public function getChoices(array $options, $data)
    {
        if ($data instanceof PersistentCollection) {
            return $data->toArray();
        }
        return $this->entityManager->getRepository($options['class'])->findBy(['id' => $data]);
    }
}
Maracaibo answered 30/11, 2020 at 14:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.