Symfony2 : Radio buttons in a collection

前端 未结 4 626
说谎
说谎 2021-02-07 13:39

In my application, I created a form using the collection field type :

$builder->add(\'tags\', \'collection\', array(
   \'type\' => new TagTyp         


        
4条回答
  •  深忆病人
    2021-02-07 14:17

    You're not tackling the problem from the right angle. If there should be a main tag, then this property should not be added in the Tag entity itself, but in the entity that contains it!

    I'm speaking of the data_class entity related to the form having the tags attribute. This is the entity that should have a mainTag property.

    If defined properly, this new mainTag attribute will not be a boolean, for it will contain a Tag instance, and thus will not be associated to a checkbox entry.

    So, the way I see it, you should have a mainTag property containing your instance and a tags property that conatins all other tags.

    The problem with that is that your collection field will no longer contain the main tag. You should thus also create a special getter getAllTags that will merge your main tag with all others, and change your collection definition to:

    $builder->add('allTags', 'collection', array(
        'type' => new TagType(),
        'label' => false,
        'allow_add' => true,
        'allow_delete' => true,
        'by_reference' => false
    ));
    

    Now, how do we add the radio boxes, you may ask? For this, you will have to generate a new field:

    $builder->add('mainTag', 'radio', array(
        'type' => 'choice',
        'multiple' => false,
        'expanded' => true,
        'property_path' => 'mainTag.id', // Necessary, for 'choice' does not support data_classes
    ));
    

    These are the basics however, it only grows more complex from here. The real problem here is how your form is displayed. In a same field, you mix the usual display of a collection and the display of a choice field of the parent form of that collection. This will force you to use form theming.

    To allow some room to reusability, you need to create a custom field. The associated data_class:

    class TagSelection
    {
        private mainTag;
    
        private $tags;
    
        public function getAllTags()
        {
            return array_merge(array($this->getMainTag()), $this->getTags());
        }
    
        public function setAllTags($tags)
        {
            // If the main tag is not null, search and remove it before calling setTags($tags)
        }
    
        // Getters, setters
    }
    

    The form type:

    class TagSelectionType extends AbstractType
    {
        protected buildForm( ... )
        {
            $builder->add('allTags', 'collection', array(
                'type' => new TagType(),
                'label' => false,
                'allow_add' => true,
                'allow_delete' => true,
                'by_reference' => false
            ));
    
            // Since we cannot know which tags are available before binding or setting data, a listener must be used
            $formFactory = $builder->getFormFactory();
            $listener = function(FormEvent $event) use ($formFactory) {
    
                $data = $event->getForm()->getData();
    
                // Get all tags id currently in the data
                $choices = ...;
                // Careful, in PRE_BIND this is an array of scalars while in PRE_SET_DATA it is an array of Tag instances
    
                $field = $this->factory->createNamed('mainTag', 'radio', null, array(
                    'type' => 'choice',
                    'multiple' => false,
                    'expanded' => true,
                    'choices' => $choices,
                    'property_path' => 'mainTag.id',
                ));
                $event->getForm()->add($field);
            }
    
            $builder->addEventListener(FormEvent::PRE_SET_DATA, $listener);
            $builder->addEventListener(FormEvent::PRE_BIND, $listener);
        }
    
        public function getName()
        {
            return 'tag_selection';
        }
    
        public function setDefaultOptions(OptionsResolverInterface $resolver)
        {
            $resolver->setDefaults(array(
                'data_class' => 'TagSelection', // Adapt depending on class name
                // 'prototype' => true,
            ));
       }
    }
    

    Finally, in the form theme template:

    {% block tag_selection_widget %}
        {% spaceless %}
        {# {% set attr = attr|default({})|merge({'data-prototype': form_widget(prototype)}) %} #}
        
      {% for child in form.allTags %}
    • {{ form_widget(form.mainTag[child.name]) }} {{ form_widget(child) }}
    • {% endfor %}
    {% endspaceless %} {% endblock tag_selection_widget %}

    Lastly, we need to include that in your parent entity, the one that originally contained tags:

    class entity
    {
        // Doctrine definition and whatnot
        private $tags;
    
        // Doctrine definition and whatnot
        private $mainTag;
    
        ...
        public setAllTags($tagSelection)
        {
            $this->setMainTag($tagSelection->getMainTag());
            $this->setTags($tagSelection->getTags());
        }
    
        public getAllTags()
        {
            $ret = new TagSelection();
            $ret->setMainTag($this->getMainTag());
            $ret->setTags($this->getTags());
    
            return $ret;
        }
    
        ...
    }
    

    And in your original form:

    $builder->add('allTags', new TagSelection(), array(
        'label' => false,
    ));
    

    I recognize the solution I propose is verbose, however it seems to me to be the most efficient. What you are trying to do cannot be done easily in Symfony.

    You can also note that there is an odd "prototype" option in the comment. I just wanted to underline a very useful property of "collection" in your case: the prototype option contains a blank item of your collection, with placeholders to replace. This allow to quickly add new items in a collection field using javascript, more info here.

提交回复
热议问题