Based on the list of most popular search results leading to my blog, using Symfony2 for building some kind of webservice seems to be quite "hot". It also seems many people are struggling with the question how they should serialize their content to and from XML and/or JSON. Two beautiful bundles are already available for this, FOSRestBundle and JMSSerializerBundle. Both will help you a lot with this issue, by providing transparent conversion between formats for both the request content and the response body. Nevertheless, a few things are missing. For example, I want to really serialize and deserialize objects, for example, when I send a request with a comment (or part of it) like this:

<?xml version="1.0" encoding="UTF-8"?>
<comment>
    <from><![CDATA[Matthias Noback]]></from>
    <created_at><![CDATA[2012-02-28T22:10:52+01:00]]></created_at>
</comment>"

I want this to become immediately available in my controller as a Comment object (be it an entity, or more safe, some kind of a Data Transfer Object):

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

Now I can do whatever I want with the Comment object, without any inline deserialization of the request content.

Modify controller arguments

What we need is a way to modify the controller arguments. This may be done by setting attributes on the Request object. In fact, this is how the framework itself does it: if your controller has an argument which is type-hinted as a Request it will magically have as it's value the current Request object.

Now in the case of a comment sent as XML, we want the request content to be serialized into a Comment Data Transfer Object, that should be available as the value of a controller argument. We can accomplish this by creating an event listener, which hooks into the "kernel.controller" event. This event is fired before the controller is called, and every listener receives a FilterControllerEvent object. Inside the listener, we can set an extra attribute on the current Request object, which will later replace the argument of the controller with the name of the attribute (the framework will take care of this in it's ControllerResolver).

Our event listener, which tries to transform XML to Comment objects, needs the Serializer from the JMSSerializerBundle (please install it first, see the documentation of this bundle for instructions). We will pass the Serializer as a constructor argument.

Using the Serializer, the listener will try to convert XML data to a Comment object, and then use it to populate the Request attribute called "comment".

namespace Matthias\RestBundle\EventListener;

use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use JMS\SerializerBundle\Serializer\Serializer;
use JMS\SerializerBundle\Exception\XmlErrorException;
use Matthias\RestBundle\DataTransferObject\Comment;

class ControllerArgumentsListener
{
    private $serializer;

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

    public function onKernelController(FilterControllerEvent $event)
    {
        $request = $event->getRequest();

        // the controller is an attribute of the request
        $controller = $request->attributes->get('_controller');

        // controller looks like class::method, we need array(class, method)
        $controller = explode('::', $controller);

        try {
            new \ReflectionParameter($controller, 'comment');
        }
        catch (\ReflectionException $e) {
            // no controller argument named "comment" was found
            return;
        }

        if ($request->attributes->has('comment')) {
            // comment attribute already exists, do not overwrite it
            return;
        }

        try
        {
            // deserialize the request content into a Comment object
            $comment = $this->serializer->deserialize(
                 $request->getContent(),
                'Matthias\\RestBundle\\DataTransferObject\\Comment',
                'xml'
            );
        }
        catch (XmlErrorException $e)
        {
            // we've tried, but it didn't work out
            return;
        }

        // set the comment attribute on the request,
        // this will be used as the $comment argument of the controller
        $request->attributes->set('comment', $comment);
    }
}

As you can see, this implementation is quite specific, namely for transforming XML request content to a Comment object, which by the way looks like this:

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;
    }
}

The class definition contains the necessary directions for the serializer for serializing to XML and deserializing to a Comment object. Of course this may freely be extended.

To make all this work, we only need to add the event listener to our list of service definitions, and tag the service as a "kernel.event_listener", listening to the "kernel.controller" event:

<?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.controller_listener"
                 class="Matthias\RestBundle\EventListener\ControllerArgumentsListener">
            <tag name="kernel.event_listener" event="kernel.controller" method="onKernelController" />
            <argument type="service" id="serializer" />
        </service>
    </services>
</container>

Everything should work now! In case you wish to test the conversion, you may try some unctional testing using PHPUnit:

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

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;

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

        $server = array(
            'HTTP_ACCEPT' => 'text/xml',
            'HTTP_CONTENT_TYPE' => 'text/xml; charset=UTF-8',
        );

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

Closing remarks

It is not a very difficult task to refactor the event listener and make it more general. Also, it should check if the client accepts XML, by reading and parsing the Accept and Content-Type header of the request (FOSRestBundle provides excellent ways to accomplish this).

PHP Symfony2 annotations events reflection serializer