Dynamic Generation for Submitted Forms with Form events - symfony

I've a little problem with FormEvents, I want do 3 fields populated dynamically.
I explain, I've 3 fields: Project > Box > Cell, the user choose a Project, the Box list is updated, he choose a Box, the cell list is updated.
To do it, I use FormEvent like the documentation say (http://symfony.com/doc/current/cookbook/form/dynamic_form_modification.html#cookbook-form-events-submitted-data)
But I've a problem, for just one field dynamically updated, it's work, but no for 2 fields... Actually a user can choose a project, and when he does it, the box field is updated. But, when he choose a box, the cell field wasn't updated...
But, I've find something, who permit it to work, just change something in a ->add() and inversed to ->add(). But I don't want it.
My code is:
$builder
->add('project', EntityType::class, array(
'class' => 'AppBundle\Entity\Project',
'choice_label' => 'name',
'placeholder' => '-- select a project --',
'mapped' => false,
))
->add('box', EntityType::class, array(
'class' => 'AppBundle\Entity\Box',
'choice_label' => 'name',
'placeholder' => '-- select a box --',
'choices' => [],
))
->add('cell', ChoiceType::class, array(
'placeholder' => '-- select a cell --',
))
;
And when I change it to:
builder
->add('box', EntityType::class, array(
'class' => 'AppBundle\Entity\Box',
'choice_label' => 'name',
'placeholder' => '-- select a box --',
// 'choices' => [],
))
->add('project', EntityType::class, array(
'class' => 'AppBundle\Entity\Project',
'choice_label' => 'name',
'placeholder' => '-- select a project --',
'mapped' => false,
))
->add('cell', ChoiceType::class, array(
'placeholder' => '-- select a cell --',
))
;
It's work... But I want an empty list for box at the start, and I want project before box...
A little precision, this form is embded in an other form as a CollectionType.
All the code of this Type:
<?php
namespace AppBundle\Form;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TubeType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('project', EntityType::class, array(
'class' => 'AppBundle\Entity\Project',
'choice_label' => 'name',
'placeholder' => '-- select a project --',
'mapped' => false,
))
->add('box', EntityType::class, array(
'class' => 'AppBundle\Entity\Box',
'choice_label' => 'name',
'placeholder' => '-- select a box --',
'choices' => [],
))
->add('cell', ChoiceType::class, array(
'placeholder' => '-- select a cell --',
))
;
// MODIFIER
$boxModifier = function (FormInterface $form, $project) {
$boxes = (null === $project) ? [] : $project->getBoxes();
$form->add('box', EntityType::class, array(
'class' => 'AppBundle\Entity\Box',
'choice_label' => 'name',
'placeholder' => '-- select a box --',
'choices' => $boxes,
));
};
$cellModifier = function(FormInterface $form, $box) {
$cells = (null === $box) ? [] : $box->getEmptyCells();
$form->add('cell', ChoiceType::class, array(
'placeholder' => '-- select a cell --',
'choices' => $cells,
));
};
// FORM EVENT LISTENER
$builder->get('project')->addEventListener(
FormEvents::POST_SUBMIT,
function(FormEvent $event) use ($boxModifier) {
$project = $event->getForm()->getData();
$boxModifier($event->getForm()->getParent(), $project);
}
);
$builder->get('box')->addEventListener(
FormEvents::POST_SUBMIT,
function(FormEvent $event) use ($cellModifier) {
$box = $event->getForm()->getData();
$cellModifier($event->getForm()->getParent(), $box);
}
);
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Tube'
));
}
}
Thanks a lot to your help :)

