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
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).
pay for essay

This kind of technique becomes really popular before but for sure there might be some other people who already made some improvements that can help them in attaining a better output in terms of deserializing request content right into controller arguments.

Alex

You casually mentioned" Data Transfer Object", indicating that this is know territory for you.

The reason I found your post is that I was looking for de-serializing / persisting nested objects. for instance company has address. On the client this would be flattened.
Some kind of logic in between the domain model and the channel to the client would translate between the domain model and the client viewmodel. But the DTO docs state that the DTO itself cannot have any business logic.

In this post I found another example which does some mapping. Is that the way to go then?

Matthias Noback

Thanks for your comment. This really is an area of great interest to me. In my opinion, the way to go would be to use mappers to define the rules for creating a DTO based on a domain object. This object can be serialized using for example the serializer from the jmsserializerbundle, which supports recursive serializing of objects to XML or JSON. Depending on the kind of API you develop, you may also define another mapper for the inverse transformation: from request content to DTO. I would be happy to exchange ideas about this subject, and answer any question you have. I have the feeling there is not too many experience in this area in the php world. A very good resource is the book Service design patterns and the corresponding website. Good luck.

Cory

Hi Matthias,

I am trying to set this up, but I keep getting an error saying: Fatal error: Class 'BIM\BimfsBundle\EventListener\ControllerArgumentsListener' not found in /Library/WebServer/Documents/BIMfs-v2/app/cache/test/appTestDebugProjectContainer.php on line 403

Is there some step I am missing? I have tried warming up the cache but it seems it is not be registering my namespace.

Thanks,
Cory

Matthias Noback

Hi Cory, this looks like a problem with autoloading the file: have you registered the namespace "BIM" in app/autoload.php? And do the namespaces and the path match exactly?
BIM\BimfsBundle\EventListener\ControllerArgumentsListener should be in /src/BIM/BimfsBundle/EventListener/ControllerArgumentsListener.php
Good luck!

Cory

Hi Matthias,

I did not have the bundle in the auto loader, however adding it does not resolve the error. I wish there was a easy way to see what bundles where loaded by symfony2 similiar to how I can check routes via the console.
Here is what I added, but the issue remains
$loader->registerNamespaces(array(
'BIM' => __DIR__.'/../src',
));

-Cory

Cory

After much pain and anguish, I discovered I had a typo in a folder name.

Thanks,
Cory

Jonathan

Do you think that this is something that could be moved into FOS bundle in order that it can handle this case?

Matthias Noback

Hi Jonathan,
I think it could be added to the bundle, though I am not sure what should be the exact way. In the meantime I have made this all a lot more general, using annotation. Johannes Schmitt suggested to use a ParamConverter (from the SensioFrameworkExtraBundle). The concept certainly has great potential, especially since you can also add validation to the objects that were deserialized from the request content - something I was looking for since the body listener from the bundle returns simple arrays.