I know this question has been asked already a couple of times, but there hasn't been an answer that actually helped me solving my problem.
I've got three EventSubscribers for three Dropdowns who are dependent on each other.
So in my FormType I say:
public function buildForm(FormBuilderInterface $builder, array $options)
{
// solution showmethecode
$pathToAgencies = 'agencies';
//
$builder
->addEventSubscriber(new AddChannel1Subscriber($pathToAgencies))
->addEventSubscriber(new AddChannel3Subscriber($pathToAgencies))
->addEventSubscriber(new AddAgencySubscriber($pathToAgencies));
}
and one of my EventSubscribers looks like that:
...
...
public static function getSubscribedEvents() {
return array(
FormEvents::PRE_SET_DATA => 'preSetData',
FormEvents::PRE_SUBMIT => 'preSubmit'
);
}
private function addChannel1Form($form, $channel1s = null) {
$formOptions = array(
'class' => 'AppBundle:Channel1',
'property' => 'name',
'label' => 'label.channel1s',
'empty_value' => 'label.select_channel1s',
'mapped' => false,
'expanded' => false,
'translation_domain' => 'UploadProfile',
'multiple' => true,
'required' => false,
'attr' => array(
'class' => 'channel1s'
),
);
if ($channel1s){
$formOptions['data'] = $channel1s;
}
$form->add('channel1s', 'entity', $formOptions);
}
public function preSetData(FormEvent $event) {
$data = $event->getData();
$form = $event->getForm();
if (null === $data) {
return;
}
$accessor = PropertyAccess::createPropertyAccessor();
$agency = $accessor->getValue($data, $this->pathToAgency);
$channel1s = ($agency) ? $agency->getChannel3s()->getChannel1s() : null;
$this->addChannel1Form($form, $channel1s);
}
public function preSubmit(FormEvent $event) {
$form = $event->getForm();
$this->addChannel1Form($form);
}
...
Now I'm getting the error "Attempted to call an undefined method named "getChannel3s" of class "Doctrine\Common\Collections\ArrayCollection"." and (I think) this is because my $data in my preSetData is NULL but I don't know why it's null. Am I looking at the wrong spot or where is my mistake here?
preSetData is executed before the original data (which shall be modified if given) is bound to the form ( which is then stored in $options['data']).
The "data" in preSetData is the one you provide to createForm($type, $data = null, array $options = array()).
So before this is set -> the form obviously doesn't have any data and the event-data isn't set either. That's why $data is null inside your listener's onPreSetData method.
You're using the wrong event. Use preSubmit and build your logic around the data submitted by the user ($event->getData()). This will solve your issue.
Quick overview:
onPreSubmit:
$form->get('someButton')->isClicked() returns false
$event->getForm()->getData() returns $options['data'] if any or $options['empty_data']
$event->getData returns the submitted data (array)
you can use setData()
you can add/remove fields
onSubmit:
You can't use setData() here as data was already bound to the form
$form->isSubmitted() still returns false
$form->get('someButton')->isClicked() returns true
You can still add/remove fields
onPostSubmit:
$form->isSubmitted() returns true
"You cannot remove children from a submitted form"
"You cannot add children to a submitted form"
$form->get('someButton')->isClicked() returns true
In the preSetData declaration you get the bad class. Try this :
public function preSetData(GenericEvent $event)
Add the next use :
use Symfony\Component\EventDispatcher\GenericEvent;
Related
We are using Symfony Forms for our API to validate request data. At the moment we are facing a problem with the CollectionType which is converting the supplied value null to an empty array [].
As it is important for me to differentiate between the user suppling null or an empty array I would like to disable this behavior.
I already tried to set the 'empty_data' to null - unfortunately without success.
This is how the configuration of my field looks like:
$builder->add(
'subjects',
Type\CollectionType::class,
[
'entry_type' => Type\IntegerType::class,
'entry_options' => [
'label' => 'subjects',
'required' => true,
'empty_data' => null,
],
'required' => false,
'allow_add' => true,
'empty_data' => null,
]
);
The form get's handled like this:
$data = $apiRequest->getData();
$form = $this->formFactory->create($formType, $data, ['csrf_protection' => false, 'allow_extra_fields' => true]);
$form->submit($data);
$formData = $form->getData();
The current behavior is:
Input $data => { 'subjects' => null }
Output $formData => { 'subjects' => [] }
My desired behavior would be:
Input $data => { 'subjects' => null }
Output $formData => { 'subjects' => null }
After several tries I finally found a solution by creating a From Type Extension in combination with a Data Transformer
By creating this form type extension I'm able to extend the default configuration of the CollectionType FormType. This way I can set a custom build ModelTransformer to handle my desired behavior.
This is my Form Type Extension:
class KeepNullFormTypeExtension extends AbstractTypeExtension
{
public static function getExtendedTypes(): iterable
{
return [CollectionType::class];
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder->addModelTransformer(new KeepNullDataTransformer());
}
}
This one needs to be registered with the 'form.type_extension' tag in your service.yml:
PrivateApiBundle\Form\Extensions\KeepNullFormTypeExtension:
class: PrivateApiBundle\Form\Extensions\KeepNullFormTypeExtension
tags: ['form.type_extension']
Please note that you still use the CollectionType in your FormType and not the KeepNullFormTypeExtension as Symfony takes care about the extending...
In the KeepNullFormTypeExtension you can see that I set a custom model transformer with addModelTransformer which is called KeepNullDataTransformer
The KeepNullDataTransformer is responsible for keeping the input null as the output value - it looks like this:
class KeepNullDataTransformer implements DataTransformerInterface
{
protected $initialInputValue = 'unset';
/**
* {#inheritdoc}
*/
public function transform($data)
{
$this->initialInputValue = $data;
return $data;
}
/**
* {#inheritdoc}
*/
public function reverseTransform($data)
{
return ($this->initialInputValue === null) ? null : $data;
}
}
And that's it - this way a supplied input of the type null will stay as null.
More details about this can be found in the linked Symfony documentation:
https://symfony.com/doc/current/form/create_form_type_extension.html
https://symfony.com/doc/2.3/cookbook/form/data_transformers.html
So I'm about to create a form with three dropdowns which are interdependent.
When one or several channel1s are selected, the choices for channel 3 should change, according to what is selected for channel1. And dependent on that, the last dropdown "agencies" should change its choices.
I've already tried different solutions but none of them has worked so far. Right now I'm stuck on the solution provided by Symfony's documentation, which provides code for two entities but even with that one, my second dropdown doesn't have any values, so it's not working.
Here is my Form Type:
class SelectionType extends AbstractType {
protected $tokenStorage;
// private $manager;
public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
//solution Symfony Documentation
$channel1s = new Channel1();
$currentuser = $this->tokenStorage->getToken()->getUser();
$builder
->add('channel1s', EntityType::class, array(
'class' => 'AppBundle:Channel1',
'property' => 'name',
'label' => 'label.channel1s',
'empty_value' => 'label.select_channel1s',
'mapped' => false,
'expanded' => false,
'translation_domain' => 'UploadProfile',
'multiple' => true,
'required' => false,
));
$formModifier = function (FormInterface $form, Channel1 $channel1s = null) {
$channel3s = null === $channel1s ? array() : $channel1s->getChannel3();
$form->add('channel3s', EntityType::class, array(
'class' => 'AppBundle:Channel3',
'property' => 'name',
'label' => 'label.channel3s',
'empty_value' => 'label.select_channel3s',
'mapped' => false,
'expanded' => false,
'translation_domain' => 'UploadProfile',
'choices' => $channel3s,
'multiple' => true,
'choices_as_values' => true,
));
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formModifier) {
$data = $event->getData();
$formModifier($event->getForm(), $data->getChannel1s());
}
);
$builder->get('channel1s')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) use ($formModifier) {
$channel1s = $event->getForm()->getData();
$formModifier($event->getForm()->getparent(), $channel1s);
}
);
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'DocumentBundle\Entity\UploadProfile'
));
}
public function getName()
{
return 'uploadprofile';
}
}
I've also tried a solution with Subscribers from that page: http://showmethecode.es/php/symfony/symfony2-4-dependent-forms/ but it didn't work out either..
I think my problem is somewhere around that line:
$channel3s = null === $channel1s ? array() : $channel1s->getChannel3();
but that's just a guess..
I also added that ajax function:
var $channel1s = $('#uploadprofile_channel1s');
$channel1s.change(function() {
var $form = $(this).closest('form');
var data = {};
data[$channel1s.attr('name')] = $channel1s.val();
// data[channel3s.attr('name')] = channel3s.val();
$.ajax({
url : $form.attr('action'),
type: $form.attr('method'),
data : data,
success: function(html) {
$('#uploadprofile_channel3s').replaceWith(
$(html).find('#uploadprofile_channel3s')
);
}
});
});
My 3 entities have ManytoMany or OneToMany relationships and I should have all the right getters and setters, but if anyone needs them to varify, let me know and I will upload them!
I've been stuck on this for quite a while now, so I would be happy about any kind of help or advise!
NOTE: there's still a third entity (agency) to be added but since not even the first one's are working, I decided to upload only the first two..
ADDED:
or maybe somebody can explain to my that line:
$channel3s = null === $channel1s ? array() : $channel1s->getChannel3s();
might be, that this is my problem?
I'm trying to get data stored in a nested form but when calling $builder->getData() I'm always getting NULL.
Does anyone knows what how one should get the data inside a nested form?
Here's the ParentFormType.php:
class ParentFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('files', 'collection', array(
'type' => new FileType(),
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
'by_reference' => false
);
}
}
FileType.php
class FileType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
// Each one of bellow calls returns NULL
print_r($builder->getData());
print_r($builder->getForm()->getData());
die();
$builder->add('file', 'file', array(
'required' => false,
'file_path' => 'file',
'label' => 'Select a file to be uploaded',
'constraints' => array(
new File(array(
'maxSize' => '1024k',
))
))
);
}
public function setDefaultOptions( \Symfony\Component\OptionsResolver\OptionsResolverInterface $resolver )
{
return $resolver->setDefaults( array() );
}
public function getName()
{
return 'FileType';
}
}
Thanks!
You need to use the FormEvents::POST_SET_DATA to get the form object :
$builder->addEventListener(FormEvents::POST_SET_DATA, function ($event) {
$builder = $event->getForm(); // The FormBuilder
$entity = $event->getData(); // The Form Object
// Do whatever you want here!
});
It's a (very annoying..) known issue:
https://github.com/symfony/symfony/issues/5694
Since it works fine for simple form but not for compound form. From documentation (see http://symfony.com/doc/master/form/dynamic_form_modification.html), you must do:
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$product = $event->getData();
$form = $event->getForm();
// check if the Product object is "new"
// If no data is passed to the form, the data is "null".
// This should be considered a new "Product"
if (!$product || null === $product->getId()) {
$form->add('name', TextType::class);
}
});
The form is built before data is bound (that is, the bound data is not available at the time that AbstractType::buildForm() is called)
If you want to dynamically build your form based on the bound data, you'll need to use events
http://symfony.com/doc/2.3/cookbook/form/dynamic_form_modification.html
I have a choice field type named *sub_choice* in my form whose choices will be dynamically loaded through AJAX depending on the selected value of the parent choice field, named *parent_choice*. Loading the choices works perfectly but I'm encountering a problem when validating the value of the sub_choice upon submission. It gives a "This value is not valid" validation error since the submitted value is not in the choices of the sub_choice field when it was built. So is there a way I can properly validate the submitted value of the sub_choice field? Below is the code for building my form. I'm using Symfony 2.1.
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('parent_choice', 'entity', array(
'label' => 'Parent Choice',
'class' => 'Acme\TestBundle\Entity\ParentChoice'
));
$builder->add('sub_choice', 'choice', array(
'label' => 'Sub Choice',
'choices' => array(),
'virtual' => true
));
}
To do the trick you need to overwrite the sub_choice field before submitting the form:
public function buildForm(FormBuilderInterface $builder, array $options)
{
...
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$parentChoice = $event->getData();
$subChoices = $this->getValidChoicesFor($parentChoice);
$event->getForm()->add('sub_choice', 'choice', [
'label' => 'Sub Choice',
'choices' => $subChoices,
]);
});
}
this accept any value
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$data = $event->getData();
if(is_array($data['tags']))$data=array_flip($data['tags']);
else $data = array();
$event->getForm()->add('tags', 'tag', [
'label' => 'Sub Choice',
'choices' => $data,
'mapped'=>false,
'required'=>false,
'multiple'=>true,
]);
});
Adding an alternate approach for future readers since I had to do a lot of investigation to get my form working. Here is the breakdown:
Adding a "New" option to a dropdown via jquery
If "New" is selected display new form field "Custom Option"
Submit Form
Validate data
Save to database
jquery code for twig:
$(function(){
$(document).ready(function() {
$("[name*='[custom_option]']").parent().parent().hide(); // hide on load
$("[name*='[options]']").append('<option value="new">New</option>'); // add "New" option
$("[name*='[options]']").trigger("chosen:updated");
});
$("[name*='[options]']").change(function() {
var companyGroup = $("[name*='[options]']").val();
if (companyGroup == 'new') { // when new option is selected display text box to enter custom option
$("[name*='[custom_option]']").parent().parent().show();
} else {
$("[name*='[custom_option]']").parent().parent().hide();
}
});
});
// Here's my Symfony 2.6 form code:
->add('options', 'entity', [
'class' => 'Acme\TestBundle\Entity\Options',
'property' => 'display',
'empty_value' => 'Select an Option',
'mapped' => true,
'property_path' => 'options.optionGroup',
'required' => true,
])
->add('custom_option', 'text', [
'required' => false,
'mapped' => false,
])
To handle the form data we need to use the PRE_SUBMIT form event.
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$data = $event->getData();
$form = $event->getForm();
if (isset($data['options']) && $data['options'] === 'new') {
$customOption = $data['custom_option'];
// todo: handle this better on your own
if (empty($customOption)) {
$form->addError(new FormError('Please provide a custom option'));
return;
}
// Check for a duplicate option
$matches = $this->doctrine->getRepository('Acme\TestBundle\Entity\Options')->matchByName([$customOption]);
if (count($matches) > 0) {
$form->addError(new FormError('Duplicate option found'));
return;
}
// More validation can be added here
// Creates new option in DB
$newOption = $this->optionsService->createOption($customOption); // return object after persist and flush in service
$data['options'] = $newOption->getOptionId();
$event->setData($data);
}
});
Let me know if ya'll have any questions or concerns. I know this might not be the best solution but it works. Thanks!
you cannot not build the sub_choice validation because during you config its validator you don't know which values are valid (values depend on value of parent_choice).
What you can do is to resolve parent_choice into entity before you make new YourFormType() in your controller.
Then you can get all the possible values for sub_choice and provide them over the form constructor - new YourFormType($subChoice).
In YourFormType you have to add __construct method like this one:
/**
* #var array
*/
protected $subChoice = array();
public function __construct(array $subChoice)
{
$this->subChoice = $subChoice;
}
and use provided values in form add:
$builder->add('sub_choice', 'choice', array(
'label' => 'Sub Choice',
'choices' => $this->subChoice,
'virtual' => true
));
Suppose for sub choices you have id's right ?
Create and empty array with a certain number of values and give it as a choice
$indexedArray = [];
for ($i=0; $i<999; $i++){
$indexedArray[$i]= '';
}
then 'choices' => $indexedArray, :)
I have the following code in my buildForm method of my FormType
$builder->add('privileges', 'entity', array(
'label' => 'Privileges',
'expanded' => true,
'multiple' => true,
'class' => 'AcmeStoreBundle:AdminPrivilege',
'property'=> 'description',
'query_builder' => function(EntityRepository $er) use ($category)
{
return $er->createQueryBuilder('p')
->where('p.categoryid = :categoryID')
->andWhere('p.parentid = -1')
->setParameter('categoryID', $category->getId())
->orderBy('p.position', 'ASC');
}
));
Here if the parentid is greater than -1, then i'd like to show further form components after the checkbox where parentid is greater than -1 is created.
I've searched over Google and have been unable to find a way to do this, can anybody help?
Mat.
If I understand correctly, you can inject parentid and entity manager to form type construct from controller.
So you can run query before add field to builder, and use if-else. For example:
public function __construct($parentId, $em)
{
$this->parentId = $parentId;
$this->em = $em;
}
public function buildForm(FormBuilder $builder, array $options)
{
$choices = $this->em->getRepository()->callNeededMethod();
if($this->parentId){
$builder->add([someFieldParams]);
}else{
$builder->add([anoutherFieldParams]);
}
}