2
votes

I had this working previously but it stopped working with Symfony 2.7

What I want is to render an expanded/multiple entity choice list such that I display multiple custom properties. The goal is to list the choices as:

{name} - {description} More info

So I created a custom form type with "entity" as parent so I could customize the form rendering

<?php
namespace Study\MainBundle\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;

class ScholarshipEntityType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->setAttribute('dataType', $options['dataType']);
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'required' => false,
            'dataType' => 'entity'
        ));
    }

    public function getAllowedOptionValues(array $options)
    {
        return array('required' => array(false));
    }

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

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

}

I render the type as follows (it was just based off of the Twitter Bootstrap bundle template):

{% block scholarship_entity_widget %}
{% spaceless %}
    {% if expanded %}
        {% set label_attr = label_attr|merge({'class': (label_attr.class|default(''))}) %}
        {% set label_attr = label_attr|merge({'class': (label_attr.class ~ ' ' ~ (widget_type != '' ? (multiple ? 'checkbox' : 'radio') ~ '-' ~ widget_type : ''))}) %}
        {% if expanded %}
            {% set attr = attr|merge({'class': attr.class|default(horizontal_input_wrapper_class)}) %}
        {% endif %}
        {% for child in form %}
            {% if widget_type != 'inline' %}
            <div class="{{ multiple ? 'checkbox' : 'radio' }}">
            {% endif %}
                <label{% for attrname, attrvalue in label_attr %} {{ attrname }}="{{ attrvalue }}"{% endfor %}>
                    {{ form_widget(child, {'horizontal_label_class': horizontal_label_class, 'horizontal_input_wrapper_class': horizontal_input_wrapper_class, 'attr': {'class': attr.widget_class|default('')}}) }}
                    {{ child.vars.label.name|trans({}, translation_domain) }}
                    - {{ child.vars.label.description }}
                    <a href="{{ child.vars.label.link }}" target="_blank">More Information</a>
                </label>
            {% if widget_type != 'inline' %}
            </div>
            {% endif %}
        {% endfor %}
        {{ block('form_message') }}
        {% if expanded %}
        {% endif %}
    {% else %}
        {# not being used, just default #}
        {{ block('choice_widget_collapsed') }}
    {% endif %}
{% endspaceless %}
{% endblock %}

Finally, I use my custom type in another form:

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        // ...
        ->add('scholarships', new ScholarshipEntityType(), array(
            'class' => 'StudyMainBundle:Scholarship',
            'query_builder' => function(EntityRepository $er) use ($options) {
                return $er->findAllByOfferingQueryBuilder($options['offering']);
            },
            'choice_label' => 'entity',
            'multiple' => true,
            'expanded' => true,
            'label' => 'financial.scholarships'
        ))
    ;
}

The "property" I'm rendering is just the entity itself:

/**
 * Scholarship
 *
 * @ORM\Table(name="scholarship")
 * @ORM\Entity(repositoryClass="Study\MainBundle\Repository\ScholarshipRepository")
 */
class Scholarship
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    // ...

    /**
     * Get the Entity object for form rendering
     * 
     * @return \Study\MainBundle\Entity\Scholarship
     */
    public function getEntity()
    {
        return $this;
    }
}

Unfortunately, it looks like my trick which was passing the entire Entity to Twig and letting me access properties is no longer working. There is some change where the label is rendered as a string (I changed 'property' to 'choice_label' above for 2.7, if that matters).

Error:

Catchable Fatal Error: Object of class Study\MainBundle\Entity\Scholarship could not be converted to string

Stack Trace:

1. in vendor/symfony/symfony/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php at line 251   + 
2. at ErrorHandler ->handleError ('4096', 'Object of class Study\MainBundle\Entity\Scholarship could not be converted to string', '/var/project/vendor/symfony/symfony/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php', '251', array('choice' => object(Scholarship), 'key' => '0', 'label' => object(Closure), 'values' => array('2'), 'index' => array('Symfony\Bridge\Doctrine\Form\Type\DoctrineType', 'createChoiceName'), 'attr' => null, 'isPreferred' => array(), 'preferredViews' => array(), 'otherViews' => array(), 'value' => '2', 'nextIndex' => '2')) 
in vendor/symfony/symfony/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php at line 251   + 
3. at DefaultChoiceListFactory ::addChoiceView (object(Scholarship), '0', object(Closure), array('2'), array('Symfony\Bridge\Doctrine\Form\Type\DoctrineType', 'createChoiceName'), null, array(), array(), array()) 
in vendor/symfony/symfony/src/Symfony/Component/Form/ChoiceList/Factory/DefaultChoiceListFactory.php at line 185

Is there another way to achieve this?

I was thinking about the following (but don't know exactly how to do these or if it's worth looking into any of them):

  • transformers
  • custom type that derives from Choice and does what I want (maybe from a bundle)
  • using the choice list factory somehow
  • passing the entity as some additional field instead of the label (maybe the new 'choice_attr'?)
2
As the choice field got heavily changed in 2.7, try a few other of the new properties. Maybe 'choices_as_values' => true will do the trick. - Yoshi
I just finished converting all of my forms to do that. It didn't help in this case (I don't think it's a default change for 2.7 but something that will be default in 3.0) - Matt
In 2.5 it was possible to use the buildView method of the field type to pass arbitrary data to the template. - Joshua

2 Answers

2
votes

If I understood correctly the problem, you should implement the __toString() function in your entity, that will format the string you want to print in the Choice list for you entity.

For example:

function __toString() {
  return sprintf("%s - %s", $this->type, $this->description);
}
0
votes

Try to use the method AbstractType::buildView(FormView, FormInterface, array). There you can access the variables that get passed to the template.

I used it for a DaterangeType to declare separate ids and names for two date fields:

public function buildView(FormView $view, FormInterface $form, array $options)
{
    $view->vars['full_name_0'] = $view->vars['full_name'] . '[0]';
    $view->vars['full_name_1'] = $view->vars['full_name'] . '[1]';

    $view->vars['id_0'] = $view->vars['id'] . '_0';
    $view->vars['id_1'] = $view->vars['id'] . '_1';
}

You can then access these values as standard twig variables.