How can I use UploadField within GridFieldEditableColumns in SilverStripe 4? - silverstripe

I have a problem with GridFieldEditableColumns. I extended EditableOption with a list of files, and now I want the CMS columns to display an upload field. I took the EditableMultipleOptionField as a reference and implemented it like this:
$editableColumns = new GridFieldEditableColumns();
$editableColumns->setDisplayFields([
'Title' => [
'title' => 'Beschreibung',
'callback' => function ($record, $column, $grid) {
return TextField::create($column);
}
],
'Value' => [
'title' => 'Wert',
'callback' => function ($record, $column, $grid) {
return TextField::create($column);
}
],
'Default' => [
'title' => 'Standardmäßig aktiv?',
'callback' => function ($record, $column, $grid) {
return CheckboxField::create($column);
}
],
'Examples' => [
'title' => 'Beispielbilder',
'callback' => function ($record, $column, $grid) {
return UploadField::create($column);
}
]
]);
However, I always get errors when I use UploadField:
[Emergency] Uncaught LogicException: Field must be associated with a form to call Link(). Please use $field->setForm($form);
I also tried it with ['field' => UploadField::class], same problem. The extension works, because if I change the field for the DataObject to type Text and use a TextField instead, it works fine. How can I use an UploadField within GridFieldEditableColumns?

Related

How to add text that there are no search results for such entered word to a custom block?

I have a view that searches for indexed entity fields using context filters. I added a custom block to the view like this:
{{ drupal_block('result_entity_product_categories', {arguments}) }}
This block displays categories that match the entered word in the search. If you enter something for which there are no search results, for example, bbbbb, I need to display something like this:
Sorry
No results for: "bbbbb"
But here are some of our most popular products
P.S. The option to add text to the No Results Behavior view setting is not suitable. It is necessary to add text in the custom block.
The build() method code of my custom block:
public function build() {
$configuration = $this->getConfiguration();
$term = $configuration['arguments']['0'] ?: '';
if (empty($term)) {
return '';
}
$index = $this->entityTypeManager->getStorage('search_api_index')->load('entity_product_index');
$parse_mode = $this->parseModeManager->createInstance('terms');
$parse_mode->setConjunction('AND');
$search_query = $index->query();
$search_query->setParseMode($parse_mode)
->keys($term);
$search_result = $search_query->execute();
$rows = [];
foreach ($search_result->getResultItems() as $item) {
if (($node = $item->getOriginalObject()->getEntity()) && ($node instanceof NodeInterface)) {
$categoryKey = $node->get('field_entity_product_category')->getString();
if ($categoryKey) {
++$rows[$categoryKey];
}
}
}
$build['container'] = [
'#type' => 'container',
'#attributes' => [
'class' => ['category-counter-wrapper'],
],
];
foreach ($rows as $key => $count) {
if ($node = $this->entityTypeManager->getStorage('node')->load($key)) {
$build['container'][$key] = [
'#type' => 'container',
'#attributes' => [
'class' => ['item'],
],
'label' => [
'#type' => 'container',
'#markup' => $node->getTitle(),
'#attributes' => [
'class' => ['label'],
],
],
'count' => [
'#type' => 'container',
'#markup' => $count,
'#attributes' => [
'class' => ['count'],
],
],
'link' => [
'#type' => 'link',
'#url' => Url::fromUserInput($node->get('field_custom_url')->getString(), ['query' => ['text' => $term]]),
'#attributes' => [
'class' => ['link'],
],
],
];
}
}
return $build;
}

How to apply InputFilter validators to fieldset elements in ZF3

