Symfony2 - Dynamic form choices - validation remove - forms

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;

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.

Array-type property and Form CollectionType with data transformer

I have an entity with a property that is set as an array
/**
* #ORM\Column(type="array")
*/
private $labels = [];
this array of data stores translations of a label like
[
'en' => 'foo-in-English',
'de' => 'foo-in-German',
'ru' => 'foo-in-Russian'
]
I have a Form with the type set for the labels like:
$builder
->add('labels', CollectionType::class);
Note that the entry_type defaults (properly) to TextType here. Left as is, the template would be displayed with text fields, like:
Labels: en: _____FOO IN ENGLISH____
de: _____FOO IN GERMAN_____
ru: _____FOO IN RUSSIAN____
But, I would like the fields to be displayed with the actual language name and not the two-letter code as the label, so something like:
Labels: English: _____FOO IN ENGLISH____
German: _____FOO IN GERMAN_____
Russian: _____FOO IN RUSSIAN____
I also want to make sure that all my selected/supported languages are displayed - even if they currently have no value.
So, this seems like the proper place for a DataTransformer, but try as I might I could not get this concept to work within the Form class. It seems that attempting to transform the data of a collection type is more difficult (or impossible?) than a simpler type like text.
I've overcome this as a workaround by transforming the data within the controller before submitting it to the form and after processing the form before persistence. e.g.
$this->transformTranslations($fooEntity);
$form = $this->createForm(FooType::class, $fooEntity);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$fooEntity = $form->getData();
$this->reverseTransformTranslations($fooEntity);
$this->getDoctrine()->getManager()->persist($fooEntity);
$this->getDoctrine()->getManager()->flush();
...
I'm wondering if anyone has a better method (like how to use normal data or model transformers). I can't seem to find much online about using data transformers with collection types. TIA!
I have not personally used a doctrine array value before, however
you can define a 'default' form class for each of your translation options like so:
AppBundle\Form\LanguageStringEditorType.php
class LanguageStringEditorType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('en', TextareaType::class, ['label' => 'English'])
->add('de', TextareaType::class, ['label' => 'German'])
->add('ru', TextareaType::class, ['label' => 'Russian'])
;
}
}
If you keep the naming ('en', 'de' and 'ru') the same as your data array key names for example having an (doctrine) entity like this:
AppBundle\Entity\LanguageString.php
class LanguageString {
private $identifier;
private $translations; // this is the doctrine array type
// however I didn't feel like setting up a database for this
// test so I'm manually filling it see the next bit
... Getter and setter things ...
And create a type for that as well:
AppBundle\Form\LanguageStringType.php
class LanguageStringType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder
->add('identifier')
->add('translations', LanguageStringEditorType::class, ['label' => 'Translations'])
;
}
}
We can use this in the controller
$data = new LanguageString();
// fill some dummy content (not using database)..
$data->setIdentifier('hello_world');
$data->setTranslations([
'en' => 'Hello world!',
'de' => 'Hallo Welt!',
'ru' => 'Привет мир'
]);
$form = $this->createForm(LanguageStringType::class, $data);
return $this->render('default/index.html.twig', [
'form' => $form->createView()
]);
And the rest is done by magic, no transformers required. The data is placed in the form fields. And set to the entity when using the handleRequest. Just remember that the data key values are the same as the form builder names.
And as a bonus you have defined all your default language fields in the LanguageStringEditorType class, filled in or not.
So, I learned I needed to separate my two needs into different solutions. First I created a new form type to use instead of the text type I was using by default:
$builder
])
->add('labels', CollectionType::class, [
'entry_type' => TranslationType::class
])
This class is very simple and is only an extension of a regular TextType:
class TranslationType extends AbstractType
{
/**
* #var LocaleApiInterface
*/
private $localeApi;
/**
* TranslationType constructor.
* #param LocaleApiInterface $localeApi
*/
public function __construct(LocaleApiInterface $localeApi)
{
$this->localeApi = $localeApi;
}
/**
* {#inheritdoc}
*/
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['label'] = array_search($view->vars['name'], $this->localeApi->getSupportedLocaleNames());
}
public function getParent()
{
return TextType::class;
}
}
This satisfied the labelling issue. Second, to ensure I had all the supported locales in my data, I used a FormEventListener:
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$supportedLocales = $this->localeApi->getSupportedLocales();
$data = $event->getData();
$labels = $data['labels'];
foreach ($supportedLocales as $locale) {
if (!array_key_exists($locale, $labels)) {
$labels[$locale] = $labels['en'];
}
}
$data['labels'] = $labels;
$event->setData($data);
});
This adds the required keys to the data if they are not already present.

symfony Form with multiple steps

