This repository has been archived by the owner on Feb 24, 2023. It is now read-only.
[ParamConverters] Object ParamConverter #137
Closed
Closed
Changes from 6 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
84006aa
ObjectParamConverter
beberlei 5ba72e0
For consistency and security reasons, only allow this for request que…
beberlei 09d322f
Fix apply and CS
beberlei 0f46e03
Removed validation from ObjectParamConverter, because its not customi…
beberlei e8123ef
Add support for #__set_state() methods to create objects. Add support…
beberlei 0ce6149
Remove symfony/validator again
beberlei fdfb1f1
Remove "request" and "cookies" support.
beberlei File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony framework. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* This source file is subject to the MIT license that is bundled | ||
* with this source code in the file LICENSE. | ||
*/ | ||
|
||
namespace Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter; | ||
|
||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ConfigurationInterface; | ||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; | ||
use Symfony\Component\HttpKernel\Exception\HttpException; | ||
use Symfony\Component\HttpFoundation\Request; | ||
|
||
/** | ||
* Converts arrays into objects by mapping keys onto | ||
* constructor arguments. By restricing access to constructor | ||
* parameters only, security problems can be avoided and a central | ||
* location for filtering input data is available. | ||
* | ||
* Data is only ever retrieved from the query part of the request, | ||
* use the form framework for transforming POST data into objects. | ||
* | ||
* Converter can be used to generate criteria/filtering struct objects | ||
* that are passed to the model layer. | ||
* | ||
* Conversion works recursivly and with a special case for DateTime objects. | ||
* | ||
* Important note: This converter has to run AFTER any persistent object | ||
* converter such as the DoctrineParamConverter, so that data from the database | ||
* is preferred over user input data. Otherwise attackers could snuck in | ||
* objects to operate on. This is why only constructor arguments are mapped. | ||
* | ||
* @author Benjamin Eberlei <kontakt@beberlei.de> | ||
*/ | ||
class ObjectParamConverter implements ParamConverterInterface | ||
{ | ||
public function apply(Request $request, ConfigurationInterface $configuration) | ||
{ | ||
$param = $configuration->getName(); | ||
$options = $configuration->getOptions(); | ||
$part = isset($options['part']) ? $options['part'] : 'query'; | ||
|
||
if (!in_array($part, array('attributes', 'query', 'request', 'cookies'))) { | ||
throw new \RuntmeException("Invalid part to retrieve data from."); | ||
} | ||
|
||
if ($request->$part->has($param)) { | ||
$data = $request->$part->get($param, array()); | ||
} else { | ||
$data = array(); | ||
} | ||
|
||
$class = $configuration->getClass(); | ||
$object = $this->convertClass($class, $data); | ||
|
||
$request->attributes->set($param, $object); | ||
|
||
return true; | ||
} | ||
|
||
private function convertClass($class, $data) | ||
{ | ||
$reflClass = new \ReflectionClass($class); | ||
|
||
if ($reflClass->hasMethod('__set_state')) { | ||
return $class::__set_state($data); | ||
} | ||
|
||
$constructor = $reflClass->getConstructor(); | ||
|
||
return $reflClass->newInstanceArgs($this->convertClassArguments($constructor, $data)); | ||
} | ||
|
||
private function convertClassArguments($constructor, $data) | ||
{ | ||
$args = array(); | ||
|
||
if (!$constructor) { | ||
return array(); | ||
} | ||
|
||
if (is_scalar($data)) { | ||
return array($data); | ||
} | ||
|
||
foreach ($constructor->getParameters() as $parameter) { | ||
$argValue = null; | ||
$parameterName = $parameter->getName(); | ||
|
||
if (isset($data[$parameterName])) { | ||
$argValue = $data[$parameterName]; | ||
|
||
if ($parameter->getClass()) { | ||
if ($parameter->getClass()->getName() == "DateTime") { | ||
$argValue = new \DateTime($argValue); | ||
} else if (is_array($argValue)) { | ||
$argValue = $this->convertClass($parameter->getClass()->getName(), $argValue); | ||
} | ||
} | ||
|
||
} else if ($parameter->isOptional()) { | ||
$argValue = $parameter->getDefaultValue(); | ||
} else { | ||
throw new HttpException(400, "Missing parameter '" . $parameterName . "'"); | ||
} | ||
|
||
$args[] = $argValue; | ||
} | ||
|
||
return $args; | ||
} | ||
|
||
public function supports(ConfigurationInterface $configuration) | ||
{ | ||
return $configuration->getClass() !== null; | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
124 changes: 124 additions & 0 deletions
124
Tests/Request/ParamConverter/ObjectParamConverterTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
<?php | ||
|
||
namespace Sensio\Bundle\FrameworkExtraBundle\Tests\Request\ParamConverter; | ||
|
||
use Symfony\Component\HttpFoundation\Request; | ||
use Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\ObjectParamConverter; | ||
|
||
class ObjectParamConverterTest extends \PHPUnit_Framework_TestCase | ||
{ | ||
private $converter; | ||
|
||
public function setUp() | ||
{ | ||
$this->converter = new ObjectParamConverter(); | ||
} | ||
|
||
public function testApply() | ||
{ | ||
$request = Request::create('/', 'POST'); | ||
$request->query->set('foo', array('bar' => array('foo' => 1), 'baz' => '2012-07-21')); | ||
|
||
$config = $this->createConfiguration('foo', __NAMESPACE__ . '\\Foo'); | ||
|
||
$this->converter->apply($request, $config); | ||
|
||
$foo = $request->attributes->get('foo'); | ||
$this->assertInstanceOf(__NAMESPACE__ . '\\Foo', $foo); | ||
$this->assertInstanceOf(__NAMESPACE__ . '\\Bar', $foo->bar); | ||
$this->assertInstanceOf('DateTime', $foo->baz); | ||
$this->assertEquals(1, $foo->bar->foo); | ||
} | ||
|
||
public function testApplyPartRequest() | ||
{ | ||
$request = Request::create('/', 'POST'); | ||
$request->request->set('foo', array('bar' => array('foo' => 1), 'baz' => '2012-07-21')); | ||
|
||
$config = $this->createConfiguration('foo', __NAMESPACE__ . '\\Foo'); | ||
$config->expects($this->once())->method('getOptions')->will($this->returnValue(array('part' => 'request'))); | ||
|
||
$this->converter->apply($request, $config); | ||
|
||
$foo = $request->attributes->get('foo'); | ||
$this->assertInstanceOf(__NAMESPACE__ . '\\Foo', $foo); | ||
$this->assertInstanceOf(__NAMESPACE__ . '\\Bar', $foo->bar); | ||
$this->assertInstanceOf('DateTime', $foo->baz); | ||
$this->assertEquals(1, $foo->bar->foo); | ||
} | ||
|
||
public function testApplySetState() | ||
{ | ||
$request = Request::create('/', 'POST'); | ||
$request->query->set('foo', array('foo' => 1, 'bar' => 2)); | ||
|
||
$config = $this->createConfiguration('foo', __NAMESPACE__ . '\\Baz'); | ||
|
||
$this->converter->apply($request, $config); | ||
|
||
$baz = $request->attributes->get('foo'); | ||
$this->assertInstanceOf(__NAMESPACE__ . '\\Baz', $baz); | ||
|
||
$this->assertEquals(1, $baz->foo); | ||
$this->assertEquals(2, $baz->bar); | ||
} | ||
|
||
public function createConfiguration($name, $class, array $options = null) | ||
{ | ||
$config = $this->getMock( | ||
'Sensio\Bundle\FrameworkExtraBundle\Configuration\ConfigurationInterface', array( | ||
'getClass', 'getAliasName', 'getOptions', 'getName' | ||
)); | ||
if ($options !== null) { | ||
$config->expects($this->once()) | ||
->method('getOptions') | ||
->will($this->returnValue($options)); | ||
} | ||
$config->expects($this->any()) | ||
->method('getClass') | ||
->will($this->returnValue($class)); | ||
$config->expects($this->any()) | ||
->method('getName') | ||
->will($this->returnValue($name)); | ||
|
||
return $config; | ||
} | ||
} | ||
|
||
class Foo | ||
{ | ||
public $bar; | ||
public $baz; | ||
|
||
public function __construct(Bar $bar, \DateTime $baz) | ||
{ | ||
$this->bar = $bar; | ||
$this->baz = $baz; | ||
} | ||
} | ||
|
||
class Bar | ||
{ | ||
public $foo; | ||
|
||
public function __construct($foo) | ||
{ | ||
$this->foo = $foo; | ||
} | ||
} | ||
|
||
class Baz | ||
{ | ||
public $foo; | ||
public $bar; | ||
|
||
static public function __set_state(array $vars) | ||
{ | ||
$baz = new self(); | ||
$baz->foo = $vars['foo']; | ||
$baz->bar = $vars['bar']; | ||
|
||
return $baz; | ||
} | ||
} | ||
|
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The concept of converters is to convert request attributes to objects. I'm not sure that converting other "parts" of the request is something we want to support (cookies is probably out of question I think).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually just use it to access query, something similiar like https://github.com/FriendsOfSymfony/FOSRestBundle/blob/master/Request/ParamFetcher.php
However domain specific objects instead of the generic object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also for example http://php-and-symfony.matthiasnoback.nl/2012/03/symfony2-creating-a-paramconverter-for-deserializing-request-content/ is also very useful.
I think with named converters this actually makes sense. Say we don't make the object converter part of the iteration, but only allow to use it when explicitly named, in that case developers know what they are doing, when they use it.