Symfony form that generates another form - forms

I have a controller action the prompts the user for some free text input. When submitted the text is parsed into some number of objects that I want to put out on another form for the user to confirm that the initial parsing was done correctly.
Normally after dealing with the response to a form submission we call $this->redirectToRoute() to go off to some other path but I have all these objects laying around that I want to use. If I redirect off someplace else I lose them.
How can I keep them? I tried building my new form right there in the controller action method but then its submission does not seem to be handled properly.
/**
* #Route( "/my_stuff/{id}/text_to_objects", name="text_to_objects" )
*/
public function textToObjects( Request $request, Category $category ) {
$form = $this->createForm( TextToObjectsFormType::class, [
'category' => $category,
]);
$form->handleRequest( $request );
if( $form->isSubmitted() && $form->isValid() ) {
$formData = $form->getData();
$allTheStuff = textParserForStuff( $formData['objectText'] );
$nextForm = $this->createForm( StuffConfirmationFormType::class, $allTheStuff );
return $this->render( 'my_stuff/confirmation.html.twig', [
'form' => $nextForm->createView(),
'category' => $category,
] );
}
return $this->render( 'my_stuff/text.html.twig', [
'form' => $form->createView(),
'category' => $category,
] );
}
This does fine to the point of displaying the confirmation form but when I submit that form I just end up displaying the original TextToObjects form?
To answer albert's question, the TextToObjectsFormType just has three fields, a way to set the date & time for the group of generated objects, a way to select the origin of the objects and a textarea for the textual description. I do not set a data_class so I get an associative array back with the submitted information.
class TextToObjectsFormType extends AbstractType {
public function buildForm( FormBuilderInterface $builder, array $options ) {
$builder
->add( 'textSourceDateTime', DateTimeType::class, [
'widget' => 'single_text',
'invalid_message' => 'Not a valid date and time',
'attr' => [ 'placeholder' => 'mm/dd/yyyy hh:mm',
'class' => 'js-datetimepicker', ],
])
->add( 'objectsOrigin', EntityType::class, [
'class' => ObjectSourcesClass::class,
])
->add( 'objectText', TextareaType::class, [
'label' => 'Copy and paste object description text here',
]);
}
}
How can I get the confirmed, potentially revised, objects back to put them into the database?
Thanks.

Using your current architecture
$nextForm = $this->createForm( StuffConfirmationFormType::class, $allTheStuff );
does not bear enough information. It should contain the action parameter to tell the submission to where route the post request.
In your StuffConfirmationFormType add an 'objectText' field hidden.
Create a confirmation_stuff route
Create an action for this route
In this action if the form is valid => save your stuff ELSE re render TextToObjectsFormType with an action linked to text_to_objects
Be aware that using this technique would not prevent a user to enter non functional data by manually editing the hidden field.
Hope this helps

Related

EventListener adds new fields but Controller doesn't recognize them

I'm trying to add new fields to my form depending on the selected value from a list. My problem comes when clicking on "submit", after the code runs the eventListener method (in which the new field is added), the controller gets the form without the new field! I'm struggling to understand where the problem is.
Here's my form builder:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('DataTypeList', ChoiceType::class, [
'required' => false,
'mapped' => false,
'choices' => [
"String Type" => "DataTypeString",
"Monetary Type" => "DataTypeMonetaryNumber"
],
'attr' => ['style' => 'width: 50vw'],
]);
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
$form = $event->getForm();
$data = $event->getData();
if ($data['DataTypeList'] === "DataTypeString") {
$form->add("DataTypeString", TextType::class, [
'required' => true,
'mapped' => false,
'error_bubbling' => true,
'attr' => [
'placeholder' => 'This is an example',
'style' => 'width: 50vw'
]
]);
}
});
}
This is my controller:
$form = $this->createForm('data\AddType', $data, array(
'action' => $this->generateUrl('data_add', [
'dataKey_id' => $data->getDataAccessKey()->getId(),
'returnRoute' => $returnRoute
]),
'method' => 'POST',
));
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
//Here I make a dump() on $request that shows that my form only contains 'DataTypeList'
}
Thanks in advance for your help!
This works for me :
if ($form->isSubmitted() && $form->isValid()) {
dump($form->get('DataTypeString')->getData()); //return null;
}
It is return null, because you add your field in PRE_SUBMIT and you don't set data, but there is no excpetion, so DataTypeString field exist.
UPDATE
PRE_SUBMIT event is executed before the submit action in PHP.
This is the process
Create form and build
Set Data to the form (this data is the second parameters of createForm method in your controller)
Build form view and display the form on the browser
You fill your form on your browser and submit data
When you handleRequest in your controller, he looks in $_POST variable and see that you click on the 'submit' button of your form, he execute PRE_SUBMIT event HERE, and he do $form->submit and POST_SUBMIT event
In POST_SUBMIT he validate the form and set errors on the form fields
If your form is not valid you don't enter in if($form->isValid()) and you build the form view. And this time you have the DataTypeString fields in your form, because PRE_SUBMIT add the field
Why do you want to add this field in PRE_SUBMIT ?

