Symfony2: creating a ParamConverter for deserializing request content
Matthias Noback
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));
}
}