Symfony2: KnpPaginator only show the first page with POST form - symfony

I'm using this bundle in an application. The controller is the typical that shows a search form, take the response and process it (an example):
public function indexAction()
{
$request = $this->getRequest();
$example = new Example();
$form = $this->createForm(new ExampleFindType(), $example, array(
'action' => $this->generateUrl('example_find'),
'method' => 'POST',
));
$form->handleRequest($request);
if ($form->isValid())
{
$em = $this->getDoctrine()->getManager();
$examples = $em->getRepository('ApplicationExampleBundle:Example')
->find_by_fields($example);
$paginator = $this->get('knp_paginator');
$pagination = $paginator->paginate(
$examples,
$this->get('request')->query->get('p', 1),
20
);
return $this->render('ApplicationExampleBundle:Default:searchResults.html.twig',
array('pagination' => $pagination));
}
return $this->render('ApplicationExampleBundle:Default:index.html.twig',
array('form' => $form->createView(),
));
}
When I perform the search I see the results list and the paginator correctly. The problem appears when I press the link to the next page. The link id generated well, with the URL ending with "?p=2" but it seems that the form POST data is not resent because it sent me to the search form page ($form->isValid() is false).
If I change the form method from POST to GET and pass the parameters in the URL:
$form = $this->createForm(new ExampleFindType(), $example, array(
'action' => $this->generateUrl('example_find'),
'method' => 'GET',
));
the paginator works perfect.
Am I doing something wrong? Is possible to use a POST form?
I've searched an answer but all the KnpPagintor controller examples I've seen don't generate the query with forms, and this question hasn't helped me.
Thanks.

You shouldn't use POST method to get data.
Otherwise, if you need to use the POST method then you need the data in the session. However it's difficult to build a nice user experience while it just makes more sense to use a GET method.
You can find an extensive documentation about HTTP on MDN.
A GET method should be used when you request data.
A POST method should be used when you save data (like saving a comment into a database) or other data manipulation.
Google uses a GET on their own search page.
https://www.google.com/#q=symfony&start=10
q is what I searched for and start is the paginator value. They probably use an offset instead of a page number to avoid calculating the offset (faster and less expensive).

Related

Handle PUT requests with Symfony 4 Form

I used to use edit and update methods in my controller to submit and handle a PUT form submission. It works fine and the code looks like this,
public function edit(Category $category): Response
{
$form = $this->createForm(CategoryType::class, $category, [
'action' => $this->generateUrl('category_update', [
'id' => $category->getId(),
]),
'method' => 'PUT',
]);
return $this->render('category/edit.html.twig', [
'category_form' => $form->createView(),
]);
}
public function update(Category $category, Request $request): Response
{
$form = $this->createForm(CategoryType::class, $category, ['method' => 'PUT']);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$entityManager = $this->getDoctrine()->getManager();
$entityManager->flush();
return new Response('', Response::HTTP_NO_CONTENT);
}
return new Response('', Response::HTTP_BAD_REQUEST);
}
As PUT is not supported by HTML forms, the edit request uses POST with a '_method' parameter as 'PUT' instead of a real PUT request.
Now I want to remove the edit method and send a real PUT request from the frontend. When I used Postman to test this, I found the update method cannot handle a real PUT request.
When I use Postman to send POST + '_method'='PUT' requests, it works fine, but when I send PUT requests, it shows BAD_REQUEST, which is the last line in my code. isSubmitted() returns false.
I know I don't need to use Forms here, but it's been used in the store method. Is it possible to use it to handle a real PUT request? What should I change in my update method?
Seems like you're missing $entityManager->merge($category); in the update() method. Try adding it above $entityManager->flush(); and let us know if it works.
You need to write _method instead of method
$form = $this->createForm(CategoryType::class, $category, ['_method' => 'PUT']);
Also you need to persist object, before flushing

Render controller and get form errors from child