understanding Zend\Form\Element\Date

I have two issues while using the Zend-Form date element.
First: field binding
The edit action within my controller doesn't fillin an existing date. For example birthday. The field is just empty. (with an element type text, there is no problem).
Here how I instanciated the field:
$this->add([
'name' => 'geburtstag',
'type' => 'date',
'options' => [
'label' => 'Geburtstag:',
'format' => 'dd/mm/yyyy',
],
]);
And here my controller action.
public function addAction()
{
$form = new AnsprechpartnerForm(NULL, $this->db);
$form->get('submit')->setValue('save');
$request = $this->getRequest();
if (! $request->isPost()) {
return ['form' => $form];
}
$ansprechpartner = new Ansprechpartner();
$form->setInputFilter($ansprechpartner->getInputFilter());
$form->setData($request->getPost());
if (! $form->isValid()) {
return ['form' => $form];
}
$ansprechpartner->exchangeArray($form->getData());
$this->ansprechpartnerTable->saveAnsprechpartner($ansprechpartner);
return $this->redirect()->toRoute('ansprechpartner');
}
No inputFilter at the moment, I tried with and without.
Second: validation
I have trouble filling in dates. While I don't use any filters for this field, I would expect, I could fill any date in.
Interesting I get the message double.
I solved it.
The date element expects the format y-m-d. Now I gave it directly to the field after binding the form. The format in the field is now also korrekt.
$form->bind($notizen);
$form->get('submit')->setAttribute('value', 'edit');
$filter = new \Zend\Filter\DateTimeFormatter();
$filter->setFormat('Y-m-d');
$dat = $filter->filter($notizen->datum);
$form->get('datum')->setValue($dat);
Could be more convenient I guess.

Symfony form EntityType::class - How use it to edit data in form (3.2.13)

