diff --git a/src/ArgumentResolver/InputArgumentResolver.php b/src/ArgumentResolver/InputArgumentResolver.php index 19d79af..787ad6e 100644 --- a/src/ArgumentResolver/InputArgumentResolver.php +++ b/src/ArgumentResolver/InputArgumentResolver.php @@ -5,7 +5,6 @@ namespace Sfmok\RequestInput\ArgumentResolver; use Sfmok\RequestInput\InputInterface; -use Sfmok\RequestInput\Attribute\Input; use Symfony\Component\HttpFoundation\Request; use Sfmok\RequestInput\Factory\InputFactoryInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; @@ -13,22 +12,13 @@ class InputArgumentResolver implements ArgumentValueResolverInterface { - public function __construct(private InputFactoryInterface $inputFactory, private array $inputFormats) + public function __construct(private InputFactoryInterface $inputFactory) { } public function supports(Request $request, ArgumentMetadata $argument): bool { - if (!is_subclass_of($argument->getType(), InputInterface::class)) { - return false; - } - - /** @var Input|null $inputAttribute */ - if ($inputAttribute = $request->attributes->get('_input')) { - $this->inputFormats = [$inputAttribute->getFormat()]; - } - - return \in_array($request->getContentType(), $this->inputFormats); + return is_subclass_of($argument->getType(), InputInterface::class); } public function resolve(Request $request, ArgumentMetadata $argument): iterable diff --git a/src/Attribute/Input.php b/src/Attribute/Input.php index 3187954..8df7ed7 100644 --- a/src/Attribute/Input.php +++ b/src/Attribute/Input.php @@ -21,7 +21,7 @@ public function __construct( public function getFormat(): string { - return $this->format; + return mb_strtolower($this->format); } public function getGroups(): array diff --git a/src/DependencyInjection/RequestInputExtension.php b/src/DependencyInjection/RequestInputExtension.php index 2c220b9..41854ca 100644 --- a/src/DependencyInjection/RequestInputExtension.php +++ b/src/DependencyInjection/RequestInputExtension.php @@ -36,6 +36,7 @@ public function load(array $configs, ContainerBuilder $container): void '$serializer' => new Reference(SerializerInterface::class), '$validator' => new Reference(ValidatorInterface::class), '$skipValidation' => $config['skip_validation'], + '$inputFormats' => $config['formats'], ]) ->setPublic(false) ; @@ -48,7 +49,6 @@ public function load(array $configs, ContainerBuilder $container): void $container->register(InputArgumentResolver::class) ->setArguments([ '$inputFactory' => new Reference(InputFactoryInterface::class), - '$inputFormats' => $config['formats'], ]) ->addTag('controller.argument_value_resolver', ['priority' => 40]) ->setPublic(false) diff --git a/src/EventListener/ReadInputListener.php b/src/EventListener/ReadInputListener.php index 81cba02..cbfc984 100644 --- a/src/EventListener/ReadInputListener.php +++ b/src/EventListener/ReadInputListener.php @@ -23,7 +23,7 @@ public function onKernelController(ControllerEvent $event): void return; } - if (!\in_array($inputMetadata->getFormat(), Input::INPUT_SUPPORTED_FORMATS)) { + if (!\in_array($inputMetadata->getFormat(), Input::INPUT_SUPPORTED_FORMATS, true)) { throw new UnexpectedFormatException(sprintf( 'Only the formats [%s] are supported. Got %s.', implode(', ', Input::INPUT_SUPPORTED_FORMATS), diff --git a/src/Factory/InputFactory.php b/src/Factory/InputFactory.php index 4ab6ccc..d5da00e 100644 --- a/src/Factory/InputFactory.php +++ b/src/Factory/InputFactory.php @@ -10,8 +10,6 @@ use Sfmok\RequestInput\InputInterface; use Sfmok\RequestInput\Exception\ValidationException; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\Serializer\Exception\NotEncodableValueException; -use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use Symfony\Component\Serializer\Exception\UnexpectedValueException; use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; @@ -24,17 +22,21 @@ final class InputFactory implements InputFactoryInterface public function __construct( private SerializerInterface $serializer, private ValidatorInterface $validator, - private bool $skipValidation + private bool $skipValidation, + private array $inputFormats ) { } - public function createFromRequest(Request $request, string $type, string $format): InputInterface + public function createFromRequest(Request $request, string $type, ?string $format): InputInterface { - if (!\in_array($format, Input::INPUT_SUPPORTED_FORMATS)) { + $inputMetadata = $request->attributes->get('_input'); + $supportedFormats = (array) ($inputMetadata?->getFormat() ?? $this->inputFormats); + + if (!\in_array($format, $supportedFormats, true)) { throw new UnexpectedFormatException(sprintf( - 'Only the formats [%s] are supported. Got %s.', - implode(', ', Input::INPUT_SUPPORTED_FORMATS), - $format + 'Unexpected request content type, expected any of [%s]. Got "%s".', + implode(', ', $this->getExpectedContentTypes($request, $supportedFormats)), + $request->getMimeType($format ?? '') )); } @@ -44,8 +46,6 @@ public function createFromRequest(Request $request, string $type, string $format $format = Input::INPUT_JSON_FORMAT; } - $inputMetadata = $request->attributes->get('_input'); - try { $input = $this->serializer->deserialize($data, $type, $format, $inputMetadata?->getContext() ?? []); } catch (UnexpectedValueException $exception) { @@ -63,4 +63,14 @@ public function createFromRequest(Request $request, string $type, string $format return $input; } + + private function getExpectedContentTypes(Request $request, array $expectedFormats): array + { + $expectedContentTypes = []; + foreach ($expectedFormats as $format) { + $expectedContentTypes = [...$expectedContentTypes, ...$request->getMimeTypes($format)]; + } + + return $expectedContentTypes; + } } diff --git a/src/Factory/InputFactoryInterface.php b/src/Factory/InputFactoryInterface.php index cd6fbd0..7167885 100644 --- a/src/Factory/InputFactoryInterface.php +++ b/src/Factory/InputFactoryInterface.php @@ -9,5 +9,5 @@ interface InputFactoryInterface { - public function createFromRequest(Request $request, string $type, string $format): InputInterface; + public function createFromRequest(Request $request, string $type, ?string $format): InputInterface; } diff --git a/tests/ArgumentResolver/InputArgumentResolverTest.php b/tests/ArgumentResolver/InputArgumentResolverTest.php index d83471f..3c80994 100644 --- a/tests/ArgumentResolver/InputArgumentResolverTest.php +++ b/tests/ArgumentResolver/InputArgumentResolverTest.php @@ -8,7 +8,6 @@ use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Sfmok\RequestInput\ArgumentResolver\InputArgumentResolver; -use Sfmok\RequestInput\Attribute\Input; use Sfmok\RequestInput\Factory\InputFactoryInterface; use Sfmok\RequestInput\Tests\Fixtures\Input\DummyInput; use Symfony\Component\HttpFoundation\Request; @@ -25,119 +24,41 @@ protected function setUp(): void $this->inputFactory = $this->prophesize(InputFactoryInterface::class); } - public function testSupportsWithArgumentTypeNotInput(): void - { - $request = new Request(); - $request->headers->set('Content-Type', 'application/json'); - $argument = new ArgumentMetadata('foo', \stdClass::class, false, false, null); - - $resolver = $this->createArgumentResolver(Input::INPUT_SUPPORTED_FORMATS); - $this->assertFalse($resolver->supports($request, $argument)); - } - - /** - * @dataProvider provideSupportsWithDefaultGlobalFormats - */ - public function testSupportsWithDefaultGlobalFormats(bool $expected, ?string $contentType): void - { - $request = new Request(); - $request->headers->set('Content-Type', $contentType); - $argument = new ArgumentMetadata('foo', DummyInput::class, false, false, null); - - $resolver = $this->createArgumentResolver(Input::INPUT_SUPPORTED_FORMATS); - $this->assertSame($expected, $resolver->supports($request, $argument)); - } - - /** - * @dataProvider provideSupportsWithCustomGlobalFormats - */ - public function testSupportsWithCustomGlobalFormats(bool $expected, ?string $contentType): void - { - $request = new Request(); - $request->headers->set('Content-Type', $contentType); - - $argument = new ArgumentMetadata('foo', DummyInput::class, false, false, null); - - $resolver = $this->createArgumentResolver(['json']); - $this->assertSame($expected, $resolver->supports($request, $argument)); - } - /** - * @dataProvider provideSupportsWithCustomFormatsInInputAttribute + * @dataProvider provideSupportData */ - public function testSupportsWithCustomFormatsInInputAttribute(bool $expected, ?string $contentType): void + public function testSupports(mixed $type, bool $expectedResult): void { - $request = new Request(); - $request->headers->set('Content-Type', $contentType); - $request->attributes->set('_input', new Input('xml')); - - $argument = new ArgumentMetadata('foo', DummyInput::class, false, false, null); - - $resolver = $this->createArgumentResolver(Input::INPUT_SUPPORTED_FORMATS); - $this->assertSame($expected, $resolver->supports($request, $argument)); + $argument = new ArgumentMetadata('foo', $type, false, false, null); + $this->assertSame($expectedResult, $this->createArgumentResolver()->supports(new Request(), $argument)); } - public function testResolveSucceeds(): void + public function testResolve(): void { $dummyInput = new DummyInput(); - $request = new Request(); - $request->headers->set('Content-Type', 'application/json'); - - $argument = new ArgumentMetadata('foo', DummyInput::class, false, false, null); - - $resolver = $this->createArgumentResolver([Input::INPUT_SUPPORTED_FORMATS]); + $argument = new ArgumentMetadata('foo', $dummyInput::class, false, false, null); $this->inputFactory - ->createFromRequest($request, $argument->getType(), $request->getContentType()) + ->createFromRequest($request, $dummyInput::class, $request->getContentType()) ->shouldBeCalledOnce() ->willReturn($dummyInput) ; + $resolver = $this->createArgumentResolver(); $this->assertEquals([$dummyInput], iterator_to_array($resolver->resolve($request, $argument))); } - public function provideSupportsWithDefaultGlobalFormats(): iterable - { - yield [false, null]; - yield [false, 'application/rdf+xml']; - yield [false, 'text/html']; - yield [false, 'application/javascript']; - yield [false, 'text/plain']; - yield [false, 'application/ld+json']; - yield [true, 'application/json']; - yield [true, 'application/xml']; - yield [true, 'multipart/form-data']; - } - - public function provideSupportsWithCustomGlobalFormats(): iterable - { - yield [false, null]; - yield [false, 'application/rdf+xml']; - yield [false, 'text/html']; - yield [false, 'application/javascript']; - yield [false, 'text/plain']; - yield [false, 'application/ld+json']; - yield [true, 'application/json']; - yield [false, 'application/xml']; - yield [false, 'multipart/form-data']; - } - - public function provideSupportsWithCustomFormatsInInputAttribute(): iterable + public function provideSupportData(): iterable { - yield [false, null]; - yield [false, 'application/rdf+xml']; - yield [false, 'text/html']; - yield [false, 'application/javascript']; - yield [false, 'text/plain']; - yield [false, 'application/ld+json']; - yield [false, 'application/json']; - yield [true, 'application/xml']; - yield [false, 'multipart/form-data']; + yield [null, false]; + yield [\stdClass::class, false]; + yield ["Foo", false]; + yield [DummyInput::class, true]; } - private function createArgumentResolver(array $formats): InputArgumentResolver + private function createArgumentResolver(): InputArgumentResolver { - return new InputArgumentResolver($this->inputFactory->reveal(), $formats); + return new InputArgumentResolver($this->inputFactory->reveal()); } } \ No newline at end of file diff --git a/tests/DependencyInjection/RequestInputExtensionTest.php b/tests/DependencyInjection/RequestInputExtensionTest.php index eeba8ed..f3a3e05 100644 --- a/tests/DependencyInjection/RequestInputExtensionTest.php +++ b/tests/DependencyInjection/RequestInputExtensionTest.php @@ -57,6 +57,23 @@ public function testLoadConfiguration(): void $this->assertServiceHasTags(ReadInputListener::class, ['kernel.event_listener']); } + public function testLoadConfigurationWithDisabledOption(): void + { + (new RequestInputExtension())->load(['request_input' => ['enabled' => false]], $this->container); + + $services = [ + InputArgumentResolver::class, + ExceptionListener::class, + ReadInputListener::class, + InputFactory::class, + InputMetadataFactory::class + ]; + + foreach ($services as $service) { + $this->assertFalse($this->container->hasDefinition($service), sprintf('Definition "%s" is found.', $service)); + } + } + private function assertContainerHas(array $services, array $aliases = []): void { foreach ($services as $service) { diff --git a/tests/Factory/InputFactoryTest.php b/tests/Factory/InputFactoryTest.php index 5056acc..9eb4d57 100644 --- a/tests/Factory/InputFactoryTest.php +++ b/tests/Factory/InputFactoryTest.php @@ -37,7 +37,7 @@ protected function setUp(): void /** * @dataProvider provideDataRequestWithContent */ - public function testCreateFormRequestWithContent(Request $request): void + public function testCreateFromRequestWithContent(Request $request): void { $input = $this->getDummyInput(); $violations = new ConstraintViolationList([]); @@ -61,7 +61,7 @@ public function testCreateFormRequestWithContent(Request $request): void /** * @dataProvider provideDataRequestWithFrom */ - public function testCreateFormRequestWithForm(Request $request): void + public function testCreateFromRequestWithForm(Request $request): void { $input = $this->getDummyInput(); $violations = new ConstraintViolationList([]); @@ -84,12 +84,14 @@ public function testCreateFormRequestWithForm(Request $request): void } /** - * @dataProvider provideDataUnsupportedFormat + * @dataProvider provideDataUnsupportedContentType */ - public function testCreateFormRequestFromUnsupportedFormat(Request $request): void + public function testCreateFromRequestWithUnsupportedContentType(Request $request): void { $this->expectException(UnexpectedFormatException::class); - $this->expectExceptionMessageMatches('/Only the formats .+ are supported. Got .+./'); + $this->expectExceptionMessageMatches( + '/Unexpected request content type, expected any of .+. Got .+./' + ); $input = $this->getDummyInput(); $this->serializer->deserialize()->shouldNotBeCalled(); @@ -101,7 +103,7 @@ public function testCreateFormRequestFromUnsupportedFormat(Request $request): vo /** * @dataProvider provideDataRequestWithContent */ - public function testCreateFormRequestWithSkipValidation(Request $request): void + public function testCreateFromRequestWithSkipValidation(Request $request): void { $input = $this->getDummyInput(); @@ -119,7 +121,7 @@ public function testCreateFormRequestWithSkipValidation(Request $request): void /** * @dataProvider provideDataRequestWithContent */ - public function testCreateFormRequestWithInputMetadata(Request $request): void + public function testCreateFromRequestWithInputMetadata(Request $request): void { $input = $this->getDummyInput(); $request->attributes->set('_input', new Input(groups: ['foo'], context: ['groups' => 'foo'])); @@ -144,7 +146,7 @@ public function testCreateFormRequestWithInputMetadata(Request $request): void /** * @dataProvider provideDataRequestWithFrom */ - public function testCreateFormRequestWithDeserializationException(Request $request): void + public function testCreateFromRequestWithDeserializationException(Request $request): void { $this->expectException(DeserializationException::class); @@ -166,7 +168,7 @@ public function testCreateFormRequestWithDeserializationException(Request $reque /** * @dataProvider provideDataRequestWithFrom */ - public function testCreateFormRequestWithValidationException(Request $request): void + public function testCreateFromRequestWithValidationException(Request $request): void { $this->expectException(ValidationException::class); @@ -193,7 +195,6 @@ public function testCreateFormRequestWithValidationException(Request $request): public function provideDataRequestWithContent(): iterable { yield [new Request(server: ['CONTENT_TYPE' => 'application/json'])]; - yield [new Request(server: ['CONTENT_TYPE' => 'application/xml'])]; yield [new Request(server: ['CONTENT_TYPE' => 'application/x-json'])]; } @@ -203,9 +204,9 @@ public function provideDataRequestWithFrom(): iterable yield [new Request(server: ['CONTENT_TYPE' => 'multipart/form-data'])]; } - public function provideDataUnsupportedFormat(): iterable + public function provideDataUnsupportedContentType(): iterable { - yield [new Request(server: ['CONTENT_TYPE' => 'text/html'])]; + yield [new Request()]; yield [new Request(server: ['CONTENT_TYPE' => 'application/xhtml+xml'])]; yield [new Request(server: ['CONTENT_TYPE' => 'text/plain'])]; yield [new Request(server: ['CONTENT_TYPE' => 'application/javascript'])]; @@ -217,7 +218,12 @@ public function provideDataUnsupportedFormat(): iterable private function createInputFactory(bool $skipValidation): InputFactoryInterface { - return new InputFactory($this->serializer->reveal(), $this->validator->reveal(), $skipValidation); + return new InputFactory( + $this->serializer->reveal(), + $this->validator->reveal(), + $skipValidation, + Input::INPUT_SUPPORTED_FORMATS + ); } private function getDummyInput(): DummyInput diff --git a/tests/Fixtures/Input/DummyInput.php b/tests/Fixtures/Input/DummyInput.php index 8e84390..a8b7721 100644 --- a/tests/Fixtures/Input/DummyInput.php +++ b/tests/Fixtures/Input/DummyInput.php @@ -10,25 +10,17 @@ class DummyInput implements InputInterface { - /** - * @Assert\NotBlank() - */ + #[Assert\NotBlank] private string $title; - /** - * @Assert\NotBlank() - */ + #[Assert\NotBlank] private string $content; - /** - * @Assert\Type(type="array") - */ + #[Assert\Type(type: 'array')] private array $tags = []; - /** - * @SerializedName('author') - * @Assert\NotBlank() - */ + #[SerializedName('author')] + #[Assert\NotBlank] private string $name; public function getTitle(): string