Symfony 2 - rearrange form fields - symfony

In our Symfony2 project we have a very complex structure for forms with embedded forms.... Now we got the requirement to bring the output of the form in an specific order.
And here is the problem: We use the form_widget(form) and now we are looking for a solution in the object (e.g. via annotations) or the formbuilder to move a specific field to the end of the form. in symfony 1.4 it was the widget-movefield() function, i guess...
Thx...

You can re-order the fields using this bundle: https://github.com/egeloen/IvoryOrderedFormBundle
This allows you to do things like this:
$builder
->add('g', 'text', array('position' => 'last'))
->add('a', 'text', array('position' => 'first'))
->add('c', 'text')
->add('f', 'text')
->add('e', 'text', array('position' => array('before' => 'f')))
->add('d', 'text', array('position' => array('after' => 'c')))
->add('b', 'text', array('position' => 'first'));
This was going to be in core, but was rejected and pulled out into a bundle.

Had same issue today with the form elements ordering.
Ended up with a trait that will override finishView method and reorder items in children property of a FormView:
trait OrderedTrait
{
abstract function getFieldsOrder();
public function finishView(FormView $view, FormInterface $form, array $options)
{
/** #var FormView[] $fields */
$fields = [];
foreach ($this->getFieldsOrder() as $field) {
if ($view->offsetExists($field)) {
$fields[$field] = $view->offsetGet($field);
$view->offsetUnset($field);
}
}
$view->children = $fields + $view->children;
parent::finishView($view, $form, $options);
}
}
Then in type implement getFieldsOrder method:
use OrderedTrait;
function getFieldsOrder()
{
return [
'first',
'second',
'next',
'etc.',
];
}

There's no need to "reorder" fields. All you need to do is to call form_label and/or form_widget for each field individually. Assuming you use Twig you could, for example, do:
<div>{{ form_label(form.firstName) }}</div>
<div>{{ form_widget(form.firstName) }}</div>
<div>{{ form_label(form.lastName) }}</div>
<div>{{ form_widget(form.lastName) }}</div>

here is the solution I came up with.
I created a class that my types extend.
namespace WF\CORE\CoreBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class WfBaseForm extends AbstractType
{
protected function useFields(FormBuilderInterface $builder, $fields)
{
foreach ($builder->all() as $field) {
if (!in_array($field->getName(), $fields))
$builder->remove($field->getName());
}
}
public function reorder(FormBuilderInterface $builder, $keys = array())
{
$fields = $builder->all();
$ordered_fields = array_merge(array_flip($keys), $fields);
$this->useFields($builder, array());
foreach($ordered_fields as $field)
$builder->add($field);
}
public function getName()
{
return 'base';
}
}
Then you can use it in your inherited classes.
class AddressType extends WfBaseForm
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
// I add as many fields as I need
$builder->add( '…');
}
This class extends the one above
class ModifyAddressType extends BaseType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder->add('title', 'text', array('constraints' => array(new NotBlank())));
$this->reorder($builder, array('title', 'firstname', 'lastname'));
}
}

I had the same problem, but solved it in a different way. Here is my solution, as an idea for others who are searching for this problem.
You could add all the form fields in an event listener, because here you have all the objects data to decide on the fields order. You could for example use a method from the data object to decide on the fields order:
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Don't use $builder->add(), or just for those fields which are always
// at the beginning of the form.
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$entry = $event->getData();
$form = $event->getForm();
// Now add all the fields in the order you need them to be, e.g by reading
// the needed order from the data entry.
if ($entry->isFieldAFirst()) {
$form->add(fieldA);
$form->add(fieldB);
} else {
$form->add(fieldB);
$form->add(fieldA);
}
}
}