I am trying to create a form with multiple steps in that:
First field displayed should be a phone number type.
Once that is submitted I want to perform a lookup to see if there is a user with that phone number.
If a user is found then I need a second field for verification, so I create a zip code field.
I am currently doing this with one controller and one form type:
SearchType:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('phoneNumber', PhoneNumberType::class, array(
'default_region' => 'US',
'format' => PhoneNumberFormat::NATIONAL))
->add("finished", HiddenType::class, array(
"data" => "no"))
->add("submit", SubmitType::class);
$formModifier = function (FormInterface $form) {
$phoneNumber = $form->get('phoneNumber')->getData();
$one = $this->em->getRepository('AppBundle:Phone')->findOneByPhoneNumber($phoneNumber);
if($one){
$form->remove("submit");
$form->remove("finished");
$form->add("zipCode", TextType::class);
$form->add("finished", HiddenType::class, array(
"data" => "yes"));
$form->add("submit", SubmitType::class);
}
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formModifier) {
$formModifier($event->getForm());
}
);
$builder->get('phoneNumber')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $event) use ($formModifier) {
$formModifier($event->getForm()->getParent());
}
);
}
Controller:
$form = $this->createForm(SearchType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid() && $form->get("finished")->getData() == "Yes") {
//...
}
So, I'm not even sure if I am on the right track. I am currently trying to use the Hidden field "finished" to let me controller know when to actually do the work with the submitted data, however "finished" never gets set to "yes" and I am not sure why.
Aside from that, is there a better way for me to be doing this?
I looked into CraueFormFlowBundle but that required the underlying data to be an object (wouldn't work with Array) and I didn't think I wanted it to be in this case since I am just using the data entered to perform a query.

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

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
));
}
}

Correct way to use FormEvents to customise fields in SonataAdmin

I have a Sonata Admin class with some form fields in it, but I'd like to use the FormEvents::PRE_SET_DATA event to dynamically add another form field based on the bound data.
However, I'm running into several problems:
1) The only way I can find to add the form field to the correct 'formGroup' in the admin is by adding the new field twice (once via the formMapper and once via the form itself)... this seems very wrong and I cannot control where in the formGroup it appears.
2) The added element doesn't seem to know that it has a connected Admin (probably because it is added using Form::add()). This means, amongst other things, that it renders differently to the other fields in the form since it triggers the {% if sonata_admin is not defined or not sonata_admin_enabled or not sonata_admin.field_description %} condition in form_admin_fields.html.twig
So, this leads me to believe that I'm doing this all wrong and there must be a better way.
So...
What is the correct way to use a FormEvent to add a field to a form group, ideally in a preferred position within that group, when using SonataAdmin?
Here's some code, FWIW...
protected function configureFormFields(FormMapper $formMapper)
{
$admin = $this;
$formMapper->getFormBuilder()->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($admin, $formMapper) {
$subject = $event->getData();
// do something fancy with $subject
$formOptions = array(/* some cool stuff*/);
// If I don't add the field with the $formMapper then the new field doesn't appear on the rendered form
$formMapper
->with('MyFormGroup')
->add('foo', null, $formOptions)
->end()
;
// If I don't add the field with Form::add() then I get a Twig Exception:
// Key "foo" for array with keys "..." does not exist in my_form_template.html.twig at line xx
$event
->getForm()
->add('foo', null, $formOptions)
;
});
$formMapper
->with('MyFormGroup')
->add('fieldOne')
->add('fieldTwo')
->end()
;
}
The aim is to add the new foo field between fieldOne and fieldTwo in MyFormGroup.
Edit: here's what I came up with with the help of Cassiano's answer
protected function configureFormFields(FormMapper $formMapper)
{
$builder = $formMapper->getFormBuilder();
$ff = $builder->getFormFactory();
$admin = $this;
$formMapper->getFormBuilder()->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($ff, $admin) {
$subject = $event->getData();
// do something fancy with $subject
$formOptions = array(
'auto_initialize' => false,
'class' => 'My\ProjectBundle\Entity\MyEntity',
/* some cool stuff*/
);
$event->getForm()->add($ff->createNamed('foo', 'entity', null, $formOptions));
});
$formMapper
->with('MyFormGroup')
->add('fieldOne')
->add('foo') // adding it here gets the field in the right place, it's then redefined by the event code
->add('fieldTwo')
->end()
;
}
No time here for a long answer, I will paste a piece of code and I don't know if fits exactly in your case, in my it's part of multi dependent selects (country, state, city, neighbor).
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
use Doctrine\ORM\EntityRepository;
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper->add('myfield');
$builder = $formMapper->getFormBuilder();
$ff = $builder->getFormFactory();
$func = function (FormEvent $e) use ($ff) {
$form = $e->getForm();
if ($form->has('myfield')) {
$form->remove('myfield');
}
$form->add($ff->createNamed('myfield', 'entity', null, array(
'class' => '...',
'attr' => array('class' => 'form-control'),
'auto_initialize' => false,
'query_builder' => function (EntityRepository $repository) use ($pais) {
$qb = $repository->createQueryBuilder('estado');
if ($pais instanceof ...) {
$qb = $qb->where('myfield.other = :other')
->setParameter('other', $other);
} elseif(is_numeric($other)) {
$qb = $qb->where('myfield.other = :other_id')
->setParameter('other_id', $other);
}
return $qb;
}
)));
};
$builder->addEventListener(FormEvents::PRE_SET_DATA, $func);
$builder->addEventListener(FormEvents::PRE_BIND, $func);
}