You should use $builder->addEventListener. For multiple fields all you need to do is to have dynamic fields inside FormEvents::PRE_SET_DATA event handler. Also, use parent field data, as explained in the doc to fetch child field choices.
I have used this approach, for generating Country, State and City Entities in hierarchical fields. Let me know if it helps or you need more information.
EDIT : For bigger logic, you can use eventSubscriber which will keep your code clean and you also get to re-use dynamic part of the form for somewhere else.
For multiple dependent hierarchical fields, just add them through conditions in the eventSubscriber class.
Update with code snippet :
Here is a walk through on code snippet that worked for me in Symfony 2.7
Note : I don't replace the dynamic html field as described in the document, instead via jQuery I simply collect child options as per selected parent option and fill in those. When submitted, The form recognises correct child options as per eventSubscriber context. So here is how you might do it :
In your parent Form type (where you have all 3 fields) call a eventSubscriber instead of defining those 3 fields :
$builder->add(); // all other fields..
$builder->addEventSubscriber(new DynamicFieldsSubscriber());
Create an eventSubscriber as defined in the doc, here the file name is DynamicFieldsSubscriber
<?php
namespace YourBundle\Form\EventListener;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Doctrine\ORM\EntityRepository;
use Symfony\Component\Form\FormInterface;
class DynamicFieldsSubscriber implements EventSubscriberInterface
{
/**
* Define the events we need to subscribe
* #return type
*/
public static function getSubscribedEvents()
{
return array(
FormEvents::PRE_SET_DATA => 'preSetData', // check preSetData method below
FormEvents::PRE_SUBMIT => 'preSubmitData', // check preSubmitData method below
);
}
/**
* Handling form fields before form renders.
* #param FormEvent $event
*/
public function preSetData(FormEvent $event)
{
$location = $event->getData();
// Location is the main entity which is obviously form's (data_class)
$form = $event->getForm();
$country = "";
$state = "";
$district = "";
if ($location) {
// collect preliminary values for 3 fields.
$country = $location->getCountry();
$state = $location->getState();
$district = $location->getDistrict();
}
// Add country field as its static.
$form->add('country', 'entity', array(
'class' => 'YourBundle:Country',
'label' => 'Select Country',
'empty_value' => ' -- Select Country -- ',
'required' => true,
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('c')
->where('c.status = ?1')
->setParameter(1, 1);
}
));
// Now add all child fields.
$this->addStateField($form, $country);
$this->addDistrictField($form, $state);
}
/**
* Handling Form fields before form submits.
* #param FormEvent $event
*/
public function preSubmitData(FormEvent $event)
{
$form = $event->getForm();
$data = $event->getData();
// Here $data will be in array format.
// Add property field if parent entity data is available.
$country = isset($data['country']) ? $data['country'] : $data['country'];
$state = isset($data['state']) ? $data['state'] : $data['state'];
$district = isset($data['district']) ? $data['district'] : $data['district'];
// Call methods to add child fields.
$this->addStateField($form, $country);
$this->addDistrictField($form, $state);
}
/**
* Method to Add State Field. (first dynamic field.)
* #param FormInterface $form
* #param type $country
*/
private function addStateField(FormInterface $form, $country = null)
{
$countryCode = (is_object($country)) ? $country->getCountryCode() : $country;
// $countryCode is dynamic here, collected from the event based data flow.
$form->add('state', 'entity', array(
'class' => 'YourBundle:State',
'label' => 'Select State',
'empty_value' => ' -- Select State -- ',
'required' => true,
'attr' => array('class' => 'state'),
'query_builder' => function (EntityRepository $er) use($countryCode) {
return $er->createQueryBuilder('u')
->where('u.countryCode = :countrycode')
->setParameter('countrycode', $countryCode);
}
));
}
/**
* Method to Add District Field, (second dynamic field)
* #param FormInterface $form
* #param type $state
*/
private function addDistrictField(FormInterface $form, $state = null)
{
$stateCode = (is_object($state)) ? $state->getStatecode() : $state;
// $stateCode is dynamic in here collected from event based data flow.
$form->add('district', 'entity', array(
'class' => 'YourBundle:District',
'label' => 'Select District',
'empty_value' => ' -- Select District -- ',
'required' => true,
'attr' => array('class' => 'district'),
'query_builder' => function (EntityRepository $er) use($stateCode) {
return $er->createQueryBuilder('s')
->where('s.stateCode = :statecode')
->setParameter('statecode', $stateCode);
}
));
}
}
After this, you need to write jQuery events which should update child options on change of parent option explicitly, You shouldn't face any error on submission of the form.
Note : The code above is extracted and changed for publishing here. Take care of namespace and variables where ever required.

Related

Symfony 3: How to use two choices/dropdowns from two tables in one form