As I understand, you want to use only form_widget(form) in final template.
Let assume we have two inherited models (ModelA, ModelB) and form types for them (ModelAType, ModelBType)
class ModelA {
private $A;
private $B;
// Getters and setters
}
class ModelB extends ModelA {
private $C;
// Getters and setters
}
/**
* #DI\Service(id = "form.type.modelA")
* #DI\Tag("form.type", attributes={ "alias":"model_a_type" })
*/
class FormAType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('A')
->add('B')
;
}
// getName and so on
}
/**
* #DI\Service(id = "form.type.modelA")
* #DI\Tag("form.type", attributes={ "alias":"model_b_type" })
*/
class FormAType extends AbstractType {
public function getParent() {
return "model_a_type";
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('C')
;
}
// getName and so on
}
If you render formB, you will get A,B,C order, but you want A,C,B.
To accomplish that, create form template and add reference to app. config file:
#app/config/config.yml
twig:
....
form:
resources:
- YourAppBundle:Form:fields.html.twig
{# src/YourAppBundle/Resources/views/Form/fields.html.twig #}
{% block model_a_type_widget %}
{{ form_widget(form.A) }}
{{ form_widget(form.B) }}
{% endblock model_a_type_widget %}
{% block model_b_type_widget %}
{{ form_widget(form.A) }}
{{ form_widget(form.C) }}
{{ block('model_a_type_widget') }}
{% endblock model_b_type_widget %}
Now, when you render formB, you will see desired order and keep code structured.
It happens because every widget rendered only once, so if you render it before calling parent block, you will change their order.

You can do it right in your controller before you render the template:
$form = ...
$view = $form->createView();
// Perform standard array operations to reorder: `$view->children`
// Here's a very simple example:
$firstField = $view->children['firstField'];
unset($view->children['firstField']);
$view->children['firstField'] = $firstField;
return array('form' => $view);
uasort works as well, however usort creates a non-associative array which will break the form rendering later on.

{{ form_start(form) }}
<div>{{ form_label(form.title) }}</div>
<div>{{ form_widget(form.title,{'id': 'blog_title'} )}}</div>
<div>{{ form_label(form.tag) }}</div>
<div>{{ form_widget(form.tag,{'id': 'blog_tag'} )}}</div>
<div>{{ form_label(form.category) }}</div>
<div>{{ form_widget(form.category,{'id': 'blog_category'} )}}</div>
<div>{{ form_label(form.image) }}</div>
<div>{{ form_widget(form.image,{'id': 'blog_image'} )}}</div>
<div>{{ form_label(form.body) }}</div>
<div>{{ form_widget(form.body,{'id': 'editor'} )}}</div>
<input type="submit" class="btn btn-success" value="Create" />
{{ form_end(form) }}

Related

How to define a custom form type class for html5 datalist in order to handle entityType in Symfony > 3.4

I'm setting up a custom form type for datalist and it works fine using a preset choices, but I'm unable to set up it in order to let it handle an EntityType.
That's my working code
<?php
// path and filename
// /src/form/type/DatalistType.php
namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType; // unused at the moment
class DatalistType extends AbstractType {
private $entityManager;
public function __construct(EntityManagerInterface $entityManager) {
$this->entityManager = $entityManager;
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults([
'choices' => [
'Math' => 'Math',
'Physics' => 'Physics',
'Chemistry' => 'Chemistry',
],
]);
}
public function getParent() {
return ChoiceType::class;
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setRequired(['choices']);
}
public function buildView(FormView $view, FormInterface $form, array $options) {
$view->vars['choices'] = $options['choices'];
}
public function getName() {
return 'datalist';
}
}
<?php
// path and filename
// /src/form/DegreeType.php
namespace App\Form;
use App\Entity\Degree;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use App\Form\Type\DatalistType;
class DegreeType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('degree', DatalistType::class, [
'placeholder' => 'Choose a master degree',
])
;
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults([
'data_class' => Degree::class,
]);
}
}
// TWIG TEMPLATE
// path and filename
// templates/form/fields.html.twig
?>
{% block datalist_widget %}
<div class="form-group">
<input list="{{ id }}_list" {{ block('widget_attributes') }} class="form-control">
<datalist id="{{ id }}_list">
{% for choice in choices %}
<option value="{{ choice }}"></option>
{% endfor %}
</datalist>
</div>
{% endblock %}
// config/packages/twig.yaml
twig:
paths: ['%kernel.project_dir%/templates']
debug: '%kernel.debug%'
strict_variables: '%kernel.debug%'
form_themes: ['form/fields.html.twig']
I changed the getParent() method in order to return an EntityType::class
public function getParent() {
return EntityType::class;
}
Removed the default vaulues for $resolver inside configureOptions() method
public function configureOptions(OptionsResolver $resolver) {
}
then inside the form builder
->add('degree',DatalistType::class , [
'label' => 'Choose an master degree',
'class' => Degree::class
])
I expect it works as for the static values, but it didn't.
I've read any kind of question here like
Symfony Forms: HTML5 datalist
but I think the answers posted wasn't complete or it was for old version of Symfony, not for > 3.4
The solution is to remove all methods inside DatalistType
and leave just constructor and getParent(): EntityType::class
<?php
// path and filename
// /src/form/type/DatalistType.php
namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
class DatalistType extends AbstractType {
private $entityManager;
public function __construct(EntityManagerInterface $entityManager) {
$this->entityManager = $entityManager;
}
public function getParent() {
return EntityType::class;
}
}
Then, change the template
{% block datalist_widget %}
<div class="form-group">
<input {{ block('widget_attributes') }} list="{{ form.vars.id }}_list" value="{{ form.vars.value }}" class="form-control" >
<datalist id="{{ form.vars.id }}_list">
{% for choice in choices %}
<option>
{{ choice.label }}
</option>
{% endfor %}
</datalist>
</div>
{% endblock %}
It works fine!!!

