From 75a46a5d8ba402bf2821e44e8d711442ecd2543d Mon Sep 17 00:00:00 2001 From: Christian Flothmann Date: Wed, 3 Jun 2015 09:22:08 +0200 Subject: [PATCH] SensioFrameworkExtraBundle 2.0.x compatibility --- .travis.yml | 2 + DependencyInjection/FOSRestExtension.php | 20 ++ Request/RequestBodyParamConverter20.php | 40 +++ .../config/request_body_param_converter.xml | 4 - .../RequestBodyParamConverterController.php | 49 +++ .../TestBundle/Resources/config/routing.yml | 4 + .../RequestBodyParamConverterTest.php | 31 ++ .../app/RequestBodyParamConverter/bundles.php | 8 + .../app/RequestBodyParamConverter/config.yml | 20 ++ .../RequestBodyParamConverter20Test.php | 312 ++++++++++++++++++ UPGRADING.md | 1 + 11 files changed, 487 insertions(+), 4 deletions(-) create mode 100644 Request/RequestBodyParamConverter20.php create mode 100644 Tests/Functional/Bundle/TestBundle/Controller/RequestBodyParamConverterController.php create mode 100644 Tests/Functional/RequestBodyParamConverterTest.php create mode 100644 Tests/Functional/app/RequestBodyParamConverter/bundles.php create mode 100644 Tests/Functional/app/RequestBodyParamConverter/config.yml create mode 100644 Tests/Request/RequestBodyParamConverter20Test.php diff --git a/.travis.yml b/.travis.yml index 8ffba6535..7aaa12642 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,6 +21,8 @@ matrix: env: deps="low" - php: 5.5 env: SYMFONY_VERSION='2.3.* symfony/expression-language:2.4.*' + - php: 5.5 + env: SYMFONY_VERSION='2.3.* sensio/framework-extra-bundle:2.*' - php: 5.5 env: SYMFONY_VERSION=2.4.* - php: 5.5 diff --git a/DependencyInjection/FOSRestExtension.php b/DependencyInjection/FOSRestExtension.php index e8c95302a..d9d004a93 100644 --- a/DependencyInjection/FOSRestExtension.php +++ b/DependencyInjection/FOSRestExtension.php @@ -202,6 +202,26 @@ private function loadBodyConverter(array $config, $validator, XmlFileLoader $loa { if (!empty($config['body_converter'])) { if (!empty($config['body_converter']['enabled'])) { + $parameter = new \ReflectionParameter( + array( + 'Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\ParamConverterInterface', + 'supports', + ), + 'configuration' + ); + + if ('Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter' === $parameter->getClass()->getName()) { + $container->setParameter( + 'fos_rest.converter.request_body.class', + 'FOS\RestBundle\Request\RequestBodyParamConverter' + ); + } else { + $container->setParameter( + 'fos_rest.converter.request_body.class', + 'FOS\RestBundle\Request\RequestBodyParamConverter20' + ); + } + $loader->load('request_body_param_converter.xml'); } diff --git a/Request/RequestBodyParamConverter20.php b/Request/RequestBodyParamConverter20.php new file mode 100644 index 000000000..abd7d9089 --- /dev/null +++ b/Request/RequestBodyParamConverter20.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\RestBundle\Request; + +use Symfony\Component\HttpFoundation\Request; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\ConfigurationInterface; + +/** + * This code is needed for SensioFrameworkExtraBundle 2.x compatibility + * https://github.com/FriendsOfSymfony/FOSRestBundle/issues/622 + * + * @author Tyler Stroud + */ +class RequestBodyParamConverter20 extends AbstractRequestBodyParamConverter +{ + /** + * {@inheritDoc} + */ + public function apply(Request $request, ConfigurationInterface $configuration) + { + return $this->execute($request, $configuration); + } + + /** + * {@inheritDoc} + */ + public function supports(ConfigurationInterface $configuration) + { + return null !== $configuration->getClass(); + } +} diff --git a/Resources/config/request_body_param_converter.xml b/Resources/config/request_body_param_converter.xml index 55036c645..896262d22 100644 --- a/Resources/config/request_body_param_converter.xml +++ b/Resources/config/request_body_param_converter.xml @@ -4,10 +4,6 @@ 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"> - - FOS\RestBundle\Request\RequestBodyParamConverter - - diff --git a/Tests/Functional/Bundle/TestBundle/Controller/RequestBodyParamConverterController.php b/Tests/Functional/Bundle/TestBundle/Controller/RequestBodyParamConverterController.php new file mode 100644 index 000000000..b97cf719d --- /dev/null +++ b/Tests/Functional/Bundle/TestBundle/Controller/RequestBodyParamConverterController.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\RestBundle\Tests\Functional\Bundle\TestBundle\Controller; + +use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Symfony\Component\HttpFoundation\Response; + +class RequestBodyParamConverterController extends Controller +{ + public function putPostAction(Post $post) + { + return new Response($post->getName()); + } +} + +class Post +{ + private $name; + private $body; + + public function getName() + { + return $this->name; + } + + public function setName($name) + { + $this->name = $name; + } + + public function getBody() + { + return $this->body; + } + + public function setBody($body) + { + $this->body = $body; + } +} diff --git a/Tests/Functional/Bundle/TestBundle/Resources/config/routing.yml b/Tests/Functional/Bundle/TestBundle/Resources/config/routing.yml index b7c96cb15..f96794db0 100644 --- a/Tests/Functional/Bundle/TestBundle/Resources/config/routing.yml +++ b/Tests/Functional/Bundle/TestBundle/Resources/config/routing.yml @@ -1,3 +1,7 @@ +request_body_param_converter: + path: /body-converter + defaults: { _controller: TestBundle:RequestBodyParamConverter:putPost } + test_serializer_error_exception: path: /serializer-error/exception.{_format} defaults: { _controller: TestBundle:SerializerError:exception } diff --git a/Tests/Functional/RequestBodyParamConverterTest.php b/Tests/Functional/RequestBodyParamConverterTest.php new file mode 100644 index 000000000..41134e84c --- /dev/null +++ b/Tests/Functional/RequestBodyParamConverterTest.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\RestBundle\Tests\Functional; + +class RequestBodyParamConverterTest extends WebTestCase +{ + public function testRequestBodyIsDeserialized() + { + $client = $this->createClient(array('test_case' => 'RequestBodyParamConverter')); + $client->request( + 'POST', + '/body-converter', + array(), + array(), + array('CONTENT_TYPE' => 'application/json'), + '{"name": "Post 1", "body": "This is a blog post"}' + ); + + $this->assertSame(200, $client->getResponse()->getStatusCode()); + $this->assertSame('Post 1', $client->getResponse()->getContent()); + } +} diff --git a/Tests/Functional/app/RequestBodyParamConverter/bundles.php b/Tests/Functional/app/RequestBodyParamConverter/bundles.php new file mode 100644 index 000000000..cad1cd761 --- /dev/null +++ b/Tests/Functional/app/RequestBodyParamConverter/bundles.php @@ -0,0 +1,8 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FOS\RestBundle\Tests\Request; + +use FOS\RestBundle\Request\RequestBodyParamConverter20; +use JMS\Serializer\Exception\RuntimeException; +use JMS\Serializer\Exception\UnsupportedFormatException; + +class RequestBodyParamConverter20Test extends AbstractRequestBodyParamConverterTest +{ + private $serializer; + private $converter; + + public function setUp() + { + // skip the test if the installed version of SensioFrameworkExtraBundle + // is not compatible with the RequestBodyParamConverter20 class + $parameter = new \ReflectionParameter( + array( + 'Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\ParamConverterInterface', + 'supports', + ), + 'configuration' + ); + if ('Sensio\Bundle\FrameworkExtraBundle\Configuration\ConfigurationInterface' !== $parameter->getClass()->getName()) { + $this->markTestSkipped('skipping RequestBodyParamConverter20Test due to an incompatible version of the SensioFrameworkExtraBundle'); + } + + $this->serializer = $this->getMock('JMS\Serializer\SerializerInterface'); + $this->converter = $this->getMock( + 'FOS\RestBundle\Request\RequestBodyParamConverter20', + array('getDeserializationContext'), + array($this->serializer) + ); + } + + public function testConstructThrowsExceptionIfValidatorIsSetAndValidationArgumentIsNull() + { + $this->setExpectedException('InvalidArgumentException'); + new RequestBodyParamConverter20( + $this->serializer, + null, + null, + $this->getMock('Symfony\Component\Validator\ValidatorInterface') + ); + } + + public function testSupports() + { + $config = $this->createConfiguration('FOS\RestBundle\Tests\Request\Post', 'post'); + $this->assertTrue($this->converter->supports($config)); + } + + public function testSupportsWithNoClass() + { + $this->assertFalse($this->converter->supports($this->createConfiguration(null, 'post'))); + } + + public function testApply() + { + $requestBody = '{"name": "Post 1", "body": "This is a blog post"}'; + $expectedPost = new Post('Post 1', 'This is a blog post'); + + $this->serializer->expects($this->once()) + ->method('deserialize') + ->with($requestBody, 'FOS\RestBundle\Tests\Request\Post', 'json') + ->will($this->returnValue($expectedPost)); + + $this->converter->expects($this->once()) + ->method('getDeserializationContext') + ->will($this->returnValue($this->createDeserializationContext())); + + $request = $this->createRequest('{"name": "Post 1", "body": "This is a blog post"}', 'application/json'); + + $config = $this->createConfiguration('FOS\RestBundle\Tests\Request\Post', 'post'); + $this->converter->apply($request, $config); + + $this->assertSame($expectedPost, $request->attributes->get('post')); + } + + public function testApplyWithUnsupportedContentType() + { + $this->serializer->expects($this->once()) + ->method('deserialize') + ->will($this->throwException(new UnsupportedFormatException('unsupported format'))); + + $this->converter->expects($this->once()) + ->method('getDeserializationContext') + ->will($this->returnValue($this->createDeserializationContext())); + + $request = $this->createRequest('', 'text/html'); + + $this->setExpectedException('Symfony\Component\HttpKernel\Exception\HttpException', 'unsupported format'); + + $config = $this->createConfiguration('FOS\RestBundle\Tests\Request\Post', 'post'); + $this->converter->apply($request, $config); + } + + public function testApplyWhenSerializerThrowsException() + { + $this->serializer->expects($this->once()) + ->method('deserialize') + ->will($this->throwException(new RuntimeException('serializer exception'))); + + $this->converter->expects($this->once()) + ->method('getDeserializationContext') + ->will($this->returnValue($this->createDeserializationContext())); + + $request = $this->createRequest(); + + $this->setExpectedException( + 'Symfony\Component\HttpKernel\Exception\HttpException', + 'serializer exception' + ); + + $config = $this->createConfiguration('FOS\RestBundle\Tests\Request\Post', 'post'); + $this->converter->apply($request, $config); + } + + public function testApplyWithSerializerContextOptionsForJMSSerializer() + { + $requestBody = '{"name": "Post 1", "body": "This is a blog post"}'; + $options = array( + 'deserializationContext' => array( + 'groups' => array('group1'), + 'version' => '1.0', + ), + ); + + $context = $this->createDeserializationContext( + $options['deserializationContext']['groups'], + $options['deserializationContext']['version'] + ); + + $this->converter->expects($this->once()) + ->method('getDeserializationContext') + ->will($this->returnValue($context)); + + $this->serializer->expects($this->once()) + ->method('deserialize') + ->with($requestBody, 'FOS\RestBundle\Tests\Request\Post', 'json', $context); + + $request = $this->createRequest($requestBody, 'application/json'); + $config = $this->createConfiguration('FOS\RestBundle\Tests\Request\Post', 'post', $options); + + $this->converter->apply($request, $config); + } + + public function testApplyWithDefaultSerializerContextExclusionPolicy() + { + $this->converter = $this->getMock( + 'FOS\RestBundle\Request\RequestBodyParamConverter20', + array('getDeserializationContext'), + array($this->serializer, array('group1'), '1.0') + ); + + $context = $this->createDeserializationContext(array('group1'), '1.0'); + $request = $this->createRequest('', 'application/json'); + $config = $this->createConfiguration('FOS\RestBundle\Tests\Request\Post', 'post'); + + $this->converter->expects($this->once()) + ->method('getDeserializationContext') + ->will($this->returnValue($context)); + + $this->serializer->expects($this->once()) + ->method('deserialize') + ->with('', 'FOS\RestBundle\Tests\Request\Post', 'json', $context); + + $this->converter->apply($request, $config); + } + + public function testApplyWithSerializerContextOptionsForSymfonySerializer() + { + $this->serializer = $this->getMock('Symfony\Component\Serializer\SerializerInterface', array('serialize', 'deserialize')); + $this->converter = new RequestBodyParamConverter20($this->serializer); + $requestBody = '{"name": "Post 1", "body": "This is a blog post"}'; + + $options = array( + 'deserializationContext' => array( + 'json_decode_options' => 2, // JSON_BIGINT_AS_STRING + ), + ); + + $this->serializer->expects($this->once()) + ->method('deserialize') + ->with($requestBody, 'FOS\RestBundle\Tests\Request\Post', 'json', $options['deserializationContext']); + + $request = $this->createRequest($requestBody, 'application/json'); + $config = $this->createConfiguration('FOS\RestBundle\Tests\Request\Post', 'post', $options); + + $this->converter->apply($request, $config); + } + + public function testApplyWithValidationErrors() + { + $validator = $this->getMockBuilder('Symfony\Component\Validator\Validator') + ->disableOriginalConstructor() + ->getMock(); + $validationErrors = $this->getMock('Symfony\Component\Validator\ConstraintViolationList'); + + $this->converter = new RequestBodyParamConverter20($this->serializer, null, null, $validator, 'validationErrors'); + + $expectedPost = new Post('Post 1', 'This is a blog post'); + $this->serializer->expects($this->once()) + ->method('deserialize') + ->with('', 'FOS\RestBundle\Tests\Request\Post', 'json') + ->will($this->returnValue($expectedPost)); + + $request = $this->createRequest('', 'application/json'); + $options = array( + 'validator' => array( + 'groups' => array('group1'), + 'traverse' => true, + 'deep' => true, + ), + ); + + $validator->expects($this->once()) + ->method('validate') + ->with($expectedPost, array('group1'), true, true) + ->will($this->returnValue($validationErrors)); + + $config = $this->createConfiguration('FOS\RestBundle\Tests\Request\Post', 'post', $options); + $this->converter->apply($request, $config); + + $this->assertSame($expectedPost, $request->attributes->get('post')); + $this->assertSame($validationErrors, $request->attributes->get('validationErrors')); + } + + public function testDefaultValidatorOptions() + { + $this->converter = new RequestBodyParamConverter20($this->serializer); + $reflClass = new \ReflectionClass($this->converter); + $method = $reflClass->getMethod('getValidatorOptions'); + $method->setAccessible(true); + $options = $method->invoke($this->converter, array()); + + $expected = array( + 'groups' => null, + 'traverse' => false, + 'deep' => false, + ); + + $this->assertEquals($expected, $options); + } + + public function testDefaultValidatorOptionsMergedWithUserOptions() + { + // Annotation example + // @ParamConverter( + // post, + // class="AcmeBlogBundle:Post", + // options={"validator"={"groups"={"Posting"}} + // ) + $userOptions = array( + 'validator' => array( + 'groups' => array('Posting'), + ), + ); + + $expectedOptions = array( + 'groups' => array('Posting'), + 'traverse' => false, + 'deep' => false, + ); + + $converterMock = $this->getMockBuilder('FOS\RestBundle\Request\RequestBodyParamConverter20') + ->disableOriginalConstructor() + ->getMock() + ; + + $reflClass = new \ReflectionClass($converterMock); + $method = $reflClass->getMethod('getValidatorOptions'); + $method->setAccessible(true); + $mergedOptions = $method->invoke($converterMock, $userOptions); + + $this->assertEquals($expectedOptions, $mergedOptions); + } + + public function testValidatorOptionsStructureAfterMergeWithUserOptions() + { + // Annotation example + // @ParamConverter( + // post, + // class="AcmeBlogBundle:Post", + // options={"validator"={"groups"={"Posting"}} + // ) + $userOptions = array( + 'validator' => array( + 'groups' => array('Posting'), + ), + ); + $config = $this->createConfiguration(null, null, $userOptions); + + $validator = $this->getMockBuilder('Symfony\Component\Validator\Validator') + ->disableOriginalConstructor() + ->getMock(); + $this->converter = new RequestBodyParamConverter20($this->serializer, null, null, $validator, 'validationErrors'); + $request = $this->createRequest(); + + $this->converter->apply($request, $config); + } +} diff --git a/UPGRADING.md b/UPGRADING.md index 5d58e593a..83af3729a 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -13,6 +13,7 @@ This document will be updated to list important BC breaks and behavioral changes * Dropped support for Symfony 2.2 (which includes dropping support for "pattern" in favor of only supporting "path" in routes), see https://github.com/FriendsOfSymfony/FOSRestBundle/pull/952 * Dropped support for SensioFrameworkExtraBundle 2.x, see https://github.com/FriendsOfSymfony/FOSRestBundle/pull/952 + (support for SensioFrameworkExtraBundle was added back in version 1.6.1 of the FOSRestBundle) ### upgrading from 1.4.*