I have a template where I render a widget which contains a form:
{{ render(controller('ApplicationDemoBundle:Demo:newWidget', {'demo' : entity })) }}
The newWidgetAction calls a createAction:
public function createAction(Request $request)
{
$entity = new Demo();
$form = $this->createCreateForm($entity);
$form->handleRequest($request);
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($entity);
$em->flush();
return $this->redirect($this->generateUrl('demo_show', array('id' => $entity->getId())));
}
return array(
'entity' => $entity,
'form' => $form->createView(),
)
// Something like this would be awesome with passing the form containing errors with
// return $this->redirect($this->getRequest()->headers->get('referer'));
}
Imagine the submitted form (user acts in show theme) produces an error. This would make a return to the newWidget template which does not display the full layout.
My question is now: What is the right way to pass the errors from the child controller (newWidget) to the main template (show)?, without modifying the showActions function parameter to pass the formerrors over there.
There is a similar thread to this question: Symfony2 render and form
In this case where sessions used but I'm more than curious if this is the way to go.
The problem is that each fragment (a sub-controller) uses a virtual request. This is to protect the original request from modification by possibly unexpected forwards, and a fragment is essentially a forward taking place during a rendering stage.
It is possible to access the top level request using:
$this->container->get('request'); then handing the request with the form in the fragment, but this may get very confusing very quickly if you are using multiple forms per page.
My strategy is to follow a convention that limits the number of validated form on a page to just one. Any other forms don't require validation, or it is otherwise impossible for a form to be submitted incorrectly (hacked forms would throw server side exceptions, but the user should only see those if they are being naughty).
Try to structure your template inheritance to accommodate navigation to forms, while always showing most of the same layouts and data. You can do this by expanding the use of fragments which gives the bonus of separating your display logic.

Symfony2 confirm form submission

I need to be able to do an operation on form data before it gets persisted to the database. The problem is, the operation can be risky and I need the user's consent each time.
I'd like to do this through a confirmation form (with a little message explaining what's going on), not with a Javascript confirm window.
How can I achieve this functionality?
Here is an example of a controller action method that handles the form:
<?php
public function indexAction(Request $request)
{
...
$form = $this->createFormBuilder($myEntity)
->add('someField', 'integer', array('required' => true))
// Lots and lots of fields
->getForm();
if ($request->isMethod('POST')) {
$form->bind($request);
if ($form->isValid()) {
// CUSTOM VALIDATION HERE
// If invalid, must display a new confirmation form to ask user
// if it's alright to do a somewhat risky operation that would
// validate the form data.
// Else, persist the data.
$db = $this->getDoctrine()->getManager();
$db->persist($myEntity);
$db->flush();
return $this->redirect($this->generateUrl('my_path_to_success_page'));
}
}
return $this->render('MyBundle:Preferences:index.html.twig', array(
'form' => $form->createView(),
'errors' => $form->getErrors(),
));
}
You might want to look into CraueFormFlowBundle, which allows you to create multi-step forms.

Symfony2 Forms - Cannot add/remove entities from a collection

