I am trying to set the choices for a form select dynamically since the choices come from a service call. However, when the form renders in the view, the choices are not there.
I'm doing the following in the FormType
<?php
namespace My\Form\Customer;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class ItemReturnRequestForm extends AbstractType
{
/**
* #var EventSubscriberInterface
*/
protected $reasonsSubscriber;
/**
* Returns the name of this type.
*
* #return string The name of this type
*/
public function getName()
{
return 'item_return_request';
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('reason', 'choice', [
'label' => 'order.returns.reason_for_return',
'required' => true,
'multiple' => false,
'expanded' => false,
'placeholder' => 'order.returns.reasons.empty',
'empty_data' => null,
]);
$builder->addEventSubscriber($this->reasonsSubscriber);
}
/**
* #param EventSubscriberInterface $reasonsSubscriber
*/
public function setReasonsSubscriber(EventSubscriberInterface $reasonsSubscriber)
{
$this->reasonsSubscriber = $reasonsSubscriber;
}
}
The FormType has a service definition which injects the EventSubscriber instance since that is also a service definition with it's own dependencies.
The EventSubscrbier looks like
<?php
namespace My\Form\EventSubscriber;
use My\Customer\ItemReturnAware;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
class ReturnReasonEventSubscriber implements EventSubscriberInterface
{
use ItemReturnAware;
public static function getSubscribedEvents()
{
return [
FormEvents::PRE_SET_DATA => 'getReturnReasons',
];
}
public function getReturnReasons(FormEvent $event)
{
$form = $event->getForm();
if ($form->has('reason')) {
$options = $form->get('reason')->getConfig()->getOptions();
$options['choices'] = $this->itemReturnService->getReasons();
$form->add('reason', 'choice', $options);
}
}
}
Everything seems to work fine up until this point. Using XDEBUG I can see that the EventSubscriber is being triggered. The service call sets $option['choices'] to the expected array value & the field is added successfully.
However, when the form gets rendered. it's as if the EventSubscriber had never been called.
If it makes a difference, the options array is an un-ordered numeric list.
i.e.
$options = [
10 => 'First choice',
15 => 'Second choice',
20 => 'Third choice',
];
Any ideas?
This is an ancient question, but today I found it on the top results searching for event listener to modify form choices.
In my context I have an entity programmatically created, and I redirect the user to the editAction to finish filling the fields.
I have one choice that I can apply only in this particular case, I don't want to allow my user to use it outside it.
That's why I user the POST_SET_DATA event, because I already have an entity with populated fields.
This event listener is set in the formType, inside the
public function buildForm(FormBuilderInterface $builder, array $options)
{
Here a working solution for symfony 3.4:
$builder->addEventListener(FormEvents::POST_SET_DATA, function (FormEvent $event) {
// get the form from the event
$form = $event->getForm();
if ('myParticularMode' == $form->get('mode')->getData()) {
// get the field options
$options = $form->get('mode')->getConfig()->getOptions();
// add the mode to the choices array
$options['choices']['MY_PARTICULAR_MODE'] = 'myParticularMode_display_name';
$form->add('mode', ChoiceType::class, $options);
}
});
If you want to replace the choices, you can remove this:
$options = $form->get('mode')->getConfig()->getOptions();
and set a new array for choices.
Related
I'm working on a TagField for EasyAdmin 4 (and Symfony 6) that will rely on a TagType. This TagType will have the native ChoiceType as a parent.
This field will be rendered as a multiple select, with these attributes to allow adding tags on the fly:
[ 'data-ea-widget' => 'ea-autocomplete', 'data-ea-autocomplete-allow-item-create' => 'true' ]
To do so, I created a TagListener. Its main goal is to prefill the options with the already existing tags (on other entities) to support tag suggestion. After reading the docs and many articles, I chose to listen to the FormEvents::PRE_SET_DATA event.
Unfortunately there does not seem to be an easy way to "override" the default options, and we're left with having to override the entire field.
Here's what the TagListener looks like:
<?php
// src/Form/EventListener/TagListener.php
namespace eduMedia\TagBundle\Form\EventListener;
use eduMedia\TagBundle\Entity\TaggableInterface;
use eduMedia\TagBundle\Form\Type\TagType;
use eduMedia\TagBundle\Service\TagService;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
class TagListener implements EventSubscriberInterface
{
public function __construct(private TagService $tagService)
{
}
/**
* #inheritDoc
*/
public static function getSubscribedEvents(): array
{
return [
FormEvents::PRE_SET_DATA => 'onPreSetData',
];
}
public function onPreSetData(FormEvent $event): void
{
$form = $event->getForm();
$parentForm = $event->getForm()->getParent();
/** #var TaggableInterface $taggable */
$taggable = $parentForm->getData();
// We retrieve the existing options to override some of them
$options = $form->getConfig()->getOptions();
// if ($options['pre_set_data_called']) {
// return;
// }
// We prefill options with the existing tags for this resource type
$allTagNames = $this->tagService->getTypeTagNames($taggable->getTaggableType());
// They are our new choices
$options['choices'] = array_combine($allTagNames, $allTagNames);
// We also need to select the entity's tags
$options['data'] = $this->tagService->loadTagging($taggable)->getTagNames($taggable);
// We override the form field
// $options['pre_set_data_called'] = true;
$parentForm->add($form->getName(), TagType::class, $options);
}
}
Doing so seems to create an infinite loop, where onPreSetData is called when calling $parentForm->add(). Is that normal? Is PRE_SET_DATA dispatched again when adding a field in a listener? Is there a way to prevent this from happening?
I tried adding a pre_set_data_called form option, setting it to true when calling $parentForm->add() and exiting the listener when it is indeed true. It kind of works, but then I get this error:
An exception has been thrown during the rendering of a template ("Field "tags" has already been rendered, save the result of previous render call to a variable and output that instead.").
How can I manage to allow extra items in my custom field type?
For reference, here is my TagType class:
<?php
namespace eduMedia\TagBundle\Form\Type;
use eduMedia\TagBundle\Form\EventListener\TagListener;
use eduMedia\TagBundle\Service\TagService;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TagType extends AbstractType
{
public function __construct(private TagService $tagService)
{
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->addEventSubscriber(new TagListener($this->tagService));
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'mapped' => false,
'multiple' => true,
// 'pre_set_data_called' => false,
]);
}
public function getParent()
{
return ChoiceType::class;
}
}
And my TagField class:
<?php
namespace eduMedia\TagBundle\Admin\Field;
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldInterface;
use EasyCorp\Bundle\EasyAdminBundle\Field\FieldTrait;
use eduMedia\TagBundle\Form\Type\TagType;
class TagField implements FieldInterface
{
use FieldTrait;
public static function new(string $propertyName, ?string $label = null)
{
return (new self())
->setProperty($propertyName)
->setLabel($label)
->setFormType(TagType::class)
->setFormTypeOption('attr', [ 'data-ea-widget' => 'ea-autocomplete', 'data-ea-autocomplete-allow-item-create' => 'true' ])
->setTemplatePath('#eduMediaTag/fields/tag.html.twig')
;
}
}
I ended up not using the ChoiceType as the parent (<select> element), but rather the TextType (<input type=text> element), and splitting/exploding a simple string.
The actual bundle is live on GitHub and even though it might not be perfect (yet 😉), the implementation is way simpler and the end-user behaviour is exactly what I expected.
I'm using symfony 4 and I have this form:
<?php
namespace App\Form;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Extension\Core\Type\TimeType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use App\Entity\TypeParking;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class TypeParkingType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('libelle')
->add('tempsmax')
->add('jourdebut')
->add('jourfin')
->add('Exception_Name', TextType::class, ['property_path' => 'exception[name]'])
->add('Starting_date', DateType::class, [
'property_path' => 'exception[datedebut]',
])
->add('Ending_date', DateType::class, [
'property_path' => 'exception[datefin]',
])
->add('Starting_time', TimeType::class, ['property_path' => 'exception[heuredebut]'])
->add('Ending_time', TimeType::class, ['property_path' => 'exception[heurefin]'])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => TypeParking::class,
]);
}
}
When I create a new form I usually take the values of Exception_Name,Starting_date,Ending_date,Starting_time and Ending_time manually and put them into a single json array in a single database field.
However when I go to edit the form, the data inside the json doesn't get divided to populate each of the fields.
let's say that I have this json array: {"Exceptions": {"name": "6fdfs", "StartDate": "2015-03-03", "StartHour": "00:00:00", "EndingDate": "2015-03-03", "EndingHour": "00:00:00"}}
I'm gonna take the name value and use it to populate the Exception_Name field, etc...
TL;DR: How can I control how I want to pre-populate each field of the edit form
Edit:
this is my datamapper
<?php
// src/Form/DataMapper/ColorMapper.php
namespace App\Form\DataMapper;
use App\Painting\Color;
use Symfony\Component\Form\DataMapperInterface;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\FormInterface;
final class TypeParkingMapper implements DataMapperInterface
{
/**
* #param TypeParking|null $data
*/
public function mapDataToForms($data, $forms)
{
// there is no data yet, so nothing to prepopulate
if (null === $data) {
return;
}
/** #var FormInterface[] $forms */
$forms = iterator_to_array($forms);
// initialize form field values
$Excep=$data->getException();
$forms['Exception_Name']->setData($Excep['Exceptions']['name']);
$forms['Starting_date']->setData($Excep['Exceptions']['StartDate']);
$forms['Ending_date']->setData($Excep['Exceptions']['EndingDate']);
$forms['Starting_time']->setData($Excep['Exceptions']['StartHour']);
$forms['Ending_time']->setData($Excep['Exceptions']['EndingHoure']);
}
public function mapFormsToData($forms, &$data)
{
/** #var FormInterface[] $forms */
$forms = iterator_to_array($forms);
// as data is passed by reference, overriding it will change it in
// the form object as well
// beware of type inconsistency, see caution below
$data = new TypeParking(
$forms['Exception_Name']->getData()
);
}
}
So it works fine when I edit an object (since getException() returns the json array from my database), but when I create a new object it throws an error since there's no data to get.
So is there any way I can turn the mapper off when I create a new form and only activate it when editing a form ?
the error is : "Notice: Undefined index: Exceptions"
You could write a DataMapper to fully control how your data is mapped to your form.
I could get this to work prior to v2.8 but as symfony now uses fully qualified class names name i'm unsure sure how to proceed.
I can pass an array (to populate a choice field) to a form without issue but if there is an another formType added via a collectionType how can a pass the array?
BTW - the array is gathered from data from a custom annotations - NOT an entity
Heres my code:
PageType.php
<?php
namespace Prototype\PageBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
const ActiveComponentsType = 'Prototype\PageBundle\Form\ActiveComponentsType';
const collectionType = 'Symfony\Component\Form\Extension\Core\Type\CollectionType';
class PageType extends AbstractType
{
private $cmsComponentArray;
public function __construct($cmsComponentArray = null)
{
$this->cmsComponentArray = $cmsComponentArray;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$cmsComponentArray = $options['cmsComponentArray'];
$componentChoices = array();
foreach($cmsComponentArray as $cmsComponent){
$componentChoices[$cmsComponent['name']] = $cmsComponent['route'];
}
//correct values are shown here
//print_r($componentChoices);
$builder
->add('title')
->add('parent')
->add('template')
->add('active')
->add('content')
->add('components', collectionType, array(
'entry_type' => ActiveComponentsType, // i want to pass $cmsComponentArray to ActiveComponentsType
'allow_add' => true,
'allow_delete' => true
))
;
}
/**
* #param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Prototype\PageBundle\Entity\Page',
'cmsComponentArray' => null
));
}
}
The ActiveComponentsType embeded form does work - except I'm unsure how to pass the $componentChoices array to it.
Any ideas?
The collection type defines the entry_options option which is used to configure the options that are passed to the embedded form type.
how I can get the object attributes that i use to build this form :
controller code
$assistance1 = new Assistance();
$assistance1->setEtudiant($etudiant1);
$form = $this->get('form.factory')->create(new AssistanceType(), $assistance1);
buildform function on the Form class
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('present', 'checkbox',array(
'required' => false,
))
;
}
You could add $this->assistance1; to AssistenceType and do new AssistenceType($assistence1); But this is not good design imo.
class AssistenceType extends AbstractType
{
private $assistence1;
public function __construct($assistence1)
{
$this->assistence1 = $assistence1;
}
public function buidlForm(FormBuilderInterface $builder, array $options)
{
$builder->add('present', 'checkbox', array(
'required' => false,
'label' => $this->assistence1->getEtudiant()
));
}
}
and use it like this
$form = $this->get('form.factory')->create(new AssistanceType($assistence1), $assistance1);
You should keep in mind that building the form is decoupled from it's data. If you need the form to change based on it's properties, you will need to implement different kinds of event listeners or subscribers. Think of it like this: you only need one builder for many forms.
To implement a listener or subscriber on the form, you will most probably be off easiest to first declare your form type as a service, then register the service you will need to alter the form's behaviour, and tag it as an event listener for the form. This is all in the docs :)
However, the Form component of Symfony tends to be one of the most complex, so don't hesitate to clarify your problem a bit more so I can assist you more effectively.
I've got a problem to modify request on a form validation.
I have this entity :
class Ad {
private $id;
/**
* #var Entity\Category
* #ORM\ManyToMany(targetEntity="Entity\Category")
*
*/
protected $category;
}
For lot of case, I need many Category, so I put ManyToMany with checbox forms. But in one case, I need a form with only radio button on Category to get only one.
So I've created the corresponding FormType :
class AdFormType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('category', null, [
'class' => 'Entity\Category',
'required' => true,
'expanded' => true,
'multiple' => false,
'error_bubbling' => true
]);
}
}
The problem is when I do a $form->submit($request), it's failed because it wants ArrayCollection instead of Category entity.
So I've try to create a listener with PRE_SUBMIT Event to modify request, but it failed.
class AdListener implements EventSubscriberInterface {
public static function getSubscribedEvents() {
return array(FormEvents::PRE_SUBMIT => 'onPreBind',FormEvents::POST_SUBMIT => 'onPostBind',FormEvents::SUBMIT => 'onBind');
}
public function onPreBind(FormEvent $event) {
$data = $event->getData();
$data['category'] = [$data['category']];
$event->setData($data);
}
}
Have you some ideas to force ArrayCollection of Category ?
Thanks for helping.
Bouffe
I think what you want to do here is use the collection field type. There's a great cookbook article on this here. That's probably the proper way to implement what you're trying to do anyway.