Reputation: 812
I have a controller which processes form submission from AJAX requests. I don't want to repeat myself, so I put the form processing code in a method:
// Should process POST request
public function create(Request $request)
{
return $this->processEdit($request);
}
// Should process PUT request
public function update($id, Request $request)
{
$entity = $this->findEntity($id); // custom method
if (!$entity)
return $this->myCustomErrorResponse();
return $this->processEdit($request, $entity);
}
private function processEdit(Request $request, Entity $entity = null)
{
$form = $this->createForm('my_entity', $entity);
$form->handleRequest($request);
if ($form->isValid()) {
// Do something
} else {
// Handle invalid form
}
return $response;
}
I have the following two routes:
ajax_create:
pattern: /
defaults: { _controller: 'MyBundle:Ajax:create' }
methods: [ POST ]
ajax_update:
pattern: /{id}
defaults: { _controller: 'MyBundle:Ajax:update' }
methods: [ PUT ]
requirements:
id: \d+
However, when I submit the form via AJAX, it will not accept PUT
request and return form is not valid without any form error message. If I alter the controller code abit,
$form = $this->createForm('my_entity', $entity, array(
'method' => 'PUT',
));
... it will process PUT
request but not POST
request.
I wondered which part of Symfony2 does the HTTP method checking for a form so I tried to look for the answer in the source code, but I couldn't find a clue. Could any of you share your knowledge please?
Another question, is there a way to bypass HTTP method checking? I'm currently passing $method
to the method shown above.
Thank you very much.
Update:
To make my question clearer, my Symfony2 application routes the request (both POST and PUT) to the correct controller method.
I mentioned the altered code above, here it is:
// Should process POST request
public function create(Request $request)
{
return $this->processEdit($request);
}
// Should process PUT request
public function update($id, Request $request)
{
$entity = $this->findEntity($id); // custom method
if (!$entity)
return $this->myCustomErrorResponse();
return $this->processEdit($request, 'PUT', $entity);
}
private function processEdit(Request $request, $method = 'POST', Entity $entity = null)
{
$form = $this->createForm('my_entity', $entity, array(
'method' => $method,
));
$form->handleRequest($request);
if ($form->isValid()) {
// Do something
} else {
// Handle invalid form
}
return $response;
}
Upvotes: 3
Views: 2647
Reputation: 7606
[EDIT 2014-05-23] I have entirely modified my first answer as it was a "dirty hack".
I had exactly the same issue (and almost the same code). I have read the answers here and I found a major problem in my own code, I forgot to modify the /web/app.php
files to enable by default the HttpMethodParameterOverride
parameters. (It's a change introduced in Symfony2.2)
Now everything works as expected using the handleRequest()
function:
I don't need to modify the RequestHandler
configuration as suggested in the accepted answer.
Now the code looks like this:
/**
* Fruits CRUD service controller.
*
* @Route("/services/fruits")
*/
class FruitsController extends Controller
{
// ...
/**
* Create a fruit.
*
* @param Request $request
*
* @Rest\Post("", name="backend_fruits_create")
*
* @return View|array
*/
public function createAction(Request $request)
{
return $this->processForm($request, new Fruit());
}
/**
* Edit a fruit.
*
* @param Request $request
* @param Fruit $fruit
*
* @Rest\Put("/{id}", name="backend_fruits_edit", requirements={"id" = "\d+"})
* @throws HttpException
*
* ## DEV FORM ##
* @Rest\Get("/edit/{id}", name="backend_fruits_edit_dev", requirements={"id" = "\d+"})
* @Rest\View
* ## DEV FORM ##
*
* @return View|array
*/
public function editAction(Request $request, Fruit $fruit)
{
return $this->processForm($request, $fruit);
}
/**
* Delete a fruit.
*
* @param Fruit $fruit
*
* @Rest\Delete("/{id}", name="backend_fruits_delete")
* @throws HttpException
*
* @return View
*/
public function deleteAction(Fruit $fruit)
{
$fruit->delete();
return $this->responseHelper->createSuccessResponse(
$fruit->getTree()->getFruits(),
Response::HTTP_ACCEPTED
);
}
/**
* Form handling.
*
* @param Request $request
* @param Fruit $fruit
*
* @return View|array
*/
protected function processForm(Request $request, Fruit $fruit)
{
list($statusCode, $httpMethod, $action) = $this->getActionParameters($fruit);
$form = $this->createForm(
new FruitType(), $fruit,
array('action' => $action, 'method' => $httpMethod)
);
if (in_array($request->getMethod(), array('POST', 'PUT'))) {
if (!$form->handleRequest($request)->isValid()) {
return $this->responseHelper->createErrorResponse($form);
}
$form->getData()->save();
return $this->responseHelper->createSuccessResponse($form->getData(), $statusCode);
}
return compact('form');
}
/**
* Set the form and action parameters depending on the REST action.
*
* @param Fruit $fruit
*
* @return array
*/
protected function getActionParameters(Fruit $fruit)
{
if ($fruit->isNew()) {
$statusCode = Response::HTTP_CREATED;
$httpMethod = 'POST';
$action = $this->generateUrl('backend_fruits_create');
} else {
$statusCode = Response::HTTP_OK;
$httpMethod = 'PUT';
$action = $this->generateUrl('backend_fruits_edit', array('id' => $fruit->getId()));
}
return array($statusCode, $httpMethod, $action);
}
Note: The form type is bound to a model entity.
Note2: As you can see, I have an additional GET route. It is useful when developing as I can debug my form in my browser. As this a service constroller, I will delete the related code when finised; the route and in the processForm
function, no need to test the method and to return the form anymore.
/**
* Form handling.
*
* @param Request $request
* @param Fruit $fruit
*
* @return mixed
*/
protected function processForm(Request $request, Fruit $fruit)
{
list($statusCode, $httpMethod, $action) = $this->getActionParameters($fruit);
$form = $this->createForm(
new FruitType(), $fruit,
array('action' => $action, 'method' => $httpMethod)
);
if (!$form->handleRequest($request)->isValid()) {
return $this->responseHelper->createErrorResponse($form);
}
$form->getData()->save();
return $this->responseHelper->createSuccessResponse($form->getData(), $statusCode);
}
Note3: The response helper just creates a custom View
response object with the FOSRestBundle.
More about REST and Symfony2:
Upvotes: 3
Reputation: 985
Just a couple of (hopefully) helpful notes:
First things first, you can get the submit method from the Request object, no need to pass it separately:
Secondly, I think I found the part of the code you were looking for. First off, if you check the handleRequest
call in the Symfony Form class, you can see it calls the handleRequest
from the RequestHandler
class situated in the config (check FormConfigInterface
class). I'm guessing that the correct implementation of the RequestHandlerInterface
is NativeRequestHandler. You can see there on the line 48 the check for the equality of request methods.
Now, to handle this, you might be able to set the form's FormConfigInterface to a custom value, where you crafted the RequestHandler to your own implementation. If the NativeRequestHandler
is defined as a service, then you're in luck (currently I don't have access to the list of services). Just switch the class to point to your own implementation.
Having said all this, I think form types check is there for a reason. You should handle your form submit types separately as you are doing now. Also, using POST for inserting and editing is quite a good solution. The simpler the better, less chance of introducing new bugs!
Upvotes: 3