Symfony version 3.1.3
I have created a dropdown using the entity called Classes and you can see the Controller below,
public function studentAddClassAction( $id, Request $request )
{
// get the student from the student table
$em = $this->getDoctrine()->getManager();
$user = $em->getRepository('PIE10Bundle:Users')->find($id);
// new class object and create the form
$classes= $em->getRepository('PIE10Bundle:Classes')->findAll();
$form = $this->createForm(ClassType::class, $classes);
$form->handleRequest($request);
if( $form->isSubmitted() && $form->isValid() )
{
// form submit operations
}
return $this->render(
'PIE10Bundle:student:layout_student_addclass.html.twig',
array(
'user' => $user,
'title' => 'Add Class',
'tables'=> 1,
'form' => $form->createView()
)
);
}
and the ClassType is below
class ClassType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('classes',
EntityType::class,
array('class' => 'PIE10Bundle:Classes',
'expanded' => false,
'multiple' => false,));
$builder->add('Add Class',
SubmitType::class,
array('attr' => array('class' => 'btn btn-primary',
'style' => 'margin:15px 0;')) );
}
}
And this works fine and it gives all the classes from the database. Also I have another entity called Users and it has a column called roles (DC2Type:array) and it has a role called ROLE_PARENT and I can retrieve all the parents using the following query
$query = $this->getDoctrine()->getEntityManager()
->createQuery('SELECT u FROM PIE10Bundle:Users u WHERE u.roles LIKE :role')
->setParameter('role', '%"ROLE_PARENT"%' );
$users = $query->getResult();
My Question is how to add these parents list as a choice list into same above form in the studentAddClassAction Controller.
Please let me know in any other information is needed for this.
To have a custom set of entities as a choice list you need to use a query_builder option
So it will look like
$builder->add('parent',
EntityType::class,
array('class' => 'PIE10Bundle:Users',
'expanded' => false,
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('u')
->where('u.roles LIKE :role')
->setParameter('role', '%"ROLE_PARENT"%');
},
'multiple' => false
));

unable to transform value for property path "tagname". Expected a Doctrine\Common\Collections\Collection object

