Gérer le CamelCase avec FOSRestBundle
La stratégie de nommage par défaut de JMSSerializerBundle
est camel_case
, ce qui signifie que vos champs d'entité en CamelCase sont normalisés en leur équivalent Underscore lorsqu'ils sont sérialisés. Lors du rendu d'entités, le ViewHandler
appelle le sérialiseur et cette transformation se produit.
Cela convient parfaitement, mais la gestion des formulaires lorsque votre client envoie un corps de requête contenant des propriétés Underscore peut conduire à l'écriture de beaucoup de code. Depuis la version 1.4 de FOSRestBundle
, il existe un moyen pratique de traiter ces cas.
Montrer une entité
Supposons que vous ayez l'entité suivante:
<?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 et setters
}
Supposons que nous ayons la fonction ProductController::getAction
mappée sur la route /product/{id}
:
<?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;
}
}
Lorsque nous envoyons la requête suivante:
$ curl -i -H "Accept: application/json" http://localhost/acme/web/product/1
Nous obtenons une réponse avec un code de statut 200
et le corps suivant:
{
"id": 1,
"name": "Lorem Ipsum",
"base_price": 995.95
}
Comme prévu, la propriété basePrice
est transformée en base_price
par le sérialiseur.
Créer une entité
Nous voulons que nos clients API puissent envoyer des requêtes avec les mêmes clés que celles envoyées par notre application, de sorte que les structures de données d'entrée et de sortie soient identiques.
Par exemple, nous acceptons le corps suivant pour une requête json:
{
"name": "Sit amet",
"base_price": 99.95
}
En supposant que nous ayons le type de formulaire suivant:
<?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';
}
}
Nous mettons en œuvre l’action pour créer un nouveau produit et la mappons à la route /product
:
<?php
// Le controller
/**
* Crée un nouveau produit.
*
* @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);
}
Lorsque vous essayez de créer un nouveau produit:
$ 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
Nous obtenons une réponse avec un code de statut 400 et le corps suivant:
{
"code": 400,
"message": "Validation Failed",
"errors": {
"errors": ["This form should not contain extra fields."],
"children": {
"name": [],
"basePrice": []
}
}
}
Le problème est que nous envoyons base_price
au lieu debasePrice
, il est donc reconnu comme un champ de formulaire supplémentaire.
Pour résoudre ce problème, il existe différentes solutions:
Soumettre le formulaire manuellement
<?php
$form->submit(array(
'name' => $request->request->get('name'),
'basePrice' => $request->request->get('base_price'),
));
Cela conduit à beaucoup de code spécialement quand vous avez beaucoup de propriétés.
Modifier le formulaire
<?php
->add('base_price', null, array(
'property_path' => 'basePrice'
))
L'inconvénient est que vous devrez le faire pour chaque CamelCase sur chaque entité.
Utiliser la configuration array_normalizer
fos_rest:
body_listener:
array_normalizer: fos_rest.normalizer.camel_keys
Le normalisateur de tableau camel_keys
transformera de manière récursive toutes les propriétés contenant des Underscore en CamelCase avant que la requête ne soit traitée. Dans notre exemple du dessus, la soumission du formulaire marche directement.
Conclusion
Nous avons présenté comment utiliser la configuration array_normalizer
afin de réduire la quantité de code pour gérer les soumissions de formulaires lorsque le corps de la requête contient des propriétés en Underscore et les entités des propriétés en CamelCase.