Skip to content

Commit e366ae9

Browse files
committed
Add payload controller value resolver
1 parent eae59df commit e366ae9

File tree

12 files changed

+555
-11
lines changed

12 files changed

+555
-11
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
* IriConverter: Fix IRI url double encoding - may cause breaking change as some characters no longer encoded in output (#3552)
2020
* OpenAPI: **BC** Replace all characters other than `[a-zA-Z0-9\.\-_]` to `.` in definition names to be compliant with OpenAPI 3.0 (#3669)
2121
* Add stateless ApiResource attribute
22+
* Allow controller argument with a name different from `$data` thanks to an argument resolver
2223

2324
## 2.5.7
2425

phpstan.neon.dist

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ parameters:
4848
-
4949
message: '#Call to an undefined method Doctrine\\Persistence\\ObjectManager::getConnection\(\)#'
5050
path: src/Bridge/Doctrine/Common/Util/IdentifierManagerTrait.php
51+
-
52+
message: '#Call to function iterator_to_array\(\) on a separate line has no effect\.#'
53+
path: tests/Bridge/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolverTest.php
5154
# https://github.com/willdurand/Negotiation/issues/89#issuecomment-513283286
5255
-
5356
message: '#Call to an undefined method Negotiation\\AcceptHeader::getType\(\)\.#'
@@ -95,6 +98,9 @@ parameters:
9598
-
9699
message: '#Service "api_platform\.graphql\.fields_builder" is private\.#'
97100
path: src/GraphQl/Type/TypeBuilder.php
101+
-
102+
message: '#Service "argument_resolver" is private\.#'
103+
path: tests/Bridge/Symfony/Bundle/ArgumentResolver/PayloadArgumentResolverTest.php
98104
-
99105
message: '#Service "test" is not registered in the container\.#'
100106
path: tests/GraphQl/Type/TypesContainerTest.php
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Bridge\Symfony\Bundle\ArgumentResolver;
15+
16+
use ApiPlatform\Core\EventListener\ToggleableDeserializationTrait;
17+
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
18+
use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface;
19+
use ApiPlatform\Core\Util\RequestAttributesExtractor;
20+
use Symfony\Component\HttpFoundation\Request;
21+
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
22+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
23+
24+
class PayloadArgumentResolver implements ArgumentValueResolverInterface
25+
{
26+
use ToggleableDeserializationTrait;
27+
28+
/**
29+
* @var SerializerContextBuilderInterface
30+
*/
31+
private $serializationContextBuilder;
32+
33+
public function __construct(
34+
ResourceMetadataFactoryInterface $resourceMetadataFactory,
35+
SerializerContextBuilderInterface $serializationContextBuilder
36+
) {
37+
$this->resourceMetadataFactory = $resourceMetadataFactory;
38+
$this->serializationContextBuilder = $serializationContextBuilder;
39+
}
40+
41+
public function supports(Request $request, ArgumentMetadata $argument): bool
42+
{
43+
if ($argument->isVariadic()) {
44+
return false;
45+
}
46+
47+
$class = $argument->getType();
48+
49+
if (null === $class) {
50+
return false;
51+
}
52+
53+
if (null === $request->attributes->get('data')) {
54+
return false;
55+
}
56+
57+
$inputClass = $this->getExpectedInputClass($request);
58+
59+
if (null === $inputClass) {
60+
return false;
61+
}
62+
63+
return $inputClass === $class || is_subclass_of($inputClass, $class);
64+
}
65+
66+
public function resolve(Request $request, ArgumentMetadata $argument): \Generator
67+
{
68+
if (!$this->supports($request, $argument)) {
69+
throw new \InvalidArgumentException('Given request and argument not supported.');
70+
}
71+
72+
yield $request->attributes->get('data');
73+
}
74+
75+
private function getExpectedInputClass(Request $request): ?string
76+
{
77+
$attributes = RequestAttributesExtractor::extractAttributes($request);
78+
79+
if (!$this->isRequestToDeserialize($request, $attributes)) {
80+
return null;
81+
}
82+
83+
$context = $this->serializationContextBuilder->createFromRequest($request, false, $attributes);
84+
85+
return $context['input'] ?? $context['resource_class'];
86+
}
87+
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ public function load(array $configs, ContainerBuilder $container): void
124124
$this->registerElasticsearchConfiguration($container, $config, $loader);
125125
$this->registerDataTransformerConfiguration($container);
126126
$this->registerSecurityConfiguration($container, $loader);
127+
$this->registerArgumentResolverConfiguration($container, $loader);
127128

128129
$container->registerForAutoconfiguration(DataPersisterInterface::class)
129130
->addTag('api_platform.data_persister');
@@ -709,6 +710,11 @@ private function registerSecurityConfiguration(ContainerBuilder $container, XmlF
709710
}
710711
}
711712

713+
private function registerArgumentResolverConfiguration(ContainerBuilder $container, XmlFileLoader $loader): void
714+
{
715+
$loader->load('argument_resolver.xml');
716+
}
717+
712718
private function buildDeprecationArgs(string $version, string $message): array
713719
{
714720
return method_exists(Definition::class, 'getDeprecation')
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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+
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
10+
<argument type="service" id="api_platform.serializer.context_builder" />
11+
12+
<tag name="controller.argument_value_resolver" />
13+
</service>
14+
</services>
15+
16+
</container>

src/EventListener/DeserializeListener.php

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
use ApiPlatform\Core\Api\FormatMatcher;
1717
use ApiPlatform\Core\Api\FormatsProviderInterface;
1818
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
19-
use ApiPlatform\Core\Metadata\Resource\ToggleableOperationAttributeTrait;
2019
use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface;
2120
use ApiPlatform\Core\Util\RequestAttributesExtractor;
2221
use Symfony\Component\HttpFoundation\Request;
@@ -32,7 +31,7 @@
3231
*/
3332
final class DeserializeListener
3433
{
35-
use ToggleableOperationAttributeTrait;
34+
use ToggleableDeserializationTrait;
3635

3736
public const OPERATION_ATTRIBUTE_KEY = 'deserialize';
3837

@@ -69,15 +68,9 @@ public function __construct(SerializerInterface $serializer, SerializerContextBu
6968
public function onKernelRequest(RequestEvent $event): void
7069
{
7170
$request = $event->getRequest();
72-
$method = $request->getMethod();
73-
74-
if (
75-
'DELETE' === $method
76-
|| $request->isMethodSafe()
77-
|| !($attributes = RequestAttributesExtractor::extractAttributes($request))
78-
|| !$attributes['receive']
79-
|| $this->isOperationAttributeDisabled($attributes, self::OPERATION_ATTRIBUTE_KEY)
80-
) {
71+
$attributes = RequestAttributesExtractor::extractAttributes($request);
72+
73+
if (!$this->isRequestToDeserialize($request, $attributes)) {
8174
return;
8275
}
8376

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\EventListener;
15+
16+
use ApiPlatform\Core\Metadata\Resource\ToggleableOperationAttributeTrait;
17+
use Symfony\Component\HttpFoundation\Request;
18+
19+
trait ToggleableDeserializationTrait
20+
{
21+
use ToggleableOperationAttributeTrait;
22+
23+
private function isRequestToDeserialize(Request $request, array $attributes): bool
24+
{
25+
return
26+
'DELETE' !== $request->getMethod()
27+
&& !$request->isMethodSafe()
28+
&& [] !== $attributes
29+
&& $attributes['receive']
30+
&& !$this->isOperationAttributeDisabled($attributes, DeserializeListener::OPERATION_ATTRIBUTE_KEY);
31+
}
32+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Core\Tests\Bridge\Symfony\Bundle\ArgumentResolver;
15+
16+
class NotResource
17+
{
18+
}

0 commit comments

Comments
 (0)