I am working with two ManyToMany related entities, namely category and tag.
The entity Tag(relevant details):
/**
*
* #var string
*
* #ORM\Column(name="tagname", type="string")
*/
protected $tagname;
/**
* #ORM\ManyToMany(targetEntity="Category", mappedBy="tags")
*/
protected $categories;
The entity Category(relevant details):
/**
*
* #var string
*
* #ORM\Column(name="CategoryName", type="string",length=200)
*/
protected $categoryname;
/**
* #ORM\ManyToMany(targetEntity="Tag", inversedBy="categories")
*/
protected $tags;
I have a form with a select-input(CategoryType) and a multiple select-input(TagType) fields. Both the fields are EntityType fields. The TagType is embedded inside the CatgoryType.
For this I am not able to utilise the cascade=persist functionality and am adding the submitted tags manually inside my controller. On submission the form data gets persisted in the database without any issues.
The problem is, after submission, when I fetch the submitted category(and the associated tags) in my controller, and pass it to the form, I get this error - Unable to transform value for property path "tagname": Expected a Doctrine\Common\Collections\Collection object.
The var_dump result of the fetched category object(var_dump($category->getTags()->getValues());) gives me an array of the associated Tag objects, with the property protected 'tagname' => string 'tag1'.
From what I understand Interface Collection is quite similar to a php array and My guess is that the tagname field expects all the tagnames in an ArrayCollection or Collection object format. I am not sure whether what is the specific difference.
However I am still clueless how do I pass the already persisted category object in my form.
Here are the categoryname and tags field in the CategoryType:
$builder->add('categoryname', EntityType::class, array(
'class' => 'AppBundle:Category',
'query_builder' => function(EntityRepository $er) {
return $er->createQueryBuilder('c')
->orderBy('c.id', 'ASC');
},
'choice_label' => 'categoryname',
'expanded' => false,
'multiple' => false,
'label' => 'Choose Category',
));
$builder->add('tags', CollectionType::class, array(
'entry_type' => TagType::class,
'allow_add' => true,
'by_reference' => false,
'allow_delete' => true,
));
Here is the embedded tagname field in the TagType:
$builder->add('tagname', EntityType::class, array(
'class' => 'AppBundle:Tag',
'query_builder' => function(EntityRepository $er) {
return $er->createQueryBuilder('t')
->orderBy('t.id', 'ASC');
},
'choice_label' => 'tagname',
'expanded' => false,
'multiple' => true,
'label' => 'Choose Tags',
));
Any ideas?
Try to rid off 'multiple' => true in embedded form. This worked for me.
I've had a similar problem where my EntityType form element gave me this error message:
Unable to reverse value for property path 'my_property' Expected an array.
Setting multiple to false would make the implementation useless, so a better option for me was to set the empty_data to an empty array:
"empty_data" => [],
In your case you might be able to solve the problem by setting the empty_data to a Collection, something like this will probably work:
"empty_data" => new ArrayCollection,
Here is how you do it (from source: https://www.youtube.com/watch?v=NNCpj4otnrc):
***And here is a text version of this image #1, the reason I did not add text because it was hard to read when the format is not proper!
<?php
namespace App\Form;
use App\Entity\Post;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType;
class PostType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', TextType::class, [
'attr' => [
'placeholder' => 'Etner the title here',
'class' => 'custom_class'
]
])
->add('description', TextareaType::class, [
'attr' => [
'placeholder' => 'Enter teh description here',
]
])
->add('category', EntityType::class, [
'class' => 'App\Entity\Category'
])
->add('save', SubmitType::class, [
'attr' => [
'class' => 'btn btn-success'
]
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Post::class,
]);
}
}
***And here is a text version of this image #2, the reason I did not add text because it was hard to read when the format is not proper!
class FormController extends AbstractController
{
/**
* #Route("/form", name="form")
* #param Request $request
* #return Response
*/
public function index(Request $request)
{
$post = new Post(); // exit('this');
/*$post->setTitle('welcome');
$post->setDescription('description here');*/
$form = $this->createForm(PostType::class, $post, [
'action' => $this->generateUrl('form'),
'method' => 'POST'
]);
// handle the request
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid())
{
// var_dump($post); die();
// save to database
$em = $this->getDoctrine()->getManager();
$em->persist($post);
$em->flush();
}
return $this->render('form/index.html.twig', [
'postaform' => $form->createView()
]);
}
My assumption was wrong, I need to either make tagname field an array or create a ManyToOne or ManyToMany relationship with any other entity so it can be an arraycollection. Only then it is possible to use tagname as a multiple select field.
/**
*
* #var array
*
* #ORM\Column(name="tagname", type="array")
*/
protected $tagname;
or
/**
* #ORM\ManyToMany(targetEntity="SOME_ENTITY", mappedBy="SOME_PROPERTY")
*/
protected $tagname;
or
/**
* #ORM\ManyToOne(targetEntity="SOME_ENTITY", mappedBy="SOME_PROPERTY")
*/
protected $tagname;
The exception thrown is pretty explicit, and the problem is probably here:
$builder->add('tagname', EntityType::class, array()
According to your Tag entity, Tag::$tagname is not an entity collection, it's a string. You should add the property with
$builder->add('tagname', TextType::class, array()
shouldn't you ?

Symfony Dynamic Modification of Form without Entity

I am following the Symfony (v2.7) Cookbook recipe for dynamic form modification. What I am aiming for is displaying certain fields based on a user's radio button selection. For example, if a user wishes to filter a search based on records from the last fiscal year, he selects the "Fiscal Year" radio button from the criteriaFilter choice field type (example below), and the appropriate fields are generated. If he changes his mind and selects "Semester" instead, the fiscal year fields are replaced with the semester fields, and so on.
Example code:
$builder
->add('librarian', 'entity', array(
'class' => 'AppBundle:Staff',
'query_builder' => function(EntityRepository $er){
$qb = $er->createQueryBuilder('st');
$qb
->where('st.employmentStatus = :employmentStatus')
->setParameter('employmentStatus', 'faclib')
->orderBy('st.lastName', 'DESC')
->getQuery();
return $qb;
},
'placeholder' => 'All Librarians',
'required' => false
))
->add('program', 'entity', array(
'class' => 'AppBundle:LiaisonSubject',
'query_builder'=>function(EntityRepository $er){
$qb = $er->createQueryBuilder('ls');
$qb
->orderBy('ls.root, ls.lvl, ls.name', 'ASC')
->getQuery();
return $qb;
},
'property' => 'indentedTitle',
'placeholder' => 'All Programs',
'required' => false,
'label' => 'Program'
))
->add('criteriaFilter', 'choice', array(
'expanded' => true,
'multiple' => false,
'choices' => array(
'academic' => 'Academic Year',
'fiscal' => 'Fiscal Year',
'semester' => 'Semester',
'custom' => 'Custom Range'
),
))
;
This seems pretty straighforward based on the cookbook entry. However, the form I am creating is not bound to an entity. Therefore, fetching data via the method
$builder->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event){
$form = $event->getForm();
//normally the entity, but NULL in this case
$data = $event->getData();
...
which would normally allow for calling of getter methods on entity properties returns null. So obviously this can't work in this case.
So the question is, is there another way to dynamically generate fields inside of a form that is not tied to an entity?
You can pass options to the form, including data. So something like (from memory but it's untested):
// controller
$this->createForm(SomeForm::class, null, ['fiscalYears' => [2001, 2002]);
// type
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(['fiscalyears' => []);
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$fiscalYears = $options['fiscalYears'];
$builder->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event) use ($fiscalYears) {
$form = $event->getForm();
$form->add('fiscalYear', ChoiceType::class, [
'choices' => $fiscalYears
]);
}
}

Symfony 2: Set field as read only after first save

I have a Symfony 2 entity. When I create a new record, I must fill all the values using a form, but after saving it, one of the values, $amount shouldn't be updatable when I update the others members.
How can I accomplish this? It's possible to mark a form member as a read-only, in runtime?
By using the validation_groups and name options when creating your form, you can change the form.
The name attribute sets the form creation, and the validation_groups takes care of the validation.
For example, in the create/new method of your controller;
public function createAction(Request $request)
{
// Instantiate new Foo object
$client = new Foo();
// create the form (setting validation group)
$form = $this->formFactory->create('foo', $foo, array(
'name' => 'create',
'validation_groups' => array('create')
)
);
// form has been submitted...
if ('POST' === $request->getMethod()) {
// submits the form
$form->handleRequest($request);
// do validation
if ($form->isValid()) {
// do whatever
}
}
// either GET or validation failed, so show the form
return $this->template->renderResponse('FooBundle:foo:add.html.twig', array(
'form' => $form->createView(),
'foo' => $foo
));
}
And in the edit/update function of your controller;
public function updateAction($id, Request $request)
{
// Instantiate Client object
$client = new Foo($id);
// create the form (setting validation group)
$form = $this->formFactory->create('foo', $foo, array(
'name' => 'update',
'validation_groups' => array('update')
));
// form has been submitted...
if ('POST' === $request->getMethod()) {
// submits the form
$form->handleRequest($request);
// do validation
if ($form->isValid()) {
// do whatever
}
}
// either GET or validation failed, so show the form
return $this->template->renderResponse('FooBundle:foo/edit:index.html.twig', array(
'form' => $form->createView(),
'foo' => $foo
));
}
And your Form Type will look something like;
class FooType extends BaseAbstractType
{
protected $options = array(
'data_class' => 'FooBundle\Model\Foo',
'name' => 'foo',
);
private $roleManager;
public function __construct($mergeOptions = null)
{
parent::__construct($mergeOptions);
}
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$this->$options['name']($builder, $options);
}
private function create(FormBuilderInterface $builder, array $options)
{
// ID
$builder->add('Id', 'text', array(
'required' => true,
'label' => 'ID',
'attr' => array(
'placeholder' => 'Format: 2 alphanumeric (e.g. A1)'
)
));
// Name - only show on create
$builder->add('Name', 'text', array(
'required' => true,
'label' => 'Name',
'attr' => array(
'placeholder' => 'Your name'
)
));
// add the submit form button
$builder->add('save', 'submit', array(
'label' => 'Save'
));
}
private function update(FormBuilderInterface $builder, array $options)
{
// ID
$builder->add('Id', 'text', array(
'required' => true,
'label' => 'ID',
'attr' => array(
'placeholder' => 'Format: 2 alphanumeric (e.g. A1)',
)
));
// Name - just for show
$builder->add('Name', 'text', array(
'required' => true,
'label' => 'Name',
'attr' => array(
'readonly' => 'true' // stops it being editable
)
));
// add the submit form button
$builder->add('save', 'submit', array(
'label' => 'Save'
));
}
}
P.S. All my classes are declared as services, so how you call create forms/views/etc may be different.

add class to subset of symfony checkboxes

I've made a form in Symfony that is not bound to an entity. One of the form elements is a set of checkboxes, fieldtypes. Which fieldtype checkboxes show should depend on the value of the searchby field. I want to add a class to each checkbox to use as a show/hide hook for the javascript I will add later. But so far as I can see, the choices form element only allows for an overall class to be applied.
Here's the relevant bit of my form:
$form = $this->createFormBuilder()
->add('searchby', 'choice', array(
'label' => 'browse.label.searchby',
'choices'=>array(
'name' => 'browse.searchby.name',
'location' => 'browse.searchby.location',
'classification' => 'browse.searchby.classification'
),
'required' => true,
'multiple' => false,
'expanded' => true,
))
->add('fieldtypes', 'choice', array(
'label' => 'browse.label.fieldtypes',
'choices' => array(
'extensionAttribute12' => 'person.label.position.fr',
'title' => 'person.label.position.eng',
'l' => 'person.label.city',
'st' => 'person.label.province',
'co' => 'person.label.country',
'givenname' => 'person.label.firstname',
'sn' => 'person.label.lastname',
'name' => 'person.label.fullname',
),
'required' => true,
'multiple' => true,
'expanded' => true
));
If I want to add the class 'searchbyname' to the radiobuttons created from $options['choices']['givenname'], $options['choices']['sn'], $options['choices']['name'], how would I go about it?
After seeing that choices could be declared like this:
'choices' => array(
'classification' => array(
'extensionAttribute12' => 'person.label.position.fr',
'title' => 'person.label.position.eng',
),
'location' => array(
'l' => 'person.label.city',
'st' => 'person.label.province',
'co' => 'person.label.country',
),
'name' => array(
'givenname' => 'person.label.firstname',
'sn' => 'person.label.lastname',
'name' => 'person.label.fullname',
)
),
where the key to each sub array would be used as an <optgroup> label in a <select>; and after attempting to modify the Twig template (which was every bit a painful as #Cerad said it would be), I tried extending the ChoiceType class by creating a form type extension.
My solution is inefficient since I'm modifying the child views after they are created and because I had to include all the code from ChoiceType::finishView. I can't see how a child view is created. There is a line in ChoiceType::buildForm that reads $remainingViews = $options['choice_list']->getRemainingViews();, but since $options['choices'] was input as an array, I don't know what class getRemainingViews() is being called from.
At any rate, here it is:
<?php
namespace Expertise\DefaultBundle\Form\Extension;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class ChoiceTypeExtension extends AbstractTypeExtension
{
/**
* #return string The name of the type being extended
*/
public function getExtendedType()
{
return 'choice';
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setOptional(array('optgroup_as_class'));
$resolver->setDefaults(array('optgroup_as_class'=>false));
}
public function finishView(FormView $view, FormInterface $form, array $options)
{
if ($options['expanded']) {
// Radio buttons should have the same name as the parent
$childName = $view->vars['full_name'];
// Checkboxes should append "[]" to allow multiple selection
if ($options['multiple']) {
$childName .= '[]';
}
foreach ($view as $childView) {
$childView->vars['full_name'] = $childName;
if($options['optgroup_as_class']){
foreach($options['choices'] as $optclass => $choices){
if(!is_array($choices)) continue;
foreach($choices as $value => $label){
if($childView->vars['value'] == $value && $childView->vars['label'] == $label) {
$childView->vars['attr']['class'] = $optclass;
break 2;
}
}
}
}
}
}
}
}
Add it as a service, use the "optgroup" choices format and set optgroup_as_class to true.
I'd love to see a more efficient method.

Resources