Sonata admin form field on same line

Is there a way in configureformsfields method from admin to show field on a same line and not one below the other? A CSS class for example?
You can add CSS class with an appropriate display option to the selected field as:
->add('fieldname', null, [
'attr' => ["class" => "your-custom-class"]
])
Also you can modify .form-group class (to make all fields inline):
.form-group {
display: inline-block;
}
If you want your input to be inline with its label:
div.sonata-ba-field.sonata-ba-field-standard-natural {
display: inline-block;
}
Tutorial how to create a CSS file and load it into Sonata template could be found here.
In symfony 4 you can configure it globally in the config/packages/sonata_admin.yaml
sonata_admin:
options:
form_type: 'horizontal'
Gives:
There is some workaround:
- Create custom form type to add new option to form field
class CustomTextType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['parent_div_class'] = $options['parent_div_class'];
}
/**
* {#inheritdoc}
*/
public function getParent()
{
return TextType::class;
}
/**
* {#inheritdoc}
*/
public function getBlockPrefix()
{
return 'custom_text_type';
}
/**
* {#inheritdoc}
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(
[
'parent_div_class' => null,
]
);
}
Create custom form theme that extends sonata one
{% extends '#SonataDoctrineORMAdmin/Form/form_admin_fields.html.twig' %}
{% block form_row %}
{% if parent_div_class is defined and parent_div_class %}
<div class="{{ parent_div_class }}">
{{ parent() }}
</div>
{% else %}
{{ parent() }}
{% endif %}
{% endblock %}
Set this theme in your admin class
$this->setFormTheme(['#Admin/form/form_theme.html.twig']);
enjoy.
The SonataCoreBundle provides the form_type option for this.
# app/config/config.yml
sonata_core:
form_type: horizontal

Display Many-To-One fields in Symfony2 forms?

I am having two entity files one as Activite.php and another as Mesurage.php.
Now i want to display an Activite form with 3 fields typeActivite, emplacement and mesurage. the mesurage will be a selection that will fetch data from mesurage table. here is the code that i wrote inside Activite.php to create a many_to_one field for mesurage_id
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity="GestionEnvironnementale\ISO14001Bundle\Entity\Mesurage")
*/
private $mesurage;
Below is my Form generation Code :
class ActiviteType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('typeActivite'),
->add('emplacement'),
->add('mesurage', 'entity', array('class' => 'ISO14001Bundle:Mesurage'));
}
}
here is my form code :
<div class="well">
<form method="post" {{ form_enctype(form) }}>
{{ form_widget(form) }}
<br/> <input type="submit" value="Envoyer" class="btn btn-primary" />
</form>
<script src="{{ asset('js/jquery-2.1.1.min.js') }}"></script>
<script type="text/javascript">
$(document).ready(function()
{
var $container3 = $('div#gestionenvironnementale_iso14001bundle_activitetype_activiteMesurage');
var $lienAjout3 = $('Ajouter un mesurage');
$container3.append($lienAjout3);
$lienAjout3.click(function(h) {
ajouterMesurage($container3);
h.preventDefault();
return false;
});
var index3 = $container3.find(':input').length;
if (index3 == 0) {
ajouterMesuragePolluant($container3);
} else {
$container3.children('div').each(function() {
ajouterLienSuppression3($(this));
});
}
function ajouterMesurage($container3) {
var $prototype3 = $($container3.attr('data-prototype').replace(/__name__label__/g, 'Mesurage n°' + (index3+1))
.replace(/__name__/g, index3));
ajouterLienSuppression3($prototype3);
$container3.append($prototype3);
index3++;
}
function ajouterLienSuppression3($prototype3) {
$lienSuppression3 = $('Supprimer');
$prototype3.append($lienSuppression3);
$lienSuppression3.click(function(h) {
$prototype3.remove();
h.preventDefault();
return false;
});
}
});
the code works very well but I dont want to display the list of Mesurage, I want to display the form of Mesurage to add a new !!
If you want to display a form even for mesurage, you have to take a look at embed form
So, basically, you have to create a FormType for mesurage (call it MesurageFormType) and modify your ActiviteType as follows
class ActiviteType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('typeActivite'),
->add('emplacement'),
->add('mesurage', 'collection', array(
'type' => new MesurageFormType(),
'allow_add' => true,));
}
}
This should be fine but if you want to render in a different way your form you should use prototype and jquery