I have a form for a "Person" entity that has a collection of "Nicknames".
I have implemented this following the guide in the cookbook (http://symfony.com/doc/current/cookbook/form/form_collections.html)
When I create a new "Person" I can add "Nicknames" as expected successfully.
Using a similar form I have an edit page. On the edit page I have set up the front end in the same way as the new page.
On the edit page, I can edit the existing nicknames and the other simple fields successfully. However, when I submit a new "Nickname" (or try to remove one), it does not work.
I have var_dump()'ed the request object before it is binded to the "Person" and I have var_dump()ed the "Person" object after binding. The request does have the "Nicknames" collection passed correctly. However, they are not being binded to the Person at all, despite the fact that everything is the same as the new page. Further, I can still edit the existing Nicknames so it is working on that front.
I cannot for the life of me figure out why Symfony isn't creating these new Nicknames or adding them to the Person as it should. I have looked around and searched but everything just confirms that it should be working - but it isn't.
Heres the entry for the Nicknames on the Person form type:
->add('nicknames', 'collection', array(
'type' => new NicknameType(),
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
'by_reference' => false,
'required' => false
))
Nickname type only has one field - "nickname" and its a text input.
Any help? Thanks for reading.
EDIT:
Here is the update action (changed Person to Player):
public function updateAction($id, Request $request)
{
$em = $this->getDoctrine()->getManager();
$player = $em->getRepository('SCDBAppBundle:Player')->find($id);
if (!$player) {
throw $this->createNotFoundException('Unable to find Player.');
}
$editForm = $this->createForm(new EditPlayerType(), $player);
$deleteForm = $this->createGenericIDForm($id);
$editForm->bindRequest($request);
if ($editForm->isValid()) {
$em->persist($player);
$em->flush();
return $this->redirect($this->generateUrl('admin_player_edit', array('id' => $id)));
}
return array(
'player' => $player,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
);
}
I realize that to actually persist removing nicknames I need to change the code to persist. I'm not even worried about persistence right now, because the main issue is that after I bind the request to the Player form, the $player object does not contain the updated Nicknames collection.
If you are not using Symfony master but 2.0.x, beware, because the "collection" type is buggy there. If you are using Symfony master however, make sure to include a recent version of Doctrine (2.2.3 or 2.1.7) because there two bugs have been fixed that affect the correct working of the "collection" type.
Maybe try to use NicknameType::class instead of new NicknameType().
And I suggest to add cascade: [ persist ] conf attribute to your entity.
You have to get the data from the request with:
$objPerson = $editForm->getData();
$em->persist($objPerson);
$em->flush();
Otherwise you update the old object without changes from the form.
Short: You have only updated the object which was loaded from the database before.

Symfony2 Functional test: Passing form data directly

I am using phpunit to run functional tests but I am having a problem with a few of the forms. The problem is that phpunit is not aware of JS, and I have a form with a dynamically populated select box that needs jQuery.
So I need to pass the form data directly. The 'book' gives the following example:
// Directly submit a form (but using the Crawler is easier!)
$client->request('POST', '/submit', array('name' => 'Fabien'));
When I used this example the controller didn't receive any of the form data. Intially I saw that passing the array key 'name' wasn't correct in my situation as I needed the form name which was 'timesheet' in my code. So I tried something like:
$client->request('POST', '/timesheet/create', array('timesheet[project]' => '100'));
But this still didn't work. In the controller I tried to understand what was happening and what if anything was being received:
$postData = $request->request->get('timesheet');
$project = $postData['project'];
This didn't work and $project remained empty. However if I used the following code I got the value:
$project = $request->request->get('timesheet[project]');
But clearly that's not what I want. Atleast though I can see that there is some POST data. My last attempt was to try the following in the test method:
$this->crawler = $this->client->request('POST', '/timesheet/create/', array('timesheet' => array(project => '100'));
So I am trying to pass a 'timesheet' array as the first element of the request parameter array. But with this I get the error:
Symfony\Component\Form\Exception\UnexpectedTypeException: Expected argument of type "array", "string" given (uncaught exception) at /mnt/hgfs/pmt/src/vendor/symfony/src/Symfony/Component/Form/Form.php line 489
I would be very happy if someone can expand on what's in the 'book' about how I am supposed to get this working.
Form bind in controller:
if ($request->getMethod() == 'POST') {
$form->bindRequest($request);
if ($form->isValid()) {
$postData = $request->request->get('timesheet');
$project = $postData['project'];
$timesheetmanager = $this->get('wlp_pmt.timesheet_db_access');
$timesheetmanager->editTimesheet($timesheet);
return $this->redirect($this->generateUrl('timesheet_list'));
}
}
If you are wanting to know how to inject arrays of POST data using the test client...
In your test method, do something like
$crawler = $client->request('POST', '/foo', array(
'animal_sounds' => array(
'cow' => 'moo',
'duck' => 'quack'
)
); // This would encode to '/foo?animal_sounds%5Bcow%5D=moo&animal_sounds%5Bduck%5D=quack'
$this->assertTrue( ... );
In the controller, you would access your params like this:
$data = $request->request->get('animal_sounds');
$cowNoise = $data['cow'];
$duckNoise = $data['duck'];
Or you could just use the forms API if the test method was injecting valid form data...
do you have a $request parameter in your action?
that was the reason why my request->get() was empty:
//WRONG
public function projectAction()
{
$request = Request::createFromGlobals();
$project = $request->request->get('timesheet[project]');
//$project will be empty
}
//CORRECT
public function projectAction(Request $request)
{
$project = $request->request->get('timesheet[project]');
//$project is not empty
}
see
How do I create a functional test which includes a POST to a page with parameters?
Try to use $form->bind($clientData) instead of $form->bindRequest($request).

Resources