Maintain posted order in symfony2 choice input field (with choice list) - forms

I'm using the Symfony2 framework in my project and use the Form component to create forms. I'm using the choice input field type to enable users to multi select options and I'm using a plugin to enable users to order these options.
Unfortunately the order of these options isn't maintained when posting the form to the controller. The request has the correct order by the Form component uses the order of the choices option.
How can I maintain the posted order using the Form component and choice input field type?
For the record, I did search on Google, Stackoverflow and at Github and I only found an issue about keeping the order of the preferred_choices (https://github.com/symfony/symfony/issues/5136). This issue does speak about a sort option but I can't find this option in the Symfony2 documentation.

I tried to solve same problem : it was needed to select several organizations and sort them in list.
And after $form->getData() my order from request was changed.
I made form event handlers and found that data have right order on FormEvents::PRE_SUBMIT event and I saved it in $this->preSubmitData.
After that, on FormEvents::SUBMIT event I overwrite data with wrong order (in real, it depends on order from choices option) from $this->preSubmitData. (You can remove array_merge from method)
class PriorityOrganizationSettingsType extends AbstractType {
private $preSubmitData;
/**
* #param FormBuilderInterface $builder
* #param array $options
* #throws \Exception
*/
public function buildForm(FormBuilderInterface $builder, array $options)
$builder
->add('organizations', 'choice', array(
'multiple' => 'true',
'required' => false,
'choices' => $this->getPriorityOperatorChoices(),
'attr' => [
'class' => 'multiselect-sortable',
'style' => 'height: 350px; width:100%;'
]
))
;
$builder->addEventListener(FormEvents::SUBMIT, array($this, 'submitEvent'));
$builder->addEventListener(FormEvents::PRE_SUBMIT, array($this, 'preSubmitEvent'));
}
public function preSubmitEvent(FormEvent $event) {
$this->preSubmitData = $event->getData();
}
public function submitEvent(FormEvent $event) {
$event->setData(array_merge(
$event->getData(),
$this->preSubmitData
));
}
}

Related

Symfony4 Forms - How do you conditionally disable a form field?

So what is the best way to have a form render effectively the same form over and over again, with conditionally disabled fields based on the Entity's property values?
I have an Invoice Entity and need a form for creating the invoice, and also the same form with various fields disabled at various stages of the invoicing process (generated, sent, paid etc).
I think the simplest answer is to disable them dynamically in the twig template via form_row options but surely this will affect server side validation of the form as it is not aware the field has been disabled?
What is the best way to disbale a field based on a value in the database?
EDIT 1:
Changed question from Dynamically disable a field in the twig template or seperate class for each form? to Symfony4 Forms - How do you conditionally disable a form field?
Thanks to #Cerad. The answer is in fact Form Events
In the form type (App\Form\InvoicesType for me), add a method call to the end of the builder:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$plus_thirty_days = new \DateTime('+28 days');
$builder
->add('client', EntityType::class, array(
'class' => Clients::class,
'choice_label' => 'name',
'disabled' => false,
) )
// the event that will handle the conditional field
->addEventListener(
FormEvents::PRE_SET_DATA,
array($this, 'onPreSetData')
);;
}
and then in the same class, create a public method named the same as the string in the array (onPreSetData for this example):
public function onPreSetData(FormEvent $event)
{
// get the form
$form = $event->getForm();
// get the data if 'reviewing' the information
/**
* #var Invoices
*/
$data = $event->getData();
// disable field if it has been populated with a client already
if ( $data->getClient() instanceof Clients )
$form->add('client', EntityType::class, array(
'class' => Clients::class,
'choice_label' => 'name',
'disabled' => true,
) );
}
From here you can update the field to be any valid FormType and specify any valid options as you would a normal form element in the From Builder and it will replace the previous one, laving it in the same original position in the form.

TranslatorInterface errors, can't use tooltips in form that is used in another

