symfony Form with multiple steps - forms

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.

Related

Symfony Forms: Adding a CallbackTransformer to a field that is added in an EventListener

I have a somewhat complex form and am struggling with adding a ModelTransformer to a dynamically added field.
First I have a basic form with some fields and one CollectionType field that includes a custom Type:
class FilterType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
// adding some other fields here ...
$builder->add('conditions', Type\CollectionType::class, [
'entry_type' => FilterRowType::class,
'allow_add' => true,
'prototype' => true,
'allow_delete' => true,
'entry_options' => ['label' => false],
]);
}
}
The FilterRowType consists of several fields that are depending on each other.
First the user has to select an option from a dropdown and then another field is added whose type and options depend on the selected value of the first field.
The second field could be TextType or NumberType or even ChoiceType with its choices again depending on the first field.
Finally I need to add a CallbackTransformer to this second field.
So here is what I currently have (widely stripped of stuff I think is not important for this question):
class FilterRowType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('attribute', Type\ChoiceType::class, [
'choices' => $this->getAttributeChoices(),
]);
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($builder) {
$this->addDynamicInputs($event, $builder);
});
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($builder) {
$this->addDynamicInputs($event, $builder);
});
}
public function addDynamicInputs(FormEvent $event, FormBuilderInterface $builder)
{
$form = $event->getForm();
$data = $event->getData();
// adding some other fields ...
$valueConfig = $this->getValueConfig($data['attribute']);
$form->add('value', $valueConfig['type'], $valueConfig['options']);
$valueConfig['options']['auto_initialize'] = false;
$form->add(
$builder->create('value', $valueConfig['type'], $valueConfig['options'])
->addModelTransformer($this->getCallbackTransformer ())
->getForm()
);
}
}
And this is actually working ! :)
BUT:
As you might already have spotted I am actually adding the 'value' field twice here.
This happened by accident as I added the CallbackTransformer later and forgot to delete the original line.
The problem is that if I now remove the original line $form->add('value', $valueConfig['type'], $valueConfig['options']); I run into an exception:
Neither the property "value" nor one of the methods "value()", "getvalue()"/"isvalue()"/"hasvalue()" or "__call()" exist and have public access in class "Symfony\Component\Form\FormView".
Probably because I set $valueConfig['options']['auto_initialize'] = false; for the new creation of the field?
But if I remove that line I run into a different error:
Automatic initialization is only supported on root forms. You should set the "auto_initialize" option to false on the field "value"
Of course I could leave everything as it is with adding the 'value' field twice.
But that seems a very fishy solution to me and I am afraid that it might have some unforeseen consequences even if currently everything seems to work fine.
So can maybe someone with more insight into symfony forms enlighten me?
Are there possible problems with my 'solution' ?
Is there a better/proper way of doing what I am trying to do?
I had the same case today. Here is what I did :
Create an extension :
namespace App\Form\Extension;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class ModelTransformerExtension extends AbstractTypeExtension {
public static function getExtendedTypes(): iterable {
return [FormType::class];
}
public function buildForm(FormBuilderInterface $builder, array $options) {
parent::buildForm($builder, $options);
if (isset($options['model_transformer'])) {
$builder->addModelTransformer($options['model_transformer']);
}
}
public function configureOptions(OptionsResolver $resolver) {
parent::configureOptions($resolver);
$resolver->setDefaults(array('model_transformer' => null));
}
}
Use it in your form field options :
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$form = $event->getForm();
$form->add('fieldName', $TextType::class, [
'model_transformer' => // Your transformer here
]);
});

Getting user input data in FormEvents::SUBMIT

I am looking for a way to get user data in the formEvents::SUBMIT event.
I can get it in formEvents::PRE_SUBMIT using
$builder->addEventListener(FormEvents::PRE_SUBMIT, function() {
//...
$userInput = $event->getData());
//...
});
I can get it in formEvents::POST_SUBMIT using
$builder->addEventListener(FormEvents::POST_SUBMIT, function() {
//...
$userInput = $event->getForm()->getViewData()
//...
});
But in formEvents::SUBMIT I cannot find where, though I read it can be accessed:
Symfony2: dynamic generation of embedded form
I can find other project getting data using the same method as PRE_SUBMIT, for instance here:
https://github.com/Klerik/web-dersen/blob/65ba1bafa574dc2bda6dd2bb738fe0d33a06bf71/src/Catalog/FilmsBundle/Form/EventListener/UploadFileSubscriber.php
or there:
https://github.com/alexandresalome/bros/blob/59c111eb481c2fd672f3646390455ddad65dd800/src/Bros/ServerBundle/Form/Type/BrowserType.php
Though in my situation I test with user input that does not fit the Normalisation - $event->getForm()['field']->getData() sends an object without the user data which have been refused.
Still I want to retrieve user input to:
* change accordingly (testing data validity) my current form (which cannot be done in POST_SUBMIT),
* use my form related object (not available in PRE_SUBMIT)
EDIT
Here are the relevant parts of the ...Type Class :
public function buildForm(FormBuilderInterface $builder, Array $options)
{
$adminUserQuery = $options['admin_user_query'];
$builder
->add('admin_user', 'model', Array( //using Propel as ORM
'label' => FALSE,
'multiple' => FALSE,
'expanded' => FALSE,
'class' => 'App\\CoreBundle\\Model\\AdminUser',
'query' => $adminUserQuery,
'empty_value' => 'form.placeholder.admin_user',
'property' => 'email',
));
//...
$builder->addEventListener(
FormEvents::PRE_SUBMIT, function (FormEvent $formEvent)
{
var_dump($formEvent->getData(), '---'); // prints: array(1) { ["admin_user"]=> string(17) "test#email.test" }
var_dump($formEvent->getData()['admin_user'], '---'); // prints: string(15) "test#email.test"
//...
});
$builder->addEventListener(
FormEvents::SUBMIT, function (FormEvent $formEvent)
{
var_dump($formEvent->getData()); // prints: object(App\CoreBundle\Model\EventUserRole)#1659 {...}
var_dump($formEvent->getData()->getAdminUser()); // prints: NULL
//...
}
);
}
Anyone ?
In the FormEvents::Submit action, you can do this (assuming you are editing a User):
$builder->addEventListener(FormEvents::SUBMIT, function ($event) {
// Your Entity/Document
$user = $event->getData();
// Grab the actual form
$form = $event->getForm();
// ...
});

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

Symfony 2 basic GET form generated URL

I have been trying to create an extremely basic symfony form (used for search functionality) with only one input. It uses GET method on submit. It seems to work as expected, however it generates an extremely ugly and unnecessarily long URL. I have been trying to 'clean' the URL up for a quite a while now, I was wondering if someone ran into the same problem and knows how to fix it?
Form
$form = $this->createFormBuilder($search)
->setMethod('GET')
->add('q', 'text')
->add('search', 'submit')
->getForm();
On submit the form generates the following URL:
search?form[q]=red+apple&form[search]=&form[_token]=bb342d7ef928e984713d8cf3eda9a63440f973f2
Desired URL:
search?q=red+apple
Thanks in advance!
To create your desired URL, you will have to set the form name by using createNamedBuilder which you'll just leave blank ''.
To remove _token you need to set csrf_protection to false. Please look into csrf protection to make sure you know what could happen if it is turned off.
Changing your code to the following should give you the results you want.
$form = $this->get('form.factory')->createNamedBuilder('', 'form', $search, array(
'csrf_protection' => false,
))->setMethod('GET')
->add('q', 'text')
->add('search', 'submit')
->getForm();
This should produce a URL like:
search?q=red+apple&search=
Edit:
If you want to get rid of &search=, one way would be to change search from submit to button.
->add('search', 'button')
This will require javascript to submit your form.
Here is simple example in jquery:
//This assumes one form and one button
$(document).ready(function(){
$('button').click(function(){
$('form').submit();
});
});
This will produce a URL like:
search?q=red+apple
To access GET vars you put something like this in your controller:
public function yourSearchAction(Request $request)
{
// your code ...
$form->handleRequest($request);
if ($form->isValid()) {
$getVars = $form->getData();
$q = $getVars['q'];
$page = $getVars['page'];
$billing = $em
//Do something
}
return //your code
}
Just to clarify if you are adding page to your URL you will need to add it to your form:
->add('page', 'text')
Old question but, for people who want to know, this does the job too (Symfony 2.8) :
<?php
// src/AppBundle/Form/SearchType.php
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\OptionsResolver\OptionsResolver;
class SearchType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->setMethod('GET')
->add('q', TextType::class)
->add('submit', SubmitType::class))
;
}
public function getBlockPrefix(){
return '';
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'csrf_protection' => false,
]);
}
}
In your controller :
<?php
//...
use AppBundle\Form\SearchType;
//...
public function yourSearchAction(Request $request)
{
$form = $this->createForm(SearchType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$q = $form->get('q')->getData();
// ...
}
// ...
}

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;