Skip to content

Commit aa40414

Browse files
committed
Add payload param converter
1 parent a12ac48 commit aa40414

File tree

8 files changed

+248
-0
lines changed

8 files changed

+248
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## 2.6.x-dev
44

55
* MongoDB: Possibility to add execute options (aggregate command fields) for a resource, like `allowDiskUse` (#3144)
6+
* Allow controller argument with a name different from `$data` thanks to an argument resolver
67

78
## 2.5.1
89

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace ApiPlatform\Core\Bridge\Symfony\Bundle\ArgumentResolver;
4+
5+
use ApiPlatform\Core\Util\RequestAttributesExtractor;
6+
use Symfony\Component\HttpFoundation\Request;
7+
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
8+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
9+
10+
class PayloadArgumentResolver implements ArgumentValueResolverInterface
11+
{
12+
public function supports(Request $request, ArgumentMetadata $argument): bool
13+
{
14+
if ($argument->isVariadic()) {
15+
return false;
16+
}
17+
18+
$class = $argument->getType();
19+
20+
if (null === $class) {
21+
return false;
22+
}
23+
24+
if (null === $request->attributes->get('data')) {
25+
return false;
26+
}
27+
28+
$attributes = RequestAttributesExtractor::extractAttributes($request);
29+
30+
if ($attributes['resource_class'] === $class) {
31+
return true;
32+
}
33+
34+
return is_subclass_of($attributes['resource_class'], $class);
35+
}
36+
37+
public function resolve(Request $request, ArgumentMetadata $argument): \Generator
38+
{
39+
if (!$this->supports($request, $argument)) {
40+
throw new \InvalidArgumentException('Given request and argument not supported.');
41+
}
42+
43+
yield $request->attributes->get('data');
44+
}
45+
}

src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
use Doctrine\Common\Annotations\Annotation;
3636
use phpDocumentor\Reflection\DocBlockFactoryInterface;
3737
use Ramsey\Uuid\Uuid;
38+
use Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\ParamConverterInterface;
3839
use Symfony\Component\BrowserKit\AbstractBrowser;
3940
use Symfony\Component\Cache\Adapter\ArrayAdapter;
4041
use Symfony\Component\Config\FileLocator;
@@ -117,6 +118,7 @@ public function load(array $configs, ContainerBuilder $container): void
117118
$this->registerElasticsearchConfiguration($container, $config, $loader);
118119
$this->registerDataTransformerConfiguration($container);
119120
$this->registerSecurityConfiguration($container, $loader);
121+
$this->registerArgumentResolverConfiguration($container, $loader);
120122

121123
$container->registerForAutoconfiguration(DataPersisterInterface::class)
122124
->addTag('api_platform.data_persister');
@@ -632,4 +634,9 @@ private function registerSecurityConfiguration(ContainerBuilder $container, XmlF
632634
$loader->load('security.xml');
633635
}
634636
}
637+
638+
private function registerArgumentResolverConfiguration(ContainerBuilder $container, XmlFileLoader $loader): void
639+
{
640+
$loader->load('argument_resolver.xml');
641+
}
635642
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?xml version="1.0" ?>
2+
3+
<container xmlns="http://symfony.com/schema/dic/services"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5+
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
6+
7+
<services>
8+
<service id="api_platform.payload_argument_resolver" class="ApiPlatform\Core\Bridge\Symfony\Bundle\ArgumentResolver\PayloadArgumentResolver" public="false">
9+
<tag name="controller.argument_value_resolver" />
10+
</service>
11+
</services>
12+
13+
</container>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace ApiPlatform\Core\Tests\Bridge\Symfony\Bundle\ArgumentResolver;
4+
5+
class NotResource
6+
{
7+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
<?php
2+
3+
namespace ApiPlatform\Core\Tests\Bridge\Symfony\Bundle\ArgumentResolver;
4+
5+
use ApiPlatform\Core\Bridge\Symfony\Bundle\ArgumentResolver\PayloadArgumentResolver;
6+
use PHPUnit\Framework\TestCase;
7+
use Symfony\Component\HttpFoundation\Request;
8+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
9+
10+
/**
11+
* @covers \ApiPlatform\Core\Bridge\Symfony\Bundle\ArgumentResolver\PayloadArgumentResolver
12+
*/
13+
class PayloadArgumentResolverTest extends TestCase
14+
{
15+
public function testItSupportsRequestWithPayloadOfExpectedType(): void
16+
{
17+
$resolver = new PayloadArgumentResolver();
18+
$argument = $this->createArgumentMetadata(ResourceImplementation::class);
19+
$request = $this->createRequest();
20+
21+
$this->assertTrue($resolver->supports($request, $argument));
22+
}
23+
24+
public function testItSupportsRequestWithPayloadOfChildType(): void
25+
{
26+
$resolver = new PayloadArgumentResolver();
27+
$argument = $this->createArgumentMetadata(ResourceInterface::class);
28+
$request = $this->createRequest();
29+
30+
$this->assertTrue($resolver->supports($request, $argument));
31+
}
32+
33+
/**
34+
* @dataProvider provideUnsupportedArguments
35+
*/
36+
public function testItDoesNotSupportArgumentThatCannotBeResolved(ArgumentMetadata $argument): void
37+
{
38+
$resolver = new PayloadArgumentResolver();
39+
$request = $this->createRequest();
40+
41+
$this->assertFalse($resolver->supports($request, $argument));
42+
}
43+
44+
/**
45+
* @dataProvider provideUnsupportedRequests
46+
*/
47+
public function testItDoesNotSupportRequestWithoutPayloadOfExpectedType(Request $request): void
48+
{
49+
$resolver = new PayloadArgumentResolver();
50+
$argument = $this->createArgumentMetadata(ResourceInterface::class);
51+
52+
$this->assertFalse($resolver->supports($request, $argument));
53+
}
54+
55+
public function testItResolvesArgumentFromRequestWithDataOfExpectedType(): void
56+
{
57+
$resolver = new PayloadArgumentResolver();
58+
$argument = $this->createArgumentMetadata(ResourceImplementation::class);
59+
$request = $this->createRequest();
60+
61+
$this->assertSame(
62+
[$request->attributes->get('data')],
63+
iterator_to_array($resolver->resolve($request, $argument))
64+
);
65+
}
66+
67+
public function testItResolvesArgumentFromRequestWithDataOfChildType(): void
68+
{
69+
$resolver = new PayloadArgumentResolver();
70+
$argument = $this->createArgumentMetadata(ResourceInterface::class);
71+
$request = $this->createRequest();
72+
73+
$this->assertSame(
74+
[$request->attributes->get('data')],
75+
iterator_to_array($resolver->resolve($request, $argument))
76+
);
77+
}
78+
79+
/**
80+
* @dataProvider provideUnsupportedArguments
81+
*/
82+
public function testItDoesNotResolveForUnsupportedArgument(ArgumentMetadata $argument): void
83+
{
84+
$resolver = new PayloadArgumentResolver();
85+
$request = $this->createRequest();
86+
87+
$this->expectException(\InvalidArgumentException::class);
88+
89+
iterator_to_array($resolver->resolve($request, $argument));
90+
}
91+
92+
/**
93+
* @dataProvider provideUnsupportedRequests
94+
*/
95+
public function testItDoesNotResolveForUnsupportedRequest(Request $request): void
96+
{
97+
$resolver = new PayloadArgumentResolver();
98+
$argument = $this->createArgumentMetadata(ResourceInterface::class);
99+
100+
$this->expectException(\InvalidArgumentException::class);
101+
102+
iterator_to_array($resolver->resolve($request, $argument));
103+
}
104+
105+
public function provideUnsupportedRequests(): iterable
106+
{
107+
yield 'GET request' => [$this->createRequestWithoutData('GET')];
108+
109+
yield 'HEAD request' => [$this->createRequestWithoutData('HEAD')];
110+
111+
yield 'OPTIONS request' => [$this->createRequestWithoutData('OPTIONS')];
112+
113+
yield 'TRACE request' => [$this->createRequestWithoutData('TRACE')];
114+
115+
yield 'DELETE request' => [$this->createRequestWithoutData('DELETE')];
116+
117+
$requestWithoutAttributes = $this->createRequest();
118+
$requestWithoutAttributes->attributes->replace([]);
119+
yield 'request without attributes' => [$requestWithoutAttributes];
120+
121+
yield 'request with receive=false' => [$this->createRequestWithoutData('PUT', [
122+
'_api_receive' => false,
123+
])];
124+
125+
yield 'request on operation with deserialization disabled' => [$this->createRequestWithoutData('PUT', [
126+
'_api_item_operation_name' => 'update_no_deserialize',
127+
])];
128+
}
129+
130+
public function provideUnsupportedArguments(): iterable
131+
{
132+
yield 'argument without type declaration' => [$this->createArgumentMetadata()];
133+
yield 'variadic argument' => [$this->createArgumentMetadata(ResourceImplementation::class, true)];
134+
}
135+
136+
private function createRequest(string $method = 'PUT', array $attributes = []): Request
137+
{
138+
$request = Request::create('/foo', $method);
139+
$request->attributes->replace($attributes + [
140+
'_api_resource_class' => ResourceImplementation::class,
141+
'_api_receive' => true,
142+
'_api_item_operation_name' => 'update',
143+
'data' => new ResourceImplementation(),
144+
]);
145+
146+
return $request;
147+
}
148+
149+
private function createRequestWithoutData(string $method = 'PUT', array $attributes = []): Request
150+
{
151+
$request = $this->createRequest($method, $attributes);
152+
$request->attributes->remove('data');
153+
154+
return $request;
155+
}
156+
157+
private function createArgumentMetadata(?string $type = null, bool $isVariadic = false): ArgumentMetadata
158+
{
159+
return new ArgumentMetadata('foo', $type, $isVariadic, false, null);
160+
}
161+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace ApiPlatform\Core\Tests\Bridge\Symfony\Bundle\ArgumentResolver;
4+
5+
class ResourceImplementation implements ResourceInterface
6+
{
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace ApiPlatform\Core\Tests\Bridge\Symfony\Bundle\ArgumentResolver;
4+
5+
interface ResourceInterface
6+
{
7+
}

0 commit comments

Comments
 (0)