I had a form that had two fields. An InputFilter with validators was applied to it. It was working fine. Then I moved the fields to a fieldset and added the fieldset to the form. Now the assignment validators to the fields is not present. The validator objects isValid method is not triggered at all. So how to apply the InputFilter validators to fields in a fieldset? Here you are the classes:
Text class Validator
namespace Application\Validator;
use Zend\Validator\StringLength;
use Zend\Validator\ValidatorInterface;
class Text implements ValidatorInterface
{
protected $stringLength;
protected $messages = [];
public function __construct()
{
$this->stringLengthValidator = new StringLength();
}
public function isValid($value, $context = null)
{
if (empty($context['url'])) {
if (empty($value)) return false;
$this->stringLengthValidator->setMin(3);
$this->stringLengthValidator->setMax(5000);
if ($this->stringLengthValidator->isValid($value)) {
return true;
}
$this->messages = $this->stringLengthValidator->getMessages();
return false;
}
if (!empty($value)) return false;
return true;
}
public function getMessages()
{
return $this->messages;
}
}
Test class InputFilter
namespace Application\Filter;
use Application\Fieldset\Test as Fieldset;
use Application\Validator\Text;
use Application\Validator\Url;
use Zend\InputFilter\InputFilter;
class Test extends InputFilter
{
public function init()
{
$this->add([
'name' => Fieldset::TEXT,
'required' => false,
'allow_empty' => true,
'continue_if_empty' => true,
'validators' => [
['name' => Text::class],
],
]);
$this->add([
'name' => Fieldset::URL,
'required' => false,
'allow_empty' => true,
'continue_if_empty' => true,
'validators' => [
['name' => Url::class],
],
]);
}
}
Test class Fieldset
namespace Application\Fieldset;
use Zend\Form\Fieldset;
class Test extends Fieldset
{
const TEXT = 'text';
const URL = 'url';
public function init()
{
$this->add([
'name' => self::TEXT,
'type' => 'textarea',
'attributes' => [
'id' => 'text',
'class' => 'form-control',
'placeholder' => 'Type text here',
'rows' => '6',
],
'options' => [
'label' => self::TEXT,
],
]);
$this->add([
'name' => self::URL,
'type' => 'text',
'attributes' => [
'id' => 'url',
'class' => 'form-control',
'placeholder' => 'Type url here',
],
'options' => [
'label' => self::URL,
],
]);
}
}
Test class Form
namespace Application\Form;
use Application\Fieldset\Test as TestFieldset;
use Zend\Form\Form;
class Test extends Form
{
public function init()
{
$this->add([
'name' => 'test',
'type' => TestFieldset::class,
'options' => [
'use_as_base_fieldset' => true,
],
]);
$this->add([
'name' => 'submit',
'attributes' => [
'type' => 'submit',
'value' => 'Send',
],
]);
}
}
TestController class
namespace Application\Controller;
use Application\Form\Test as Form;
use Zend\Debug\Debug;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
class TestController extends AbstractActionController
{
private $form;
public function __construct(Form $form)
{
$this->form = $form;
}
public function indexAction()
{
if ($this->getRequest()->isPost()) {
$this->form->setData($this->getRequest()->getPost());
Debug::dump($this->getRequest()->getPost());
if ($this->form->isValid()) {
Debug::dump($this->form->getData());
die();
}
}
return new ViewModel(['form' => $this->form]);
}
}
TestControllerFactory class
namespace Application\Factory;
use Application\Controller\TestController;
use Application\Form\Test;
use Interop\Container\ContainerInterface;
use Zend\ServiceManager\Factory\FactoryInterface;
class TestControllerFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$form = $container->get('FormElementManager')->get(Test::class);
return new TestController($form);
}
}
Test class
namespace Application\Factory;
use Application\Filter\Test as Filter;
use Application\Entity\Form as Entity;
use Application\Form\Test as Form;
use Interop\Container\ContainerInterface;
use Zend\Hydrator\ClassMethods;
use Zend\ServiceManager\Factory\FactoryInterface;
class Test implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
return (new Form())
->setHydrator($container
->get('HydratorManager')
->get(ClassMethods::class))
->setObject(new Entity())
->setInputFilter($container->get('InputFilterManager')->get(Filter::class));
}
}
Test Fieldset
namespace Application\Factory;
use Application\Entity\Fieldset as Entity;
use Application\Fieldset\Test as Fieldset;
use Interop\Container\ContainerInterface;
use Zend\Hydrator\ClassMethods;
use Zend\ServiceManager\Factory\FactoryInterface;
class TestFieldset implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
return (new Fieldset())
->setHydrator($container->get('HydratorManager')->get(ClassMethods::class))
->setObject(new Entity());
}
}
UPDATE
I updated the fieldset class accordingly to #Nukeface advise by adding setInputFilter(). But it did not worked. It even had not executed InpuFilter class init method. Perhaps I did in wrong:
<?php
namespace Application\Fieldset;
use Application\Filter\Test as Filter;
use Zend\Form\Fieldset;
use Zend\InputFilter\InputFilterAwareTrait;
class Test extends Fieldset
{
use InputFilterAwareTrait;
const TEXT = 'text';
const URL = 'url';
public function init()
{
$this->add([
'name' => self::TEXT,
'type' => 'textarea',
'attributes' => [
'id' => 'text',
'class' => 'form-control',
'placeholder' => 'Type text here',
'rows' => '6',
],
'options' => [
'label' => self::TEXT,
],
]);
$this->add([
'name' => self::URL,
'type' => 'text',
'attributes' => [
'id' => 'url',
'class' => 'form-control',
'placeholder' => 'Type url here',
],
'options' => [
'label' => self::URL,
],
]);
$this->setInputFilter(new Filter());
}
}
Tried an answer before and ran out of chars (30k limit), so created a repo instead. The repo contains abstraction of the answer below, which is a working example.
Your question shows you having the right idea, just not yet the implementation. It also contains a few mistakes, such as setting a FQCN for a Fieldset name. Hopefully the below can have you up and running.
As a use case, we'll have a basic Address form. Relationships for Country, Timezones and other things I'll leave out of the scope. For more in depth and nesting of Fieldsets (also with Collections) I'll refer you to my repo.
General setup
First create the basic setup. Create the Entity and configuration.
Basic Entity
namespace Demo\Entity;
class Address
{
protected $id; // int - primary key - unique - auto increment
protected $street; // string - max length 255 - not null
protected $number; // int - max length 11 - not null
protected $city; // string - max length 255 - null
// getters/setters/annotation/et cetera
}
To handle this in a generic and re-usable way, we're going to need:
AddressForm (general container)
AddressFormFieldset (form needs to be validated)
AddressFieldset (contains the entity inputs)
AddressFieldsetInputFilter (must validate the data entered)
AddressController (to handle CRUD actions)
Factory classes for all of the above
a form partial
Configuration
To tie these together in Zend Framework, these need to be registered in the config. With clear naming, you can already add these. If you're using something like PhpStorm as your IDE, you might want to leave this till last, as the use statements can be generated for you.
As this is an explanation, I'm showing you now. Add this to your module's config:
// use statements here
return [
'controllers' => [
'factories' => [
AddressController::class => AddressControllerFactory::class,
],
],
'form_elements' => [ // <-- note: both Form and Fieldset classes count as Form elements
'factories' => [
AddressForm::class => AddressFormFactory::class,
AddressFieldset::class => AddressFieldsetFactory::class,
],
],
'input_filters' => [ // <-- note: input filter classes only!
'factories' => [
AddressFormInputFilter::class => AddressFormInputFilterFactory::class,
AddressFieldsetInputFilter::class => AddressFieldsetInputFilterFactory::class,
],
],
'view_manager' => [
'template_map' => [
'addressFormPartial' => __DIR__ . '/../view/partials/address-form.phtml',
],
];
Fieldset
First we create the Fieldset (and Factory) class. This is because this contains the actual object we're going to handle.
AddressFieldset
// other use statements for Elements
use Zend\Form\Fieldset;
class AddressFieldset extends Fieldset
{
public function init()
{
parent::init(); // called due to inheritance
$this->add([
'name' => 'id',
'type' => Hidden::class,
]);
$this->add([
'name' => 'street',
'required' => true,
'type' => Text::class,
'options' => [
'label' => 'Name',
],
'attributes' => [
'minlength' => 1,
'maxlength' => 255,
],
]);
$this->add([
'name' => 'number',
'required' => true,
'type' => Number::class,
'options' => [
'label' => 'Number',
],
'attributes' => [
'step' => 1,
'min' => 0,
],
]);
$this->add([
'name' => 'city',
'required' => false,
'type' => Text::class,
'options' => [
'label' => 'Name',
],
'attributes' => [
'minlength' => 1,
'maxlength' => 255,
],
]);
}
}
AddressFieldsetFactory
// other use statements
use Zend\ServiceManager\Factory\FactoryInterface;
class AddressFieldsetFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$this->setEntityManager($container->get(EntityManager::class));
/** #var AddressFieldset $fieldset */
$fieldset = new AddressFieldset($this->getEntityManager(), 'address');
$fieldset->setHydrator(
new DoctrineObject($this->getEntityManager())
);
$fieldset->setObject(new Address());
return $fieldset;
}
}
InputFilter
Above we created the Fieldset. That allows for the generation of the Fieldset for in a Form. At the same time, Zend Framework also has defaults already set per type of input (e.g. 'type' => Text::class). However, if we want to validate it to our own, more strict, standards, we need to override the defaults. For this we need an InputFilter class.
AddressFieldsetInputFilter
// other use statements
use Zend\InputFilter\InputFilter;
class AddressFieldsetInputFilter extends InputFilter
{
public function init()
{
parent::init(); // called due to inheritance
$this->add([
'name' => 'id',
'required' => true,
'filters' => [
['name' => ToInt::class],
],
'validators' => [
['name' => IsInt::class],
],
]);
$this->add([
'name' => 'street',
'required' => true,
'filters' => [
['name' => StringTrim::class], // remove whitespace before & after string
['name' => StripTags::class], // remove unwanted tags
[ // if received is empty string, set to 'null'
'name' => ToNull::class,
'options' => [
'type' => ToNull::TYPE_STRING, // also supports other types
],
],
],
'validators' => [
[
'name' => StringLength::class, // set min/max string length
'options' => [
'min' => 1,
'max' => 255,
],
],
],
]);
$this->add([
'name' => 'number',
'required' => true,
'filters' => [
['name' => ToInt::class], // received from HTML form always string, have it cast to integer
[
'name' => ToNull::class, // if received is empty string, set to 'null'
'options' => [
'type' => ToNull::TYPE_INTEGER,
],
],
],
'validators' => [
['name' => IsInt::class], // check if actually integer
],
]);
$this->add([
'name' => 'city',
'required' => false, // <-- not required
'filters' => [
['name' => StringTrim::class], // remove whitespace before & after string
['name' => StripTags::class], // remove unwanted tags
[ // if received is empty string, set to 'null'
'name' => ToNull::class,
'options' => [
'type' => ToNull::TYPE_STRING, // also supports other types
],
],
],
'validators' => [
[
'name' => StringLength::class, // set min/max string length
'options' => [
'min' => 1,
'max' => 255,
],
],
],
]);
}
}
AddressFieldsetInputFilterFactory
// other use statements
use Zend\ServiceManager\Factory\FactoryInterface;
class AddressFieldsetInputFilterFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
// Nothing else required in this example. So it's as plain as can be.
return new AddressFieldsetInputFilter();
}
}
Form & Validation
So. Above we created the Fieldset, it's InputFilter and 2 required Factory classes. This already allows us to do a great deal, such as:
use the InputFilter in stand-alone setting to dynamically validate an object
re-use Fieldset + InputFilter combination in other Fieldset and InputFilter classes for nesting
Form
use Zend\Form\Form;
use Zend\InputFilter\InputFilterAwareInterface;
// other use statements
class AddressForm extends Form implements InputFilterAwareInterface
{
public function init()
{
//Call parent initializer. Check in parent what it does.
parent::init();
$this->add([
'type' => Csrf::class,
'name' => 'csrf',
'options' => [
'csrf_options' => [
'timeout' => 86400, // day
],
],
]);
$this->add([
'name' => 'address',
'type' => AddressFieldset::class,
'options' => [
'use_as_base_fieldset' => true,
],
]);
$this->add([
'name' => 'submit',
'type' => Submit::class,
'attributes' => [
'value' => 'Save',
],
]);
}
}
Form Factory
use Zend\ServiceManager\Factory\FactoryInterface;
// other use statements
class AddressFormFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
/** #var AbstractForm $form */
$form = new AddressForm('address', $this->options);
$form->setInputFilter(
$container->get('InputFilterManager')->get(ContactFormInputFilter::class);
);
return $form;
}
}
Making it all come together
I'll show just the AddressController#addAction
AddressController
use Zend\Mvc\Controller\AbstractActionController;
// other use statements
class AddressController extends AbstractActionController
{
protected $addressForm; // + getter/setter
protected $entityManager; // + getter/setter
public function __construct(
EntityManager $entityManager,
AddressForm $form
) {
$this->entityManager = $entityManager;
$this->addressForm = $form;
}
// Add your own: index, view, edit and delete functions
public function addAction () {
/** #var AddressForm $form */
$form = $this->getAddressForm();
/** #var Request $request */
$request = $this->getRequest();
if ($request->isPost()) {
$form->setData($request->getPost());
if ($form->isValid()) {
$entity = $form->getObject();
$this->getEntityManager()->persist($entity);
try {
$this->getEntityManager()->flush();
} catch (\Exception $e) {
$this->flashMessenger()->addErrorMessage($message);
return [
'form' => $form,
'validationMessages' => $form->getMessages() ?: '',
];
}
$this->flashMessenger()->addSuccessMessage(
'Successfully created object.'
);
return $this->redirect()->route($route, ['param' => 'routeParamValue']);
}
$this->flashMessenger()->addWarningMessage(
'Your form contains errors. Please correct them and try again.'
);
}
return [
'form' => $form,
'validationMessages' => $form->getMessages() ?: '',
];
}
}
AddressControllerFactory
class AddressControllerFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
/** #var AddressController $controller */
$controller = new AddressController(
$container->get(EntityManager::class),
$container->get('FormElementManager')->get(AddressForm::class);
);
return $controller;
}
}
Display in addressFormPartial
$this->headTitle('Add address');
$form->prepare();
echo $this->form()->openTag($form);
echo $this->formRow($form->get('csrf'));
echo $this->formRow($form->get('address')->get('id'));
echo $this->formRow($form->get('address')->get('street'));
echo $this->formRow($form->get('address')->get('number'));
echo $this->formRow($form->get('address')->get('city'));
echo $this->formRow($form->get('submit'));
echo $this->form()->closeTag($form);
To use this partial, say in a add.phtml view, use:
<?= $this->partial('addressFormPartial', ['form' => $form]) ?>
This bit of code will work with the demonstrated addAction in the Controller code above.
Hope you found this helpful ;-) If you have any questions left, don't hesitate to ask.
Just use the InputFilterProviderInterface class to your fieldset. This implements the getInputFilterSpecification method to your fieldset, which executes the input filters mentioned in this method.
class MyFieldset extends Fieldset implements InputFilterProviderInterface
{
public function init()
{
$this->add([
'name' => 'textfield',
'type' => Text::class,
'attributes' => [
...
],
'options' => [
...
]
]);
}
public function getInputFilterSpecification()
{
return [
'textfield' => [
'required' => true,
'filters' => [
...
],
'validators' => [
[
'name' => YourTextValidator::class,
'options' => [
...
],
],
],
],
];
}
}
AS long as you add this fieldset in your form the bound filters and validators will be executed on the isValid method call of your form.

