Disponible pour vos projets! Contactez-moi

Gérer le CamelCase avec FOSRestBundle

Gérer le CamelCase avec FOSRestBundle Image

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.