Search code examples
ajaxrestsymfonyfosrestbundlenelmiocorsbundle

Symfony2: Save Record From Ajax Form Submission


I am totally lost. There's only so much documentation one can read before it all starts making zero sense.

I want to be able to save form data passed from outside of my Symfony application. I have already installed FOSRestBundle, JMSSerializerBundle, NelmioCorsBundle, etc.

First off, I have a FormType that looks like this:

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        ->add('title')
        ->add('requestDate')
        ->add('deliverDate')
        ->add('returnDate')
        ->add('created')
        ->add('updated')
        ->add('contentChangedBy')
    ;
}

Then I have a REST controller containing the POST method which is supposed to store the new record:

class AvRequestController extends Controller
{
    ...
    public function postAvrequestAction(Request $request){
        $entity = new AvRequest();


        $form = $this->createForm(new AvRequestType(), $entity);
        $form->handleRequest($request);

        if ($form->isValid()) {
            $em = $this->getDoctrine()->getManager();
            $em->persist($entity);
            $em->flush();

            return new \Symfony\Component\HttpFoundation\JsonResponse($entity, Codes::HTTP_CREATED);
        }

        return new \Symfony\Component\HttpFoundation\JsonResponse($request, 400); 
    }
}

Here is the test with the mock ajax form data:

$('#postform').submit(function(event){
    event.preventDefault();
    console.log("submitted");

    ajaxObject = {
        url: $("#postform").attr("action"),
        type: 'POST', // Can be GET, PUT, POST or DELETE only
        dataType: 'json',
        xhrFields: {
            withCredentials: true
        },
        crossDomain: true,
        contentType: "application/json; charset=UTF-8",
        data: JSON.stringify({"id":2, "title":"billabong", "requestDate":"2000-01-01 11:11:11", "deliverDate": "2000-01-01 11:11:11", "returnDate": "2000-01-01 11:11:11", "created": "2000-01-01 11:11:11", "updated": "2000-01-01 11:11:11", "content_changed_by":"cpuzzuol"})
    };

    // ... Add callbacks depending on requests

    $.ajax(ajaxObject)
        .done(function(data,status,xhr) {
            console.log( two );
        })
        .fail(function(data,status,xhr) {
            console.log( status );
        })
        .always(function(data,status,xhr) {
            console.log( data );
        });


    console.log("END");
});

When I submit the form, the 400 Bad Request is tripped in my POST method. Worse, my $request bag is always empty:

{"attributes":{},"request":{},"query":{},"server":{},"files":{},"cookies":{},"headers":{}}

If I do

$request->getContent()

I get my stringified data:

"{\u0022id\u0022:2,\u0022title\u0022:\u0022billabong\u0022,\u0022requestDate\u0022:\u00222000-01-01 11:11:11\u0022,\u0022deliverDate\u0022:\u00222000-01-01 11:11:11\u0022,\u0022returnDate\u0022:\u00222000-01-01 11:11:11\u0022,\u0022created\u0022:\u00222000-01-01 11:11:11\u0022,\u0022updated\u0022:\u00222000-01-01 11:11:11\u0022,\u0022content_changed_by\u0022:\u0022cpuzzuol\u0022}"

I've read that this might have something to do with FOSRestBundle's "body listener" but I've already enabled that:

body_listener: true 

UPDATE

body_listener doesn't seem to play a role at all. As the answer below states, you have to create a form with a blank name since the form you are submitting from outside of the system isn't going to have the name it would normally have if it were made inside of Symfony. Also, make sure to turn off CSRF if you don't have that set up at first.


Solution

  • Form isValid checks also for CSRF token validation. You can turn off csrf token validation in AvRequestType.

    //...
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'AppBundle\Entity\AvRequest',
            'csrf_protection' => false
        ));
    }
    //...
    

    Also, I suggest your form has name. isValid also checks for your form name.

    // form without name
    public function getName()
    {
        return '';
    }
    

    Or

    $form = $this->get('form.factory')->createNamed('', new AvRequestType(), $avRequest);
    

    If you want to create entity, you should send data without id(from JS).

    I have used "JMS serializer" to serialize my entity to json. //Controller

    public function postAvRequestAction(Request $request)
    {
        $avRequest = new AvRequest();
    
        $form = $this->createForm(new AvRequestType(), $avRequest);
        $form->handleRequest($request);
    
        $form = $this->get('form.factory')->createNamed('', new AvRequestType(), $avRequest);
    
        if ($form->isValid()) {
            $em = $this->getDoctrine()->getManager();
            $em->persist($avRequest);
            $em->flush();
    
            $serializer = $this->get('serializer');
            $serialized = $serializer->serialize($avRequest, 'json');
            return new Response($serialized);
        }
    
        return new JsonResponse(array(
            'errors' => $this->getFormErrors($form)
        ));
    }
    
    protected function getFormErrors(Form $form)
    {
        $errors = array();
    
        foreach ($form->getErrors() as $error) {
            $errors['global'][] = $error->getMessage();
        }
    
        foreach ($form as $field) {
            if (!$field->isValid()) {
                foreach ($field->getErrors() as $error) {
                    $errors['fields'][$field->getName()] = $error->getMessage();
                }
            }
        }
    
        return $errors;
    }