I am trying to create tooltips for my addressType. I have the following for that.
use Symfony\Component\Translation\TranslatorInterface;
class AddressType extends AbstractType{
/**
* #var TranslatorInterface
*/
private $translator;
/**
* #param TranslatorInterface $translator
*/
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
public function buildForm(FormBuilderInterface $builder, array $options){
$builder->add(
'country','text', array(
'label' => 'address.country',
'translation_domain' => 'messages' ,
'required' => true,
'attr' => array('class'=>"tooltipped",
'data-position' =>"bottom",
'data-delay '=>"50",
'data-tooltip' => $this->translator->trans('help.tooltips.address.country'))
I have done this multiple times for other forms and it works. However the thing is my addressType is being used for another form named customerType, which also houses tooltips and translator as well. the tooltips i use there work fine. But when I add the addresstype it crashes
->add(
'address', new AddressType(),array( // line 84
'label'=>false,
'required' => false,
The error I get is as followed:
Catchable Fatal Error: Argument 1 passed to AppBundle\Form\AddressType::__construct() must implement interface Symfony\Component\Translation\TranslatorInterface, none given, called in C:\Users\KevinDeLeeuw\Documents\GitHub\mountguru\src\AppBundle\Form\CustomerType.php on line 84 and defined
My questions how do I resolve this, or is such a thing not allowed?
You create new AddressType object in your custom form, instead of this pass form name (< v2.8) or class name AddressType::class (for symfony 2.8+)
->add('address', AddressType::class, [...])
And of course you need to define your address form type as service Defining your Forms as Services

Symfony2 - Dynamic form choices - validation remove

I have a drop down form element. Initially it starts out empty but it is populated with values via javascript after the user has made some interactions. Thats all working ok. However when I submit it always returns a validation error This value is not valid..
If I add the items to the choices list in the form code it will validate OK however I am trying to populate it dynamically and pre adding the items to the choices list is not going to work.
The problem I think is because the form is validating against an empty list of items. I don't want it to validate against a list at all. I have set validation required to false. I switched the chocie type to text and that always passes validation.
This will only validate against empty rows or items added to choice list
$builder->add('verified_city', 'choice', array(
'required' => false
));
Similar question here that was not answered.
Validating dynamically loaded choices in Symfony 2
Say you don't know what all the available choices are. It could be loaded in from a external web source?
after much time messing around trying to find it. You basically need to add a PRE_BIND listener. You add some extra choices just before you bind the values ready for validation.
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
public function buildForm(FormBuilderInterface $builder, array $options)
{
// .. create form code at the top
$ff = $builder->getFormFactory();
// function to add 'template' choice field dynamically
$func = function (FormEvent $e) use ($ff) {
$data = $e->getData();
$form = $e->getForm();
if ($form->has('verified_city')) {
$form->remove('verified_city');
}
// this helps determine what the list of available cities are that we can use
if ($data instanceof \Portal\PriceWatchBundle\Entity\PriceWatch) {
$country = ($data->getVerifiedCountry()) ? $data->getVerifiedCountry() : null;
}
else{
$country = $data['verified_country'];
}
// here u can populate choices in a manner u do it in loadChoices use your service in here
$choices = array('', '','Manchester' => 'Manchester', 'Leeds' => 'Leeds');
#if (/* some conditions etc */)
#{
# $choices = array('3' => '3', '4' => '4');
#}
$form->add($ff->createNamed('verified_city', 'choice', null, compact('choices')));
};
// Register the function above as EventListener on PreSet and PreBind
// This is called when form first init - not needed in this example
#$builder->addEventListener(FormEvents::PRE_SET_DATA, $func);
// called just before validation
$builder->addEventListener(FormEvents::PRE_BIND, $func);
}
The validation is handled by the Validator component: http://symfony.com/doc/current/book/validation.html.
The required option in the Form layer is used to control the HTML5 required attribute, so it won't change anything for you, and that is normal.
What you should do here is to configure the Validation layer according to the documentation linked above.
Found a better solution which I posted here: Disable backend validation for choice field in Symfony 2 Type
Old answer:
Just spent a few hours dealing with that problem. This choice - type is really annoying. My solution is similar to yours, maybe a little shorter. Of course it's a hack but what can you do...
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('place', 'choice'); //don't validate that
//... more form fields
//before submit remove the field and set the submitted choice as
//"static" choices to make "ChoiceToValueTransformer" happy
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$data = $event->getData();
$form = $event->getForm();
if ($form->has('place')) {
$form->remove('place');
}
$form->add('place', 'choice', array(
'choices' => array($data['place']=>'Whatever'),
));
});
}
Add this inside buildForm method in your form type class so that you can validate an input field value rather a choice from a select field value;
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) {
$form = $event->getForm();
if ($form->has('verified_city')) {
$form->remove('verified_city');
$form->add(
'verified_city',
'text',
['required' => false]
)
}
}
);
Update in Validations.yml
Kindly update the Validation.yml file in the below format : setting the group names in the each field
password:
- NotBlank: { message: Please enter password ,groups: [Default]}
Update in Form Type
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver) {
$resolver->setDefaults(array(
'data_class' => 'RegistrationBundle\Entity\sf_members',
'validation_groups' => function(FormInterface $form){
$data = $form->getData();
$member_id = $data->getMemberId();
// Block of code;
// starts Here :
if( condition == 'edit profile') {
return array('edit');
} else
{
return array('Default');
}
},
Update in Entity
/**
* #var string
*
* #ORM\Column(name="password", type="text")
* #Assert\Regex(
* pattern="/(?i)^(?=.[a-zA-Z])(?=.\d).{8,}$/",
* match=true,
* message="Your password must be at least 8 characters, including at least one number and one letter",
* groups={"Default","edit"}
* )
*/
private $password;

Symfony Form choices customize the get url

So I have a search bar form that I need to temporarly wire up to a legacy non-symfony page.
the current get url looks like the following (url-decoded)
http://localhost:9090/lagacy_page?query=test&platforms[]=Mac,Windows
but I need to make the url look like the following
http://localhost:9090/lagacy_page?query=test&platforms=Mac,Windows
Symfony is making platforms an array, does anyone no if there is a way to force it to be a comma delimitated list?
Here is the buildForm method
/**
* method to build search bar form
*
* #param \Symfony\Component\Form\FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
// the platform selector
$builder->add('platform', 'choice',
['choices' => [
Platforms::ALL => 'All Software', // TODO: need to translate this
Platforms::WINDOWS => 'Windows',
Platforms::MAC => 'Mac',
Platforms::IOS => 'iOS',
Platforms::ANDROID => 'Android',
],
'multiple' => true,
'expanded' => true]);
// the actual search bar
$builder->add('query', 'search');
}
You will want to override how Symfony2 renders the choice field.
The documentation has plenty of information about how to customize Form rendering.
If only the choice type of search form needs this, you will need to create a custom type in order to avoid conflicts with the other forms of your website.
In short, if you override the choice type using the first doc and you do not use a custom type every choice type will use the same behavior (the one you will create for your search form) and you probably don't want that.
An easy alternative solution would be to apply a custom form_div_layout.html.twig file directly to the form object. There wouldn't be any conflicts with other forms as you would use a custom template just for the search form.
After reading the docs my answer will make more sense and you will be able to solve your problem.
You have to use two form elements, since Symfony doing it in right way (according to HTML specifcation)
/**
* method to build search bar form
*
* #param \Symfony\Component\Form\FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
// the platform selector
$builder->add('platform_choice', 'choice',
['choices' => [
Platforms::ALL => 'All Software', // TODO: need to translate this
Platforms::WINDOWS => 'Windows',
Platforms::MAC => 'Mac',
Platforms::IOS => 'iOS',
Platforms::ANDROID => 'Android',
],
'multiple' => true,
'expanded' => true,
'attr' => [
'class' => 'platform-sorce'
])
->add('platform', 'hidden', [
'attr' => [
'class' => 'real-platform'
]
]);
// the actual search bar
$builder->add('query', 'search');
}
Then add JS updating you hidden field, since 'platform_choice' is disabled and wont be send.
$(function(){
var $real_platform = $('.real-platform'),
$platform_source = $('.platform-source');
$platform_source.change(function(){
$real_platform.val($(this).val().join(',');
});
$('#your-form").submit(function(){
$platform_source.attr('disabled', true);
return true;
});
});

Symfony2 Doctrine2 Many To Many Form not Saving Entities

I am having some trouble with a many to many relationship. I have Users and Assets. I would like to be able to assign users to an asset on the asset page.
The code below displays a list of users when creating/editing an asset, however changes made to the user checkboxes do not save, while the rest of the data is persisted.
If I add an entry to users_assets through the mysql client, these changes are shown in the asset list.
User
class User extends BaseUser
{
/**
* #ORM\ManyToMany(targetEntity="Asset", inversedBy="users")
*/
private $assets;
}
Asset
class Asset
{
/**
* #ORM\ManyToMany(targetEntity="User", mappedBy="assets")
*/
private $users;
}
AssetType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$form = $builder
->add('users', null, array(
'expanded' => true,
'multiple' => true
))
->getForm();
return $form;
}
For some reason I had to switch the doctrine mappings to get this to work:
Asset:
/**
* #ORM\ManyToMany(targetEntity="Adaptive\UserBundle\Entity\User", inversedBy="assets")
* #ORM\JoinTable(name="user_assets")
*/
private $users;
User:
/**
* #ORM\ManyToMany(targetEntity="Splash\SiteBundle\Entity\Asset", mappedBy="users")
*/
private $assets;
Now when I save the asset it saves the users associated. I did not need to define builder->add as an entity or collection. I simply pass it null and it uses the mapping info to fill in the entity info:
AssetType:
->add('users', null, array('expanded' => "true", "multiple" => "true"))
Not exactly sure why I needed to have the inversedBy and JoinTable info on the Asset vs The User but it seems to be working now!
Thanks For The Suggestions!!!
Weird enough I faced the same problem in 2016 and still had hard time finding the solution. I will share it for future googlers:
The problem is that what symfony essentially does when you save the form is this:
$asset->getUsers()->add($user)
And because you're on the inverse side of the relation it won't persist your changes.
What you really need is to make so that it calls this:
$asset->addUser($user)
Where addUser() is defined the following way on the Asset entity:
public function addUser(User $user)
{
//add to the inverse side
$this->users->add($user);
//add on the owning side (only this is persisted)
$user->addAsset($this); //$user->assets->add($asset);
}
So in order to make symfony use that $asset->addUser() method, you should set
'by_reference' => false
on your users field for AssetType form.
More about this setting here http://symfony.com/doc/current/reference/forms/types/form.html#by-reference
Remember you also need to define removeUser() method in the same way (so that it removes entity from the owning relation)
Not exactly sure why I needed to have the inversedBy and
JoinTable info on the Asset vs The User but it
seems to be working now!
The reason why your changes has been ignored is that doctrine persists only changes by the owning side of a relation (like #Florian said).
This is the link to Doctrine's documentation where this behaviour is explained: http://docs.doctrine-project.org/en/latest/reference/unitofwork-associations.html
At first you should drop backslash prefix in annotations (see notice here).
And you need to use entity field type:
$builder->add('users', 'entity', array(
'class' => 'AdaptiveUserBundle:User',
'expanded' => true,
'multiple' => true,
'query_builder' => function(EntityRepository $er) {
return $er->createQueryBuilder('u')
->orderBy('u.username', 'ASC');
},
));
You need to use 'collection' field type in your form.
$builder->add('users', 'collection', array(
'type' => new UserType(),
'prototype' => true,
'allow_add' => true,
'allow_delete' => true
));
You need to create the UserType() form first obviously.
Here is all the info you will need, including code samples:
http://symfony.com/doc/current/cookbook/form/form_collections.html
http://symfony.com/doc/current/reference/forms/types/collection.html