Handling CamelCase with FOSRestBundle

- 3 minute read

The default naming strategy of JMSSerializerBundle is “camel_case”, which means your entity fields in camel case are normalized to their underscore equivalent when serialized. When rendering entities the ViewHandler calls the serializer and this transformation occurs.

This is perfectly fine, but handling form submissions when your client sends a request body containing properties with underscores can lead to boilerplate code. Since version 1.4 of FOSRestBundle, there is a convenient way to deal with these cases.

Showing an entity

Suppose you have the following entity:

<?php

namespace Acme\DemoBundle\Entity;

use Doctrine\ORM\Mapping AS ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="acme_demo_product")
 */
class Product
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string")
     */
    private $name;

    /**
     * @ORM\Column(type="decimal", precision=10, scale=2)
     */
    private $basePrice;

    // Getters and setters
}

Suppose we have the ProductController::getAction mapped to the /product/{id} route:

<?php

namespace Acme\DemoBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use FOS\RestBundle\Controller\Annotations as Rest;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class ProductController extends Controller
{
    /**
     * @Rest\View()
     */
    public function getAction($id)
    {
        $em = $this->getDoctrine()->getManager();
        $product = $em->getRepository('AcmeDemoBundle:Product')->find($id);

        if (null === $product) {
            throw new NotFoundHttpException();
        }

        return $product;
    }
}

When we send the following request:

$ curl -i -H "Accept: application/json" http://localhost/acme/web/product/1

We obtain a response with a 200 status code and the following body:

{
    "id": 1,
    "name": "Lorem Ipsum",
    "base_price": 995.95
}

As expected the basePrice property is transformed to base_price by the serializer.

Creating an entity

We want our API clients to be able to send requests with the same property keys than our application sends so the input and output data structures are the same. For example, we accept the following body for a json request:

{
    "name": "Sit amet",
    "base_price": 99.95
}

Supposing we have the following form type:

<?php

namespace Acme\DemoBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class ProductType extends AbstractType
{
    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name')
            ->add('basePrice')
        ;
    }

    /**
     * {@inheritdoc}
     */
    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'Acme\DemoBundle\Entity\Product',
            'csrf_protection' => false,
        ));
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'acme_demo_product';
    }
}

We implement the action to create a new product and map it to the /product route:

<?php

// The controller

/**
 * Creates a new product.
 *
 * @param Request $request
 *
 * @return Response|View
 */
public function newAction(Request $request)
{
    $product = new Product();
    $form = $this->get('form.factory')->createNamed('', new ProductType(), $product);
    $form->submit($request);

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

        $response = new Response();
        $response->setStatusCode(201);
        $response->headers->set(
            'Location',
            $this->generateUrl(
                'acme_demo_product_get',
                array('id' => $product->getId())
            )
        );

        return $response;
    }

    return View::create($form, 400);
}

When trying to create a new product:

$ curl -i -H "Accept: application/json" -H "Content-Type: application/json" -X POST
-d '{"name":"Sit amet","base_price":99.95}' http://localhost/acme/web/product

We get a response with a 400 status code and the following body:

{
    "code": 400,
    "message": "Validation Failed",
    "errors": {
        "errors": ["This form should not contain extra fields."],
        "children": {
            "name": [],
            "basePrice": []
        }
    }
}

The problem is that we are posting base_price instead of basePrice, so it is recognized as an extra form field. To fix this problem, there are various solutions:

Submit the form manually

<?php

$form->submit(array(
    'name' => $request->request->get('name'),
    'basePrice' => $request->request->get('base_price'),
));

It leads to a lot of boilerplate code specially when you have a lot of properties.

Modify the form

<?php

->add('base_price', null, array(
    'property_path' => 'basePrice'
))

The drawback is you will have to do that for each single camel case field in every single entity.

Use the array_normalizer config

fos_rest:
    body_listener:
        array_normalizer: fos_rest.normalizer.camel_keys

The camel_keys array normalizer will recursively transform all properties containing underscores to their camel case equivalent before the request is populated with the body data. In our example, the form submission works out of the box.

Conclusion

We presented how to use the array_normalizer config in order to reduce the boilerplate code to manage form submissions when the request body contains properties with underscores and the entities camel case fields.

Leave a Comment