See also the official documentation on this subject and a post by Benjamin Eberlei.

In my previous post I wrote about a listener that deserializes the request content and replaces controller arguments with the result. Johannes Schmitt made the suggestion to use a ParamConverter for this. This of course makes sense: currently there is only a ParamConverter for converting some "id" argument into an entity with the same id (see the documentation for @ParamConverter.

In this post I will show you how to create your own ParamConverter and how we can specialize it in deserializing request content.

So, we still have the createCommentAction in the DemoController from the previous post:

class DemoController
{
    public function createCommentAction(Comment $comment)
    {
        // ...
    }
}

And also the very simple Comment class:

<?php

namespace Matthias\RestBundle\DataTransferObject;

use JMS\SerializerBundle\Annotation\XmlRoot;
use JMS\SerializerBundle\Annotation\Type;

/**
 * @XmlRoot("comment")
 */
class Comment
{
    /**
     * @Type("string")
     */
    private $from;

    /**
     * @Type("DateTime")
     */
    private $createdAt;

    public function getFrom()
    {
        return $this->from;
    }

    /**
     * @return \DateTime
     */
    public function getCreatedAt()
    {
        return $this->createdAt;
    }

    public function __construct()
    {
        //$this->createdAt = new \DateTime;
    }
}

The ParamConverter itself

First, let's create the ParamConverter itself. It should implement the ParamConverterInterface, which means it should have a supports() method. This method receives a configuration object which it can use to determine if the ParamConverter can provide the right value for a certain argument. The ParamConverter should also implement apply(), which receives the current Request object and again the configuration object. Using these two (and probably some service you injected using constructor or setter arguments) the apply() method should try to supply the action with the right argument.

In our case the ParamConverter should convert the request content (which is XML) to a full Comment object.

namespace Matthias\RestBundle\Request\ParamConverter;

use Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\ParamConverterInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ConfigurationInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use JMS\SerializerBundle\Serializer\SerializerInterface;
use JMS\SerializerBundle\Exception\XmlErrorException;

class SerializedParamConverter implements ParamConverterInterface
{
    private $serializer;

    public function __construct(SerializerInterface $serializer)
    {
        $this->serializer = $serializer;
    }

    public function supports(ConfigurationInterface $configuration)
    {
        if (!$configuration->getClass()) {
            return false;
        }

        // for simplicity, everything that has a "class" type hint is supported

        return true;
    }

    public function apply(Request $request, ConfigurationInterface $configuration)
    {
        $class = $configuration->getClass();

        try {
            $object = $this->serializer->deserialize(
                $request->getContent(),
                $class,
                'xml'
            );
        }
        catch (XmlErrorException $e) {
            throw new NotFoundHttpException(sprintf('Could not deserialize request content to object of type "%s"',
                $class));
        }

        // set the object as the request attribute with the given name
        // (this will later be an argument for the action)
        $request->attributes->set($configuration->getName(), $object);
    }
}

Now we should register the SerializedParamConverter as a service:

<?xml version="1.0" ?>
<container xmlns="https://symfony.com/schema/dic/services"
           xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="https://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd">
    <services>
        <service id="matthias.serialized_param_converter"
                 class="Matthias\RestBundle\Request\ParamConverter\SerializedParamConverter">
            <argument type="service" id="serializer" />
            <tag name="request.param_converter" priority="-100" />
        </service>
    </services>
</container>

Note that we need to give some low priority to our converter, since it supports any argument with a "class" type hint. If we don't provide the priority argument, the DoctrineParamConverter may never be called for arguments that should in fact be converted to an entity.

As it says in the documentation for the @ParamConverter annotation, if you use type hinting, you can omit the special annotation. The SerializedParamConverter will be called automatically for any unresolved action argument.

You can test all this by the same method as described in my previous post, but for completeness, my not very elegant yet pragmatic test looks like this:

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

use Matthias\RestBundle\DataTransferObject\Comment;

class DemoControllerTest extends WebTestCase
{
    public function testCreateComment()
    {
        $content = <<<EOF
<?xml version="1.0" encoding="UTF-8"?>
<comment>
    <from><![CDATA[Matthias Noback]]></from>
    <created_at><![CDATA[%now%]]></created_at>
</comment>
EOF;
        $server = array(
            'HTTP_ACCEPT' => 'text/xml',
            'HTTP_CONTENT_TYPE' => 'text/xml; charset=UTF-8',
        );

        $content = str_replace('%now%', date('c'), $content);

        $client = $this->createClient();
        $client->request('POST', '/demo/create-comment', array(), array(), $server, $content);
        var_dump($client->getResponse()->getContent());
        exit;
    }
}

And the controller itself looks like

namespace Matthias\RestBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\HttpFoundation\Response;

use Matthias\RestBundle\DataTransferObject\Comment;

class DemoController extends Controller
{
    /**
     * @param Matthias\RestBundle\DataTransferObject\Comment $comment
     * @Route("/demo/create-comment", name="demo_create_comment")
     * @Method("POST")
     */
    public function createCommentAction(Comment $comment)
    {
        return new Response(print_r($comment, true));
    }
}
PHP Symfony2 annotations serializer
Comments
This website uses MailComments: you can send your comments to this post by email. Read more about MailComments, including suggestions for writing your comments (in HTML or Markdown).
Melvin Iwuajoku

Hi, can this be used with silex? Please, as I am having problems converting request objects to entity. Thanks

Pablo Albizu

Hi Matthias!!

Thanks a lot for your post! Can I creating a ParamConverter for deserializing request when the content is an array json? In your case, imagine that you have an array json of Comments.

Once again, thank you very much.

(Excuse me, for my english)

Dmitry Krasun

And what to do if Doctrine throws exception cause can't deseriralize JSON string?

Matthias Noback

Hi Dmitry, it would indeed be a good idea to modify the exception handling part. In the first place, you should add multiple catches, one for XML errors, but one for additional serialization errors. Also, it is not such a good idea to return 404 Not found, when actually it should be 400 Bad request.

Dmitry Krasun

You forgot to add "return true;" statement to SerializedParamConverter::supports method.

Matthias Noback

It is there, right?

Dmitry Krasun

Yes, I think so. Because here https://github.com/sensio/S... we see how it works.

If it won't return true, for example, DoctrineParamConverter will be applied and exception will be thrown because DoctrineParam... can't deserialize XML or JSON string.

Excuse me, for my english

Antoni

Hi Matthias!

Firstly we have to say - you are doing very good job over here :-)