Double validation errors in custom form type

I'm implementing a custom form type that provides an autocomplete field to select a location (country,city or spot). The form type creates two fields, one text field for the autocomplete search input and one hidden field to hold the selected id of the selected location.
When typing into the text field, a server call is made and results are displayed via jquery autocomplete. If a location is selected, the id of the selected location is written to the hidden field whereas the name of the location is displayed in the text field. On the server, I use a client transformer to lookup the entity of the id passed by the hidden field. The text field is ignored.
My model class defines a location field with a property to write back the location entity annotated with a NotNull validation constraint.
Everything works perfectly fine so far but if I do not select a location, the validation message "This value should not be null." is displayed two times.
The bundle is public and can be found in my github repo. The relevant classes are the LocationFieldType and the LocationDataTransformer and the form theme.
And now for how I'm integrating the form type into my project. I added the whole code, sorry for the mass;)
In the model, I define the property as following:
class JourneyCreate
{
/**
* #Assert\NotNull()
* #Assert\Choice(choices = {"offer", "request"})
*/
public $type;
/**
* #Assert\NotNull()
* #Assert\Date()
*/
public $date;
/**
* #Assert\NotNull()
* #Assert\Time()
*/
public $time;
/**
* #Assert\NotNull()
*
*/
public $start;
/**
* #Assert\NotNull()
*
*/
public $destination;
public function buildJourney(User $owner)
{
switch($this->type)
{
case 'offer':
$journey = new JourneyOffer();
break;
case 'request':
$journey = new JourneyRequest();
break;
default:
throw new \InvalidArgumentException('Invalid journey type');
}
$journey->setDate($this->date);
$journey->setTime($this->time);
$journey->addStation(new JourneyStation($this->start));
$journey->addStation(new JourneyStation($this->destination));
$journey->setOwner($owner);
return $journey;
}
}
And in the main form I add the field as following:
class JourneyCreateType extends BaseType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('type','choice', array(
'choices' => array(
'offer' => 'Driver',
'request' => 'Passanger',
),
'empty_value'=>'',
'multiple' => false,
'expanded' => true,
))
->add('date','date',array(
'widget' => 'single_text',
'format' => $this->getDateFormat(\IntlDateFormatter::TRADITIONAL),
))
->add('time','time',array(
'widget' => 'single_text',
))
->add('start','room13_geo_location')
->add('destination','room13_geo_location')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\DemoBundle\Form\Model\JourneyCreate',
));
}
public function getName()
{
return 'journey_create';
}
}
And the controller code:
/**
* #Route("/create/{type}", defaults={"type" = null})
* #Template()
*/
public function createAction($type=null)
{
if($type !== null && !in_array($type,array('request','offer')))
{
throw new NotFoundHttpException();
}
$journeyCreate = new JourneyCreate();
$journeyCreate->type = $type;
$form = $this->createForm(new JourneyCreateType(),$journeyCreate);
if($this->isPost())
{
$form->bind($this->getRequest());
if($form->isValid())
{
$journeyCreate = $form->getData();
$journey = $journeyCreate->buildJourney($this->getCurrentUser());
$this->persistAndFlush($journey);
return $this->redirect($this->generateUrl('acme_demo_journey_edit',array('id'=>$journey->getId())));
}
}
return array(
'form' => $form->createView(),
);
}
And finaly the template code to display the form:
{% block page_body %}
<form class="form-horizontal" action="{{ path('acme_demo_journey_create') }}" method="post" novalidate>
{{form_widget(form)}}
<div class="form-actions">
<button class="btn btn-primary" type="submit">{{'form.submit'|trans}}</button>
{{'form.cancel'|trans}}
</div>
</form>
{% endblock %}
I'm having the theory that this could be because I use two form fields but don't know how to fix this. Any suggestions about how to solve this more elegant are welcome.
As complicated as this question might look, the answer is as simple as removing the {{form_errors(form)}} from the widget template block. Because the *form_row* block looks like:
{% block form_row %}
{% spaceless %}
<div class="form_row">
{{ form_label(form) }}
{{ form_errors(form) }}
{{ form_widget(form) }}
</div>
{% endspaceless %}
{% endblock form_row %}
The error was simply outputted two times.