A dropdown selection menu in a Form is available by using a EntityType form type. It is usefull if you add data. But when you try to edit data with the same FormType class, your $request data are overwritten by your EntityType.
How use the same FormType class when editing the data? (eg. editAction in controller) How pass Request $request data to FormType fields as "defaults, or selected" for element EntityType::class in FormBuilder? Is there something in $builder->add() method I can use like if(['choice_value'] !=== null ) xx : yy?
How to get something like html select selected, from Request object and pass it to xxxFormType class and bind to right EntityType::class element there.
<select>
<option value="volvo">Volvo</option>
<option value="vw">VW</option>
<option value="audi" selected>Audi</option>
</select>
I've looked at EntityType Field,
How to Dynamically Modify Forms Using Form Events and lots of StackOverflow posts, but can't find proper solution.
Controller:
public function editProdPackageAction(Request $request, ProdPackage $prodPackage)
{
$form = $this->createForm(ProdPackageFormType::class, $prodPackage);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$prodPackage = $form->getData();
$em = $this->getDoctrine()->getManager();
$em->persist($prodPackage);
$em->flush();
$this->addFlash('success', 'MSG#011E');
return $this->redirectToRoute('admin_package_list');
}
return $this->render(':admin/forms/ProdPackage:edit.html.twig',
[
'ppg' => $form->createView(),
]
);
}
Formtype:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add(
'idCatBig',
EntityType::class,
[
'label' => '* category',
'class' => 'AppBundle\Entity\ProdCategoryBig',
'choice_value' => 'id',
'choice_label' => 'shortName',
'multiple' => false,
'expanded' => false,
]
)
->add(
'dateStart',
DateType::class,
[
'label' => '* some date',
'data' => new \DateTime('now'),
'choice_translation_domain' => true,
]
)
->add(
'dateEnd',
DateType::class,
[
'label' => '* till',
'data' => new \DateTime('now'),
]
)
->add(
'packageName',
TextType::class,
[
'label' => '* package',
'attr' => ['placeholder' => 'default Name'],
'data' => 'your default value',
]
)
This is what I do in my form, I set a "pre set data listener" to check whether or not this is an edit or a create
function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add(
'dateStart',
DateType::class,
[
'label' => '* some date'
//I don't set a data field here because it is an edit
'choice_translation_domain' => true,
]
)
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
$data = $event->getData();
$form = $event->getForm();
if (!$data || null === $data->getId()) {
//if the entity does not have an ID it means that this is a new entity not an edit. because all edits have been in the database and have an id
$builder->add(
'dateStart',
DateType::class,
['label' => '* some date',
'data' => new \DateTime('now'), //this is a create so I override my previous setting and set a default data
'choice_translation_domain' => true,]
)
}
});
}
I mainly use this trick to change form fields from required to non required between edits and creates for password fields for example,and sometimes if there is something exceedingly complicated. To change data, honestly, setting default data in constructor is cleaner as Stephan suggests
Problem
You are overriding an object by using the data option.
https://symfony.com/doc/3.2/reference/forms/types/entity.html#data:
( ! ) The data option always overrides the value taken from the domain data (object) when rendering. This means the object value is also overriden when the form edits an already persisted object, causing it to lose its persisted value when the form is submitted.
Solution 1: use the controller
So the solution is pretty simple: don't use data. Instead, set default values in your addProdPackageAction action (or whatever it is called):
public function editProdPackageAction(Request $request)
{
$prodPackage = new ProdPackage();
$prodPackage->setDateEnd(new Datetime('now'));
//example: use Request or other controller methods to modify your entity
if ($this->getUser()->hasRole('ROLE_ADMIN')) {
$prodPackage->setCreatedByAdmin(true);
}
//your form
}
Solution 2: use your entity constructor
Alternatively, you can use your entity constructor method:
class ProdPackage
{
//your attributes
private $dateEnd;
public function __construct()
{
$this->dateEnd = new Datetime('now');
}
}
Found another interesting solution
in formTypeclass at $builder
->add(
'trBegin',
DateTimeType::class,
[
'label' => 'Tourney Begins',
'required' => true,
'date_widget' => 'single_text',
'time_widget' => 'single_text',
'date_format' => 'dd.MM.yyyy',
]
)
and continuing building form add:
$builder->get('trBegin')->addModelTransformer(
new CallbackTransformer(
function ($value) {
if (!$value) {
return new \DateTime('now + 60 minutes');
}
return $value;
},
function ($value) {
return $value;
}
)
);
it sets default date at moment when form is created. This method is very usefull also for EntityType object, where you can pass id of field in form and get selected your current real choice from database (not all list from the begining) very useful when using EntityField also for editing forms
$builder->get('productCategory')
->addModelTransformer(new CallbackTransformer(
function ($id) {
if (!$id) {
return;
}
return $this->em->getRepository('AppBundle:ProductCategory')->find($id);
},
function($category) {
return $category->getId();
}
));

How to make a Symfony GET form redirect to route with parameter?

