Skip to content

Commit

Permalink
Added an option to normalize between underscore and camel case in the…
Browse files Browse the repository at this point in the history
… body listener
  • Loading branch information
florianv committed Apr 20, 2014
1 parent 1ee956f commit bdbc621
Show file tree
Hide file tree
Showing 11 changed files with 385 additions and 1 deletion.
1 change: 1 addition & 0 deletions DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ private function addBodyListenerSection(ArrayNodeDefinition $rootNode)
->defaultValue(array('json' => 'fos_rest.decoder.json', 'xml' => 'fos_rest.decoder.xml'))
->prototype('scalar')->end()
->end()
->scalarNode('array_normalizer')->defaultNull()->end()
->end()
->end()
->end();
Expand Down
6 changes: 6 additions & 0 deletions DependencyInjection/FOSRestExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ public function load(array $configs, ContainerBuilder $container)

$container->setParameter($this->getAlias().'.throw_exception_on_unsupported_content_type', $config['body_listener']['throw_exception_on_unsupported_content_type']);
$container->setParameter($this->getAlias().'.decoders', $config['body_listener']['decoders']);

$arrayNormalizer = $config['body_listener']['array_normalizer'];
if (null !== $arrayNormalizer) {
$container->getDefinition($this->getAlias().'.body_listener')
->addMethodCall('setArrayNormalizer', array(new Reference($arrayNormalizer)));
}
}

if (!empty($config['format_listener']['rules'])) {
Expand Down
24 changes: 24 additions & 0 deletions EventListener/BodyListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
namespace FOS\RestBundle\EventListener;

use FOS\RestBundle\Decoder\DecoderProviderInterface;
use FOS\RestBundle\Normalizer\ArrayNormalizerInterface;
use FOS\RestBundle\Normalizer\Exception\NormalizationException;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
Expand All @@ -34,6 +36,11 @@ class BodyListener
*/
private $throwExceptionOnUnsupportedContentType;

/**
* @var ArrayNormalizerInterface
*/
private $arrayNormalizer;

/**
* Constructor.
*
Expand All @@ -46,6 +53,16 @@ public function __construct(DecoderProviderInterface $decoderProvider, $throwExc
$this->throwExceptionOnUnsupportedContentType = $throwExceptionOnUnsupportedContentType;
}

/**
* Sets the array normalizer.
*
* @param ArrayNormalizerInterface $arrayNormalizer
*/
public function setArrayNormalizer(ArrayNormalizerInterface $arrayNormalizer)
{
$this->arrayNormalizer = $arrayNormalizer;
}

/**
* Core request handler
*
Expand Down Expand Up @@ -80,6 +97,13 @@ public function onKernelRequest(GetResponseEvent $event)
if (!empty($content)) {
$data = $decoder->decode($content, $format);
if (is_array($data)) {
if (null !== $this->arrayNormalizer) {
try {
$data = $this->arrayNormalizer->normalize($data);
} catch (NormalizationException $e) {
throw new BadRequestHttpException($e->getMessage());
}
}
$request->request = new ParameterBag($data);

// Reset the method in the current request to support method-overriding
Expand Down
31 changes: 31 additions & 0 deletions Normalizer/ArrayNormalizerInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

/*
* This file is part of the FOSRest package.
*
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FOS\RestBundle\Normalizer;

/**
* Normalizes arrays.
*
* @author Florian Voutzinos <florian@voutzinos.com>
*/
interface ArrayNormalizerInterface
{
/**
* Normalizes the array.
*
* @param array $data The array to normalize
*
* @return array The normalized array
*
* @throws Exception\NormalizationException
*/
public function normalize(array $data);
}
82 changes: 82 additions & 0 deletions Normalizer/CamelKeysNormalizer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

/*
* This file is part of the FOSRest package.
*
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FOS\RestBundle\Normalizer;

use FOS\RestBundle\Normalizer\Exception\NormalizationException;

/**
* Normalizes the array by changing its keys from underscore to camel case.
*
* @author Florian Voutzinos <florian@voutzinos.com>
*/
class CamelKeysNormalizer implements ArrayNormalizerInterface
{
/**
* {@inheritdoc}
*/
public function normalize(array $data)
{
$this->normalizeArray($data);

return $data;
}

/**
* Normalizes an array.
*
* @param array &$data
*
* @throws Exception\NormalizationException
*/
private function normalizeArray(array &$data)
{
foreach ($data as $key => $val) {
$normalizedKey = $this->normalizeString($key);

if ($normalizedKey !== $key) {
if (array_key_exists($normalizedKey, $data)) {
throw new NormalizationException(sprintf(
'The key "%s" is invalid as it will override the existing key "%s"',
$key,
$normalizedKey
));
}

unset($data[$key]);
$data[$normalizedKey] = $val;
$key = $normalizedKey;
}

if (is_array($val)) {
$this->normalizeArray($data[$key]);
}
}
}

/**
* Normalizes a string.
*
* @param string $string
*
* @return string
*/
private function normalizeString($string)
{
if (false === strpos($string, '_')) {
return $string;
}

return preg_replace_callback('/_([a-zA-Z0-9])/', function ($matches) {
return strtoupper($matches[1]);
}, $string);
}
}
21 changes: 21 additions & 0 deletions Normalizer/Exception/NormalizationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

/*
* This file is part of the FOSRest package.
*
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FOS\RestBundle\Normalizer\Exception;

/**
* Exception thrown when the normalization failed.
*
* @author Florian Voutzinos <florian@voutzinos.com>
*/
class NormalizationException extends \RuntimeException
{
}
3 changes: 3 additions & 0 deletions Resources/config/body_listener.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

<parameters>

<parameter key="fos_rest.normalizer.camel_keys.class">FOS\RestBundle\Normalizer\CamelKeysNormalizer</parameter>
<parameter key="fos_rest.decoder.json.class">FOS\RestBundle\Decoder\JsonDecoder</parameter>
<parameter key="fos_rest.decoder.jsontoform.class">FOS\RestBundle\Decoder\JsonToFormDecoder</parameter>
<parameter key="fos_rest.decoder.xml.class">FOS\RestBundle\Decoder\XmlDecoder</parameter>
Expand All @@ -16,6 +17,8 @@

<services>

<service id="fos_rest.normalizer.camel_keys" class="%fos_rest.normalizer.camel_keys.class%" />

<service id="fos_rest.decoder.json" class="%fos_rest.decoder.json.class%" />

<service id="fos_rest.decoder.jsontoform" class="%fos_rest.decoder.jsontoform.class%" />
Expand Down
34 changes: 33 additions & 1 deletion Resources/doc/3-listener-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,12 +175,14 @@ public function getUserDetails(User $user)

### Body listener

The Request body decoding listener makes it possible to decode the contents of
The Request body listener makes it possible to decode the contents of
a request in order to populate the "request" parameter bag of the Request. This
for example allows to receive data that normally would be sent via POST as
``application/x-www-form-urlencode`` in a different format (for example
application/json) in a PUT.

#### Decoders

You can add a decoder for a custom format. You can also replace the default
decoder services provided by the bundle for the ``json`` and ``xml`` formats.
Below you can see how to override the decoder for the json format (the xml
Expand All @@ -203,6 +205,36 @@ If you want to be able to use form with checkbox and have true and false value (
If the listener receives content that it tries to decode but the decode fails then a BadRequestHttpException will be thrown with the message:
``'Invalid ' . $format . ' message received'``. When combined with the [exception controller support](4-exception-controller-support.md) this means your API will provide useful error messages to your API users if they are making invalid requests.
#### Array Normalizer
Array Normalizers allow to transform the data after it has been decoded in order to facilitate its processing.
For example, you may want your API's clients to be able to send requests with underscored keys but if you use a decoder
without a normalizer, you will receive the data as it is and it can lead to incorrect mapping if you submit the request directly to a Form.
If you wish the body listener to transform underscored keys to camel cased ones, you can use the ``camel_keys`` array normalizer:
```yaml
# app/config/config.yml
fos_rest:
body_listener:
array_normalizer: fos_rest.normalizer.camel_keys
```
Sometimes an array contains a key, which once normalized, will override an existing array key. For example ``foo_bar`` and ``foo_Bar`` will both lead to ``fooBar``.
If the normalizer receives this data, the listener will throw a BadRequestHttpException with the message
``The key "foo_Bar" is invalid as it will override the existing key "fooBar"``.
NB: If you use the ``camel_keys`` normalizer, you must be careful when choosing your Form name.
You can also create your own array normalizer by implementing the ``FOS\RestBundle\Normalizer\ArrayNormalizerInterface``.
```yaml
# app/config/config.yml
fos_rest:
body_listener:
array_normalizer: acme.normalizer.custom
```
### Request Body Converter Listener
[Converters](http://symfony.com/doc/master/bundles/SensioFrameworkExtraBundle/annotations/converters.html)
Expand Down
37 changes: 37 additions & 0 deletions Tests/DependencyInjection/FOSRestExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,23 @@ public function testLoadBodyListenerWithDefaults()
$this->assertTrue($this->container->hasDefinition('fos_rest.body_listener'));
$this->assertParameter($decoders, 'fos_rest.decoders');
$this->assertParameter(false, 'fos_rest.throw_exception_on_unsupported_content_type');
$this->assertFalse($this->container->getDefinition('fos_rest.body_listener')->hasMethodCall('setArrayNormalizer'));
}

public function testLoadBodyListenerWithNormalizer()
{
$config = array(
'fos_rest' => array('body_listener' => array(
'array_normalizer' => 'fos_rest.normalizer.camel_keys'
))
);
$this->extension->load($config, $this->container);

$this->assertServiceHasMethodCall(
'fos_rest.body_listener',
'setArrayNormalizer',
array(new Reference('fos_rest.normalizer.camel_keys'))
);
}

public function testDisableFormatListener()
Expand Down Expand Up @@ -501,4 +518,24 @@ public function testSerializerRequiredWhenJMSIsNotAvailable()

$this->extension->load(array(), $this->container);
}

/**
* Asserts the service definition has the method call with the specified arguments.
*
* @param string $serviceId The service id
* @param string $methodName The name of the method
* @param array $arguments The arguments of the method
*/
private function assertServiceHasMethodCall($serviceId, $methodName, array $arguments = array())
{
$message = sprintf('The service "%s" has the method call "%s"', $serviceId, $methodName);
foreach ($this->container->getDefinition($serviceId)->getMethodCalls() as $methodCall) {
if ($methodCall[0] === $methodName) {
$this->assertEquals($arguments, $methodCall[1], $message);
return;
}
}

$this->assertTrue(false, $message);
}
}
Loading

0 comments on commit bdbc621

Please sign in to comment.