We're making Restful API on Symfony2 with FOSRestBundle and we also thought about specialized ParamConverter for converting request content body to Objects.

We wanted to contribute in FosRestBundle with such ParamConverter + needed other changes in Bundle structure but you were first with 'the code'.

Are you planning to push it to FOSRestBundle or can you allow us to use your ParamConverter code (with some changes which includes automatic content-type recognition) with added comment like 'Based on Matthias Noback's Param Converter from http://...'?

Greetings from Poland! :D

Matthias Noback

Hi Antoni,
Thanks, really happy to hear about that. I've had this on my "to do" list for quite some time, but since you are ready to contribute right now, go ahead! It would be nice if you would add a small comment and a link to my blog.
Good luck!

Antoni

Done!
https://github.com/FriendsO...

Argh that first contribution wasn't easy :D (Just take a look at history of my account https://github.com/orfin - 6x attempts to make 'good commits')

It looks like on Windows you have to config git that way:


$ git config --global core.autocrlf true

instead of "input" recommended by Symfony2 devs [http://symfony.com/doc/curr...] :/

beberlei

i would be very happy if this could be either part of framework extra bundle or jms serializer bundle, only activated when the other bundle is also active. I need that for so many projects :-)

Matthias Noback

Hi, I'm happy to hear that. I would certainly encourage making this a part of some other bundle (probably FOSRestBundle, since it already uses JMSSerializerBundle and is able to differentiate between different request content types, like XML and JSON), and am also willing to contribute. It is a very elegant concept, not so many lines of code, and it makes life a lot easier :)