Send choice Value instead choice key from Symfony form

I need to send from Symfony form ChoiceType::class
But I don't need choices keys, I need to send choices values.
Is that is possible?
$form->add('section', ChoiceType::class, array(
'mapped' => false,
'choices' => array(
1 => 'value1',
2 => 'value2'
),
));
I just want to send value1 if I chose value1,
not key 1 as default.
You can use
array_flip ($array)
refer to php docs
[Since Symfony 2.7] In any case you can play with choice value through choice_value option and a Closure function (Reference):
$form->add('section', ChoiceType::class, array(
'choice_value' => function ($value, $key, $index) {
return $value;
}
));
Useful for dynamic choices.
You just need to reverse it. Also, I don't think you need 'mapped'.
Try this:
$form->add(
'section',
ChoiceType::class,
[
'choices' => [
'value1' => 1,
'value2' => 2,
],
]
);
It should work.
Mayby a bit late but i've made this and it works perfect. Without array_flip. Mayby for someone it 'll be usefull.
$dataUsers = [];
$users = [
['id' => 1, 'firstname' => 'joe', 'lastname' => 'doe'],
['id' => 2, 'firstname' => 'will', 'lastname' => 'fog'],
];
foreach ($users as $u) {
$dataUsers[] = (object)['id' => $u['id'], 'label' => $u['firstname']];
}
$builder
->add('users', ChoiceType::class, [
'choices' => $dataUsers,
'choice_label' => function ($value) {
if (is_object($value)) {
return $value->label;
} else {
return 0;
}
},
'choice_value' => function ($value) {
if (is_object($value)) {
return $value->id;
} else {
return 0;
}
},
'data' => (object)[ 'id' => 2]
]);

