Skip to content

Instantly share code, notes, and snippets.

@faizanakram99
Last active October 13, 2021 13:07
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save faizanakram99/33c97a5f5b834dceeb90003574c7b98d to your computer and use it in GitHub Desktop.
Save faizanakram99/33c97a5f5b834dceeb90003574c7b98d to your computer and use it in GitHub Desktop.
RequestDTOArgumentValueResolver
<?php
namespace Qbil\Tests\CommonBundle\Services;
use Qbil\CommonBundle\Services\DTOArgumentValueResolver;
use Qbil\Tests\Fixtures\Controller\DTOController;
use Qbil\Tests\Fixtures\DTO\Person;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory;
class ArgumentResolverTest extends KernelTestCase
{
/** @var ArgumentResolver */
private static $resolver;
public static function setUpBeforeClass(): void
{
self::bootKernel();
$factory = new ArgumentMetadataFactory();
$denormalizer = self::$container->get('serializer');
$validator = self::$container->get('validator');
self::$resolver = new ArgumentResolver($factory, [new DTOArgumentValueResolver($denormalizer, $validator)]);
}
public function testDTOIsPassedToController()
{
$request = Request::create(
'/',
Request::METHOD_POST,
['id' => 1, 'name' => 'foo']
);
$controller = [new DTOController(), 'foo'];
$expectedPerson = new Person();
$expectedPerson->id = 1;
$expectedPerson->name = 'foo';
self::assertEquals([$expectedPerson], self::$resolver->getArguments($request, $controller));
}
}
<?php
namespace Qbil\CommonBundle\Services;
use Qbil\CommonBundle\RequestDTOInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
final class DTOArgumentValueResolver implements ArgumentValueResolverInterface
{
private $denormalizer;
private $validator;
public function __construct(
DenormalizerInterface $denormalizer,
ValidatorInterface $validator
) {
$this->denormalizer = $denormalizer;
$this->validator = $validator;
}
public function supports(Request $request, ArgumentMetadata $argument): bool
{
return null !== ($type = $argument->getType()) &&
!in_array($type, Type::$builtinTypes, true) &&
in_array(RequestDTOInterface::class, class_implements($type), true);
}
public function resolve(Request $request, ArgumentMetadata $argument)
{
if (Request::METHOD_POST === $request->getMethod()) {
$data = $request->request->all();
} elseif (Request::METHOD_GET === $request->getMethod()) {
$data = $request->query->all();
}
if (
'json' === $request->getContentType()
// not sure if the second check is required, so commented out
// && in_array($request->getMethod(), [Request::METHOD_PATCH, Request::METHOD_PUT, Request::METHOD_POST], true)
) {
$data = \json_decode($request->getContent(), true, 512, JSON_THROW_ON_ERROR);
}
$dto = $this->denormalizer->denormalize($data, $argument->getType());
$this->assertDTOIsValid($dto);
yield $dto;
}
private function assertDTOIsValid(RequestDTOInterface $dto)
{
$errors = [];
foreach ($this->validator->validate($dto) as $constraintViolation) {
/* @var ConstraintViolationInterface $constraintViolation */
$errors[] = $constraintViolation->getMessage();
}
if (0 !== count($errors)) {
throw new BadRequestHttpException(implode("\n", $errors));
}
}
}
<?php
namespace Qbil\Tests\CommonBundle\Services;
use Qbil\CommonBundle\Services\DTOArgumentValueResolver;
use Qbil\Tests\Fixtures\DTO\Person;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
class DTOArgumentValueResolverTest extends KernelTestCase
{
public function testResolve()
{
self::bootKernel();
$argumentMetaData = $this->createMock(ArgumentMetadata::class);
$denormalizer = self::$container->get('serializer');
$validator = self::$container->get('validator');
$argumentValueResolver = new DTOArgumentValueResolver($denormalizer, $validator);
$request = new Request([], ['id' => 1, 'name' => 'foo']);
$request->setMethod(Request::METHOD_POST);
$expectedPerson = new Person();
$expectedPerson->id = 1;
$expectedPerson->name = 'foo';
$argumentMetaData
->method('getType')
->willReturn(Person::class);
$actualPerson = $argumentValueResolver->resolve($request, $argumentMetaData)->current();
self::assertEquals($expectedPerson, $actualPerson);
}
}
<?php
namespace Qbil\Tests\Fixtures\Controller;
use Qbil\Tests\Fixtures\DTO\Person;
class DTOController
{
public function foo(Person $person)
{
}
}
<?php
namespace Qbil\Tests\Fixtures\DTO;
use Qbil\CommonBundle\RequestDTOInterface;
use Symfony\Component\Validator\Constraints as Assert;
class Person implements RequestDTOInterface
{
/**
* @Assert\Positive(message="id should be a natural number")
*/
public $id;
public $name;
}
<?php
namespace Qbil\CommonBundle;
interface RequestDTOInterface
{
}
services:
Qbil\CommonBundle\Services\DTOArgumentValueResolver:
tags: ['controller.argument_value_resolver']
@Pierstoval
Copy link

In DTOArgumentValueResolver::resolve(), I would suggest to not merge GET and POST data or content, this could lead to bad injections, and instead to use a more straightforward implementation like if ($request->isMethod('GET')) { $data = $request->query->all(); } elseif (...) {...}

@faizanakram99
Copy link
Author

In DTOArgumentValueResolver::resolve(), I would suggest to not merge GET and POST data or content, this could lead to bad injections, and instead to use a more straightforward implementation like if ($request->isMethod('GET')) { $data = $request->query->all(); } elseif (...) {...}

Done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment