pikachu0
pikachu0

Reputation: 812

Submit a Form using PUT instead of POST

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

Answers (2)

COil
COil

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:

  • The create action uses a POST query.
  • The edit action uses a PUT query.
  • The delete action uses a DELETE query.

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

Andrej Mohar
Andrej Mohar

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:

getMethod()

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

Related Questions