Handling CamelCase with FOSRestBundle
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 into 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
{
/**
*
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('basePrice')
;
}
/**
*
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\DemoBundle\Entity\Product',
'csrf_protection' => false,
));
}
/**
*
*/
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 especially 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.