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="http://symfony.com/schema/dic/services"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://symfony.com/schema/dic/services http://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