How to get the value of one of the attributes used by Backpack?

For example, I need to take the "id" value and use it to do a search for my model Article, but this value (id) which also appears in the URL: "/article/4/edit" and in the "setColumns" parameters, I don't have any idea how to get it.
I need your help.
This is my sample code:
ArticleCrudController.php
public function __construct()
{
parent::__construct();
$this->crud->setModel('App\Models\Article');
$this->crud->setRoute("admin/article");
$this->crud->setEntityNameStrings('article', 'articles');
$this->crud->setColumns(['id', 'title', 'text']);
// WHERE ARE YOU ID?!?!!!
$article = Article::findOrFail($id);
$pictures = $article->picture()->first()->name;
$this->crud->addFields([
[
'name' => 'title',
'label' => 'Title',
'type' => 'Text'
],
[
'name' => 'text',
'label' => 'Text',
'type' => 'ckeditor'
],
[ // Browse
'name' => 'image',
'label' => 'Article immage',
'type' => 'browse',
'value' => $pictures //Obviously without id don't work :'(
]
]);
}
You could try to override the CrudController::edit method to which is passed the id as first parameter.
public function edit($id)
{
$articlePicture = Article::findOrFail($id)->picture()->first()->name;
$this->crud->addField([
'name' => 'image',
'value' => $articlePicture
]);
return parent::edit($id);
}
This could be a solution but I'm not sure it's the right way to do what you want.
Use getCurrentEntry to get the current model object in crud
$article_id = $this->crud->getCurrentEntry()->id;

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