Symfony2 Entity type form with name and image

I have an entity with both a name (string) and a file (also a string representing the filename). This is the "Icon" entity.
I have another entity called "Category" which has a name (string) and a relation to an Icon (OneToMany). I want the form to allow a user to select an Icon for a Category.
So I could display it in the form as:
$builder->add('icon', 'entity', array(
'class' => 'CroltsMainBundle:Icon',
'expanded' => true,
'multiple' => false
));
But what I really want is to display something like this in twig for each radio button:
<div>
<label for="something"><img src="/icons/{{icon.file }}" />{{icon.name}}</label>
<input type="radio" name="something" value="{{ icon.id }}" />
</div>
Is there a good way to make that type of radio form with Symfony forms? Like would a custom Type be what I want? I really haven't done too much with custom types to know how much of this is possible.
Not sure it is the best way but here is how I manage this kind of situation :
create a new formtype that behalf as entityType, IconCheckType for instance:
(http://symfony.com/doc/master/cookbook/form/create_custom_field_type.html)
namespace .....\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormInterface;
class IconCheckType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilder $builder, array $options) {
$builder -> setAttribute('dataType', $options['dataType']);
}
/**
* {#inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form) {
$view -> set('dataType', $form -> getAttribute('dataType'));
}
/**
* {#inheritdoc}
*/
public function getDefaultOptions(array $options) {
return array('required' => false,'dataType'=>'entity');
}
/**
* Returns the allowed option values for each option (if any).
*
* #param array $options
*
* #return array The allowed option values
*/
public function getAllowedOptionValues(array $options)
{
return array('required' => array(false));
}
/**
* {#inheritdoc}
*/
public function getParent(array $options) {
return 'entity';
}
/**
* {#inheritdoc}
*/
public function getName() {
return 'iconcheck';
}
}
in your Form
...
->add('icon', 'iconcheck', array(
'class' => 'CroltsMainBundle:Icon',
'property'=>'formField',
'multiple'=>false,
'expanded'=>true
))
...
Note the property=>'formField', that means that instead of return the __toString as label it will return whatever you want from the function getFormField from your entity class
So, in your entity class:
class Icon {
....
public function getFormField() {
return $this; /* or an array with only the needed attributes */
}
....
}
then you can render your custom field
{% block iconcheck_widget %}
{% for child in form %}
{% set obj=child.vars.label %}
<div>
<label for="something"><img src="/icons/{{obj.file }}" />{{obj.name}}</label>
{{ form_widget(child) }} {# the radio/checkbox #}
</div>
{{ form_widget(child) }}#}
{% endfor %}
{% endblock %}
Could you potentially make your __toString() method:
<?php
// Icon entity
public function __toString()
{
return '<img src="/icons/'. $this->file .'" />' . $this->name';
}
If not then you will have to create a custom type. However it is really easy
<?php
namespace Your\NameSpace;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormViewInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class MyCustomType extends AbstractType
{
public function getParent()
{
// By calling get parent here your custom type will
// automatically inherit all the properties/functionality
// of the type you extend
return 'radio';
}
}
Then you can make your custom widget for your type. I would read the cookbook entry if I was you because it explains the process very well. You can look at the default Twig widgets for forms to learn how to write your own.
I had to add a thumbnail in front of the select file button for image uploading today. I wound up doing this. Sorry I don't have time to create a complete example for your case.
I'm only accessing the parent to get the entity to pass to the vich_uploadable_asset() helper.
/src/AcmeBundle/Form/Type/AcmeFormType.php
<?php
namespace Acme\AcmeBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class AcmeFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder
->add('icon', 'vich_uploadable')
...
config.yml
twig:
form:
resources:
- 'AcmeBundle:Form:fields.html.twig'
services:
acme.type.vich_uploadable:
class: Acme\AcmeBundle\Form\Type\VichUploadableFieldType
arguments: ["#doctrine.orm.entity_manager"]
tags:
- { name: form.type, alias: vich_uploadable }
/src/Acme/AcmeBundle/Form/fields.html.twig
{% block vich_uploadable_widget %}
{% spaceless %}
{% if attribute(form.parent.vars.value, form.name) is not empty %}
<img src="{{ vich_uploader_asset(form.parent.vars.value, form.name) | imagine_filter('thumb_square') }}" />
{% endif %}
{{ form_widget(form) }} {# If you're extending the radio button, it would show here #}
{% endspaceless %}
{% endblock %}
Here is what I ended up doing. It took a lot of trial and error and digging into the EntityType class hierarchy and learning how Form types really work. The hardest part is looking at source code and figuring out how to get from PHP classes to Twig templates (what variables are available).
Here is what I did. It is not a perfect solution (feels a bit hacky) but it works for my purposes. The idea is to expose the underlying Entity to my view so I can get its properties.
The biggest problem is that the file property which holds the file path is hardcoded in the view. Anyway, I'm posting the entire solution as it may be helpful to others. Also I'm open to critique if someone can find a better solution.
(namespaces omitted)
Extended Entity Type
<?php
class ExtendedEntityType extends EntityType
{
public function getParent()
{
return 'extended_choice';
}
public function getName()
{
return 'extended_entity';
}
}
Extended Choice Type (just had to change addSubForms but it's private)
<?php
class ExtendedChoiceType extends ChoiceType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
if (!$options['choice_list'] && !is_array($options['choices']) && !$options['choices'] instanceof \Traversable) {
throw new FormException('Either the option "choices" or "choice_list" must be set.');
}
if ($options['expanded']) {
$this->addSubForms($builder, $options['choice_list']->getPreferredViews(), $options);
$this->addSubForms($builder, $options['choice_list']->getRemainingViews(), $options);
if ($options['multiple']) {
$builder
->addViewTransformer(new ChoicesToBooleanArrayTransformer($options['choice_list']))
->addEventSubscriber(new FixCheckboxInputListener($options['choice_list']), 10)
;
} else {
$builder
->addViewTransformer(new ChoiceToBooleanArrayTransformer($options['choice_list']))
->addEventSubscriber(new FixRadioInputListener($options['choice_list']), 10)
;
}
} else {
if ($options['multiple']) {
$builder->addViewTransformer(new ChoicesToValuesTransformer($options['choice_list']));
} else {
$builder->addViewTransformer(new ChoiceToValueTransformer($options['choice_list']));
}
}
if ($options['multiple'] && $options['by_reference']) {
// Make sure the collection created during the client->norm
// transformation is merged back into the original collection
$builder->addEventSubscriber(new MergeCollectionListener(true, true));
}
}
/**
* {#inheritdoc}
*/
public function getParent()
{
return 'choice';
}
/**
* {#inheritdoc}
*/
public function getName()
{
return 'extended_choice';
}
/**
* Adds the sub fields for an expanded choice field.
*
* #param FormBuilderInterface $builder The form builder.
* #param array $choiceViews The choice view objects.
* #param array $options The build options.
*/
private function addSubForms(FormBuilderInterface $builder, array $choiceViews, array $options)
{
foreach ($choiceViews as $i => $choiceView) {
if (is_array($choiceView)) {
// Flatten groups
$this->addSubForms($builder, $choiceView, $options);
} else {
$choiceOpts = array(
'value' => $choiceView->value,
// Expose more data
'label' => array(
'data' => $choiceView->data,
'label' => $choiceView->label,
),
'translation_domain' => $options['translation_domain'],
);
if ($options['multiple']) {
$choiceType = 'checkbox';
// The user can check 0 or more checkboxes. If required
// is true, he is required to check all of them.
$choiceOpts['required'] = false;
} else {
$choiceType = 'radio';
}
$builder->add((string) $i, $choiceType, $choiceOpts);
}
}
}
}
Services
<service id="crolts_main.type.extended_choice" class="My\MainBundle\Form\Type\ExtendedChoiceType">
<tag name="form.type" alias="extended_choice" />
</service>
<service id="crolts_main.type.extended_entity" class="My\MainBundle\Form\Type\ExtendedEntityType">
<tag name="form.type" alias="extended_entity" />
<argument type="service" id="doctrine" />
</service>
form_layout.html.twig
(this is based on MopaBootStrapBundle but the idea is the same. The difference is that MopaBootstrap wraps the <label> around the <radio>)
{% block extended_choice_widget %}
{% spaceless %}
{% if expanded %}
{{ block('extended_choice_widget_expanded') }}
{% else %}
{# not being used, just default #}
{{ block('choice_widget_collapsed') }}
{% endif %}
{% endspaceless %}
{% endblock extended_choice_widget %}
{% block extended_choice_widget_expanded %}
{% spaceless %}
<div {{ block('widget_container_attributes') }}>
{% for child in form %}
<label class="{{ (multiple ? 'checkbox' : 'radio') ~ (widget_type ? ' ' ~ widget_type : '') ~ (inline is defined and inline ? ' inline' : '') }}">
{{ form_widget(child, {'attr': {'class': attr.widget_class|default('')}}) }}
{% if child.vars.label.data.file is defined %}
<img src="{{ vich_uploader_asset(child.vars.label.data, 'file')}}" alt="">
{% endif %}
{{ child.vars.label.label|trans({}, translation_domain) }}
</label>
{% endfor %}
</div>
{% endspaceless %}
{% endblock extended_choice_widget_expanded %}
Usage
<?php
$builder->add('icon', 'extended_entity', array(
'class' => 'MyMainBundle:MenuIcon',
'property' => 'name', // this is still used in label.label
'expanded' => true,
'multiple' => false
));

Resources