Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Extending EntityType to allow extra choices set with AJAX calls

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';
    }
}
like image 278
Alsatian Avatar asked May 11 '15 22:05

Alsatian


2 Answers

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

like image 139
Alsatian Avatar answered Oct 20 '22 19:10

Alsatian


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);
            }
        );
    }
    //...
}
like image 38
Mateusz Piechnat Avatar answered Oct 20 '22 19:10

Mateusz Piechnat