Skip to content

Commit 76e4dac

Browse files
author
Mokhtar Tlili
committed
add bundle configuration
1 parent baeadd7 commit 76e4dac

File tree

9 files changed

+292
-22
lines changed

9 files changed

+292
-22
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
"symfony/framework-bundle": "^4.4 || ^5.4 || ^6.0",
3434
"phpspec/prophecy-phpunit": "^2.0",
3535
"friendsofphp/php-cs-fixer": "^3.13",
36-
"phpstan/phpstan": "^1.9"
36+
"phpstan/phpstan": "^1.9",
37+
"symfony/yaml": "^6.1"
3738
},
3839
"autoload": {
3940
"psr-4": {

src/ArgumentResolver/InputArgumentResolver.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,19 @@
1313
class InputArgumentResolver implements ArgumentValueResolverInterface
1414
{
1515
private InputFactoryInterface $inputFactory;
16+
private array $inputFormats;
17+
private bool $enabled;
1618

17-
public function __construct(InputFactoryInterface $inputFactory)
19+
public function __construct(InputFactoryInterface $inputFactory, array $inputFormats, bool $enabled = true)
1820
{
1921
$this->inputFactory = $inputFactory;
22+
$this->inputFormats = $inputFormats;
23+
$this->enabled = $enabled;
2024
}
2125

2226
public function supports(Request $request, ArgumentMetadata $argument): bool
2327
{
24-
if (!\in_array($request->getContentType(), InputFactoryInterface::INPUT_FORMATS)) {
28+
if (!$this->enabled || !\in_array($request->getContentType(), $this->inputFormats)) {
2529
return false;
2630
}
2731

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sfmok\RequestInput\DependencyInjection;
6+
7+
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
8+
use Symfony\Component\Config\Definition\ConfigurationInterface;
9+
use Sfmok\RequestInput\Factory\InputFactoryInterface;
10+
11+
/**
12+
* Configuration.
13+
*
14+
* @internal
15+
*/
16+
class Configuration implements ConfigurationInterface
17+
{
18+
public function getConfigTreeBuilder(): TreeBuilder
19+
{
20+
$treeBuilder = new TreeBuilder('request_input');
21+
$root = $treeBuilder->getRootNode();
22+
23+
$root
24+
->fixXmlConfig('format', 'formats')
25+
->addDefaultsIfNotSet()
26+
->children()
27+
->booleanNode('enabled')
28+
->defaultTrue()
29+
->end()
30+
->arrayNode('formats')
31+
->defaultValue(InputFactoryInterface::INPUT_FORMATS)
32+
->requiresAtLeastOneElement()
33+
->scalarPrototype()->end()
34+
->validate()
35+
->ifTrue(function ($values) {
36+
foreach ($values as $value) {
37+
if (!\in_array($value, InputFactoryInterface::INPUT_FORMATS)) {
38+
return true;
39+
}
40+
}
41+
return false;
42+
})
43+
->thenInvalid(sprintf('Only the formats %s are supported. Got %s.', implode(', ', InputFactoryInterface::INPUT_FORMATS), '%s'))
44+
->end()
45+
->end()
46+
->end();
47+
48+
return $treeBuilder;
49+
}
50+
}

src/DependencyInjection/RequestInputExtension.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,24 @@
99
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
1010
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
1111

12+
/**
13+
* @internal
14+
*/
1215
class RequestInputExtension extends Extension
1316
{
14-
public function load(array $configs, ContainerBuilder $container)
17+
public function load(array $configs, ContainerBuilder $container): void
18+
{
19+
$configuration = new Configuration();
20+
$config = $this->processConfiguration($configuration, $configs);
21+
22+
# define a few parameters
23+
$container->setParameter('request_input.enabled', $config['enabled']);
24+
$container->setParameter('request_input.formats', $config['formats']);
25+
26+
$this->loadServicesFiles($container);
27+
}
28+
29+
protected function loadServicesFiles(ContainerBuilder $container): void
1530
{
1631
$loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
1732
$loader->load('services.yaml');

src/Factory/InputFactory.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
use Symfony\Component\Serializer\SerializerInterface;
1212
use Symfony\Component\Validator\Validator\ValidatorInterface;
1313

14+
/**
15+
* @internal
16+
*/
1417
final class InputFactory implements InputFactoryInterface
1518
{
1619
private SerializerInterface $serializer;

src/Resources/config/services.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ services:
22
_defaults:
33
autowire: true
44

5-
Sfmok\RequestInput\Factory\InputFactory: ~
5+
Sfmok\RequestInput\Factory\InputFactory:
6+
arguments:
7+
$inputFormats: '%request_input.formats%'
8+
$enabled: '%request_input.enabled%'
69

710
Sfmok\RequestInput\Factory\InputFactoryInterface: '@Sfmok\RequestInput\Factory\InputFactory'
811

tests/ArgumentResolver/InputArgumentResolverTest.php

Lines changed: 59 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,43 @@ protected function setUp(): void
2525
}
2626

2727
/**
28-
* @dataProvider provideSupports
28+
* @dataProvider provideSupportsWithDefaultFormats
2929
*/
30-
public function testSupports(bool $expected, Request $request, ArgumentMetadata $argument): void
30+
public function testSupportsWithDefaultFormats(bool $expected, ?string $contentType, string $inputClass): void
3131
{
32-
$resolver = $this->createArgumentResolver();
32+
$request = new Request();
33+
$request->headers->set('Content-Type', $contentType);
34+
35+
$argument = new ArgumentMetadata('foo', $inputClass, false, false, null);
36+
37+
$resolver = $this->createArgumentResolver(InputFactoryInterface::INPUT_FORMATS);
38+
$this->assertSame($expected, $resolver->supports($request, $argument));
39+
}
40+
41+
/**
42+
* @dataProvider provideSupportsWithCustomFormats
43+
*/
44+
public function testSupportsWithCustomFormats(bool $expected, ?string $contentType, string $inputClass): void
45+
{
46+
$request = new Request();
47+
$request->headers->set('Content-Type', $contentType);
48+
49+
$argument = new ArgumentMetadata('foo', $inputClass, false, false, null);
50+
51+
$resolver = $this->createArgumentResolver(['json']);
3352
$this->assertSame($expected, $resolver->supports($request, $argument));
3453
}
3554

3655
public function testResolveSucceeds(): void
3756
{
3857
$dummyInput = new DummyInput();
39-
$resolver = $this->createArgumentResolver();
58+
59+
$request = new Request();
60+
$request->headers->set('Content-Type', 'application/json');
61+
4062
$argument = new ArgumentMetadata('foo', DummyInput::class, false, false, null);
41-
$request = new Request([], [], [], [], [], ['CONTENT_TYPE' => 'application/json']);
63+
64+
$resolver = $this->createArgumentResolver([InputFactoryInterface::INPUT_FORMATS]);
4265

4366
$this->inputFactory
4467
->createFromRequest($request, $argument->getType(), $request->getContentType())
@@ -49,21 +72,40 @@ public function testResolveSucceeds(): void
4972
$this->assertEquals([$dummyInput], iterator_to_array($resolver->resolve($request, $argument)));
5073
}
5174

52-
public function provideSupports(): iterable
75+
public function provideSupportsWithDefaultFormats(): iterable
76+
{
77+
yield [false, null, \stdClass::class];
78+
yield [false, 'application/rdf+xml', \stdClass::class];
79+
yield [false, 'text/html', DummyInput::class];
80+
yield [false, 'application/javascript', DummyInput::class];
81+
yield [false, 'text/plain', DummyInput::class];
82+
yield [false, 'application/ld+json', DummyInput::class];
83+
yield [false, 'application/json', \stdClass::class];
84+
yield [true, 'application/json', DummyInput::class];
85+
yield [true, 'application/xml', DummyInput::class];
86+
yield [false, 'application/xml', \stdClass::class];
87+
yield [false, 'multipart/form-data', \stdClass::class];
88+
yield [true, 'multipart/form-data', DummyInput::class];
89+
}
90+
91+
public function provideSupportsWithCustomFormats(): iterable
5392
{
54-
yield [false, new Request(), new ArgumentMetadata('foo', \stdClass::class, false, false, null)];
55-
yield [false, new Request([], [], [], [], [], ['CONTENT_TYPE' => 'application/rdf+xml']), new ArgumentMetadata('foo', \stdClass::class, false, false, null)];
56-
yield [false, new Request([], [], [], [], [], ['CONTENT_TYPE' => 'text/html']), new ArgumentMetadata('foo', DummyInput::class, false, false, null)];
57-
yield [false, new Request([], [], [], [], [], ['CONTENT_TYPE' => 'application/javascript']), new ArgumentMetadata('foo', DummyInput::class, false, false, null)];
58-
yield [false, new Request([], [], [], [], [], ['CONTENT_TYPE' => 'text/plain']), new ArgumentMetadata('foo', DummyInput::class, false, false, null)];
59-
yield [true, new Request([], [], [], [], [], ['CONTENT_TYPE' => 'application/json']), new ArgumentMetadata('foo', DummyInput::class, false, false, null)];
60-
yield [false, new Request([], [], [], [], [], ['CONTENT_TYPE' => 'application/ld+json']), new ArgumentMetadata('foo', DummyInput::class, false, false, null)];
61-
yield [true, new Request([], [], [], [], [], ['CONTENT_TYPE' => 'application/xml']), new ArgumentMetadata('foo', DummyInput::class, false, false, null)];
62-
yield [true, new Request([], [], [], [], [], ['CONTENT_TYPE' => 'multipart/form-data']), new ArgumentMetadata('foo', DummyInput::class, false, false, null)];
93+
yield [false, null, \stdClass::class];
94+
yield [false, 'application/rdf+xml', \stdClass::class];
95+
yield [false, 'text/html', DummyInput::class];
96+
yield [false, 'application/javascript', DummyInput::class];
97+
yield [false, 'text/plain', DummyInput::class];
98+
yield [false, 'application/ld+json', DummyInput::class];
99+
yield [false, 'application/json', \stdClass::class];
100+
yield [true, 'application/json', DummyInput::class];
101+
yield [false, 'application/xml', DummyInput::class];
102+
yield [false, 'application/xml', \stdClass::class];
103+
yield [false, 'multipart/form-data', \stdClass::class];
104+
yield [false, 'multipart/form-data', DummyInput::class];
63105
}
64106

65-
private function createArgumentResolver(): InputArgumentResolver
107+
private function createArgumentResolver(array $formats): InputArgumentResolver
66108
{
67-
return new InputArgumentResolver($this->inputFactory->reveal());
109+
return new InputArgumentResolver($this->inputFactory->reveal(), $formats);
68110
}
69111
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sfmok\RequestInput\Tests\DependencyInjection;
6+
7+
use PHPUnit\Framework\TestCase;
8+
use Sfmok\RequestInput\DependencyInjection\Configuration;
9+
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
10+
use Symfony\Component\Config\Definition\ConfigurationInterface;
11+
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
12+
use Symfony\Component\Config\Definition\Processor;
13+
14+
class ConfigurationTest extends TestCase
15+
{
16+
private Configuration $configuration;
17+
private Processor $processor;
18+
19+
protected function setUp(): void
20+
{
21+
$this->configuration = new Configuration();
22+
$this->processor = new Processor();
23+
}
24+
25+
public function testDefaultConfig(): void
26+
{
27+
$treeBuilder = $this->configuration->getConfigTreeBuilder();
28+
$config = $this->processor->processConfiguration($this->configuration, []);
29+
30+
$this->assertInstanceOf(ConfigurationInterface::class, $this->configuration);
31+
$this->assertInstanceOf(TreeBuilder::class, $treeBuilder);
32+
$this->assertEquals([
33+
'enabled' => true,
34+
'formats' => ['json', 'xml', 'form'],
35+
], $config);
36+
37+
}
38+
39+
public function invalidFormatsProvider(): iterable
40+
{
41+
yield [['js']];
42+
yield [['html']];
43+
yield [['json', 'xml', 'form', 'txt']];
44+
yield [['form', 'pdf']];
45+
}
46+
47+
/**
48+
* @dataProvider invalidFormatsProvider
49+
*/
50+
public function testInvalidFormatsConfig(array $formats): void
51+
{
52+
$this->expectException(InvalidConfigurationException::class);
53+
$this->expectExceptionMessageMatches('/Only the formats .+ are supported. Got .+./');
54+
55+
$this->processor->processConfiguration($this->configuration, [
56+
'request_input' => [
57+
'formats' => $formats
58+
]
59+
]);
60+
}
61+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sfmok\RequestInput\Tests\DependencyInjection;
6+
7+
use PHPUnit\Framework\TestCase;
8+
use Sfmok\RequestInput\ArgumentResolver\InputArgumentResolver;
9+
use Sfmok\RequestInput\DependencyInjection\RequestInputExtension;
10+
use Sfmok\RequestInput\EventListener\ExceptionListener;
11+
use Sfmok\RequestInput\Factory\InputFactory;
12+
use Sfmok\RequestInput\Factory\InputFactoryInterface;
13+
use Symfony\Component\DependencyInjection\ContainerBuilder;
14+
15+
class RequestInputExtensionTest extends TestCase
16+
{
17+
public const DEFAULT_CONFIG = [
18+
'request_input' => [
19+
'enabled' => true,
20+
'formats' => ['json', 'xml', 'form'],
21+
]
22+
];
23+
24+
private ContainerBuilder $container;
25+
26+
protected function setUp(): void
27+
{
28+
$this->container = new ContainerBuilder();
29+
}
30+
31+
public function testLoadConfiguration(): void
32+
{
33+
$config = self::DEFAULT_CONFIG;
34+
(new RequestInputExtension())->load($config, $this->container);
35+
36+
$services = [
37+
InputArgumentResolver::class,
38+
ExceptionListener::class,
39+
InputFactory::class,
40+
];
41+
42+
$aliases = [
43+
InputFactoryInterface::class
44+
];
45+
46+
$parameters = [
47+
'request_input.enabled' => $config['request_input']['enabled'],
48+
'request_input.formats' => $config['request_input']['formats'],
49+
];
50+
51+
$this->assertContainerHas($services, $aliases, $parameters);
52+
53+
$this->assertServiceHasTags(InputArgumentResolver::class, ['controller.argument_value_resolver']);
54+
$this->assertServiceHasTags(ExceptionListener::class, ['kernel.event_listener']);
55+
}
56+
57+
private function assertContainerHas(array $services, array $aliases = [], array $parameters = []): void
58+
{
59+
foreach ($services as $service) {
60+
$this->assertTrue($this->container->hasDefinition($service), sprintf('Definition "%s" not found.', $service));
61+
}
62+
63+
foreach ($aliases as $alias) {
64+
$this->assertContainerHasAlias($alias);
65+
}
66+
67+
foreach ($parameters as $parameterKey => $parameterValue) {
68+
$this->assertContainerHasParameter($parameterKey, $parameterValue);
69+
}
70+
}
71+
72+
private function assertContainerHasAlias(string $alias): void
73+
{
74+
$this->assertTrue($this->container->hasAlias($alias), sprintf('Alias "%s" not found.', $alias));
75+
}
76+
77+
private function assertContainerHasParameter(string $parameterKey, $parameterValue): void
78+
{
79+
$this->assertTrue($this->container->hasParameter($parameterKey), sprintf('Parameter "%s" not found.', $parameterKey));
80+
$this->assertSame($this->container->getParameter($parameterKey), $parameterValue);
81+
}
82+
83+
private function assertServiceHasTags(string $service, array $tags = []): void
84+
{
85+
$serviceTags = $this->container->getDefinition($service)->getTags();
86+
87+
foreach ($tags as $tag) {
88+
$this->assertArrayHasKey($tag, $serviceTags, sprintf('Tag "%s" not found on the service "%s".', $tag, $service));
89+
}
90+
}
91+
}

0 commit comments

Comments
 (0)