I want to create a form for searching a profile by username which redirect then to the profile page of the user. Btw, I use Symfony 3.2.
I reckon the natural way for doing this would be a GET action form. It would even allow a customer to change the url directly with the good username to see its profile.
Here is the code of my controller :
ProfileController.php
//...
/** #Route("/profil/search", name="profil_search") */
public function searchAction() {
$builder = $this->createFormBuilder();
$builder
->setAction($this->generateUrl('profil_show'))
->setMethod('GET')
->add('username', SearchType::class, array('label' => 'Username : '))
->add('submit', SubmitType::class, array('label' => 'Search'));
$form = $builder->getForm();
return $this->render('profils/profil_search.html.twig', [
'form' => $form->createView(),
]);
}
/** #Route("/profil/show/{username}", name="profil_show") */
public function showAction($username) {
$repository = $this->getDoctrine()->getRepository('AppBundle:User');
$searchedUser = $repository->findOneByUsername($username);
return $this->render('profils/profil_show.html.twig', [
'searchedUser' => $searchedUser,
]);
}
//...
This code will lead to the following error message :
Some mandatory parameters are missing ("username") to generate a URL for
route "profil_show".
I read the documentation thoroughly but couldn't guess, how can I pass the username variable to the profil_show route as a parameter ?
If my way of doing is not the good one, thanks for telling me in comments but I'd still like to know how to use GET forms.
EDIT :
Thanks to #MEmerson answer, I get it now. So for future noobs like me, here is how I did it :
/** #Route("/profil/search", name="profil_search") */
public function searchAction(Request $request) {
$data = array();
$builder = $this->createFormBuilder($data);
$builder
//->setAction($this->generateUrl('profil_show'))
//->setMethod('GET')
->add('username', SearchType::class, array('label' => 'Username : '))
->add('submit', SubmitType::class, array('label' => 'Search'));
$form = $builder->getForm();
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$data = $form->getData();
return $this->redirectToRoute('profil_show', array('username' => $data["username"]));
}
return $this->render('profils/profil_search.html.twig', [
'method' => __METHOD__,
'form' => $form->createView(),
'message' => $message,
]);
}
If you take a look at the error message it says that the problem is where you are trying to generate the URL for the path 'profil_show'.
Your controller annotations require that the URL be populated with a user name
/** #Route("/profil/show/{username}", name="profil_show") */
this means that Symfony is expecting http://yoursite.com/profil/show/username for the route. But if you want to pass it as a GET form posting it really should be expecting http://yoursite.com/profil/show?username
you can add a second route or change your existing route to be
/** #Route("/profil/show", name="profil_show_search") */
that should solve your problem.

How to set as a default value in a Symfony 2 form field the authentified username from the FOSUserBundle

i find this snippet useful indeed to put a default value in my form while creating it
$builder
->add('myfield', 'text', array(
'label' => 'Field',
'data' => 'Default value'))
;
what if i want to replace 'default value' with an authentified person from the FOSUser bundle? ( that return true to is_granted("IS_AUTHENTICATED_REMEMBERED"))
i can retrieve that name on a twig file with
{{ app.user.username }}
i have also done it in a controller method with
$username=$this->container->get('security.context')->getToken()->getUser()->getUsername()
but i can't manage to make this working in my form!
i am not sure i understand that container thing well ...neither how to transfer variables betweenn classes and controller...
something around this maybe??
->add('myfield', 'text', array(
'label' => 'Field',
'data' => FOS\UserBundle\Model::$this->getUsername()))
You can passe variable from your controller to your form :
in your controller :
$username=$this->container->get('security.context')->getToken()->getUser()->getUsername()
$form = $this->createForm(new MyFormType($username), $entity);
in your form :
protected $username;
public function __construct (array $username = null)
{
$this->username = $username ;
}
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('myfield', 'text', array(
'label' => 'Field',
'data' => $this->username))
}
Another way to set default values into a form is to set them on the underlying data object for the form, as in this example from the Symfony documentation on building a form:
public function newAction()
{
// create a task and give it some dummy data for this example
$task = new Task();
$task->setTask('Write a blog post');
$task->setDueDate(new \DateTime('tomorrow'));
$form = $this->createFormBuilder($task)
->add('task', 'text')
->add('dueDate', 'date')
->getForm();
return $this->render('AcmeTaskBundle:Default:new.html.twig', array(
'form' => $form->createView(),
));
}
In this example, the form's underlying data object is a Task and the values set on the task are the default values to be displayed in the form. The task object is not persistent. This approach works just as well with a form class and assuming the underlying object for your form is a User would look something like this:
$username = $this->container->get('security.context')->getToken()->getUser()->getUsername();
$user = new User();
$user->setUsername($username);
// could set any other default values on user here
$form = $this->createForm(new MyFormClass(), $user);
The disadvantage of this approach is if the form is used in many places requiring the same defaults the code would be repeated. This is not a situation I've come across so far - my User forms are re-used for create/edit but edit doesn't require the defaults.