Skip to content

Commit d02b68b

Browse files
lyrixxsoyuka
authored andcommitted
feat(serializer): set the object-to-populate in deserializer context as soon as data !== null
1 parent aa20430 commit d02b68b

File tree

4 files changed

+147
-13
lines changed

4 files changed

+147
-13
lines changed

src/State/Provider/DeserializeProvider.php

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232

3333
final class DeserializeProvider implements ProviderInterface
3434
{
35+
// @see ApiPlatform\Symfony\Controller\MainController
36+
public const ASSIGN_OBJECT_TO_POPULATE = '_api_assign_object_to_populate';
37+
3538
public function __construct(
3639
private readonly ?ProviderInterface $decorated,
3740
private readonly SerializerInterface $serializer,
@@ -75,19 +78,24 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
7578
throw new UnsupportedMediaTypeHttpException('Format not supported.');
7679
}
7780

78-
$method = $operation->getMethod();
79-
80-
if (
81-
null !== $data
82-
&& (
83-
'POST' === $method
81+
if ($operation instanceof HttpOperation && null === ($serializerContext[self::ASSIGN_OBJECT_TO_POPULATE] ?? null)) {
82+
$method = $operation->getMethod();
83+
$assignObjectToPopulate = 'POST' === $method
8484
|| 'PATCH' === $method
85-
|| ('PUT' === $method && !($operation->getExtraProperties()['standard_put'] ?? true))
86-
)
87-
) {
85+
|| ('PUT' === $method && !($operation->getExtraProperties()['standard_put'] ?? true));
86+
87+
if ($assignObjectToPopulate) {
88+
$serializerContext[self::ASSIGN_OBJECT_TO_POPULATE] = true;
89+
trigger_deprecation('api-platform/core', '5.0', 'To assign an object to populate you should set "%s" in your denormalizationContext, not defining it is deprecated.', self::ASSIGN_OBJECT_TO_POPULATE);
90+
}
91+
}
92+
93+
if (null !== $data && ($serializerContext[self::ASSIGN_OBJECT_TO_POPULATE] ?? false)) {
8894
$serializerContext[AbstractNormalizer::OBJECT_TO_POPULATE] = $data;
8995
}
9096

97+
unset($serializerContext[self::ASSIGN_OBJECT_TO_POPULATE]);
98+
9199
try {
92100
return $this->serializer->deserialize((string) $request->getContent(), $serializerContext['deserializer_type'] ?? $operation->getClass(), $format, $serializerContext);
93101
} catch (PartialDenormalizationException $e) {

src/State/Tests/Provider/DeserializeProviderTest.php

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,15 @@
1414
namespace ApiPlatform\State\Tests\Provider;
1515

1616
use ApiPlatform\Metadata\Get;
17+
use ApiPlatform\Metadata\HttpOperation;
18+
use ApiPlatform\Metadata\Patch;
1719
use ApiPlatform\Metadata\Post;
20+
use ApiPlatform\Metadata\Put;
1821
use ApiPlatform\State\Provider\DeserializeProvider;
1922
use ApiPlatform\State\ProviderInterface;
2023
use ApiPlatform\State\SerializerContextBuilderInterface;
24+
use PHPUnit\Framework\Attributes\DataProvider;
25+
use PHPUnit\Framework\Attributes\IgnoreDeprecations;
2126
use PHPUnit\Framework\TestCase;
2227
use Symfony\Component\HttpFoundation\Request;
2328
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
@@ -26,8 +31,10 @@
2631

2732
class DeserializeProviderTest extends TestCase
2833
{
34+
#[IgnoreDeprecations]
2935
public function testDeserialize(): void
3036
{
37+
$this->expectUserDeprecationMessage('Since api-platform/core 5.0: To assign an object to populate you should set "_api_assign_object_to_populate" in your denormalizationContext not defining it is deprecated.');
3138
$objectToPopulate = new \stdClass();
3239
$serializerContext = [];
3340
$operation = new Post(deserialize: true, class: 'Test');
@@ -128,4 +135,101 @@ public function testRequestWithEmptyContentType(): void
128135
$this->expectException(UnsupportedMediaTypeHttpException::class);
129136
$provider->provide($operation, [], $context);
130137
}
138+
139+
#[DataProvider('provideMethodsTriggeringDeprecation')]
140+
#[IgnoreDeprecations]
141+
public function testDeserializeTriggersDeprecationWhenContextNotSet(HttpOperation $operation): void
142+
{
143+
$this->expectUserDeprecationMessage('Since api-platform/core 5.0: To assign an object to populate you should set "_api_assign_object_to_populate" in your denormalizationContext not defining it is deprecated.');
144+
145+
$objectToPopulate = new \stdClass();
146+
$serializerContext = [];
147+
$decorated = $this->createStub(ProviderInterface::class);
148+
$decorated->method('provide')->willReturn($objectToPopulate);
149+
150+
$serializerContextBuilder = $this->createMock(SerializerContextBuilderInterface::class);
151+
$serializerContextBuilder->method('createFromRequest')->willReturn($serializerContext);
152+
153+
$serializer = $this->createMock(SerializerInterface::class);
154+
$serializer->expects($this->once())->method('deserialize')->with(
155+
'test',
156+
'Test',
157+
'format',
158+
['uri_variables' => ['id' => 1], 'object_to_populate' => $objectToPopulate] + $serializerContext
159+
)->willReturn(new \stdClass());
160+
161+
$provider = new DeserializeProvider($decorated, $serializer, $serializerContextBuilder);
162+
$request = new Request(content: 'test');
163+
$request->headers->set('CONTENT_TYPE', 'ok');
164+
$request->attributes->set('input_format', 'format');
165+
$provider->provide($operation, ['id' => 1], ['request' => $request]);
166+
}
167+
168+
public static function provideMethodsTriggeringDeprecation(): iterable
169+
{
170+
yield 'POST method' => [new Post(deserialize: true, class: 'Test')];
171+
yield 'PATCH method' => [new Patch(deserialize: true, class: 'Test')];
172+
yield 'PUT method (non-standard)' => [new Put(deserialize: true, class: 'Test', extraProperties: ['standard_put' => false])];
173+
}
174+
175+
public function testDeserializeSetsObjectToPopulateWhenContextIsTrue(): void
176+
{
177+
$objectToPopulate = new \stdClass();
178+
$serializerContext = [DeserializeProvider::ASSIGN_OBJECT_TO_POPULATE => true];
179+
$operation = new Post(deserialize: true, class: 'Test');
180+
$decorated = $this->createStub(ProviderInterface::class);
181+
$decorated->method('provide')->willReturn($objectToPopulate);
182+
183+
$serializerContextBuilder = $this->createMock(SerializerContextBuilderInterface::class);
184+
$serializerContextBuilder->method('createFromRequest')->willReturn($serializerContext);
185+
186+
$serializer = $this->createMock(SerializerInterface::class);
187+
$serializer->expects($this->once())->method('deserialize')->with(
188+
'test',
189+
'Test',
190+
'format',
191+
$this->callback(function (array $context) use ($objectToPopulate) {
192+
$this->assertArrayHasKey(AbstractNormalizer::OBJECT_TO_POPULATE, $context);
193+
$this->assertSame($objectToPopulate, $context[AbstractNormalizer::OBJECT_TO_POPULATE]);
194+
195+
return true;
196+
})
197+
)->willReturn(new \stdClass());
198+
199+
$provider = new DeserializeProvider($decorated, $serializer, $serializerContextBuilder);
200+
$request = new Request(content: 'test');
201+
$request->headers->set('CONTENT_TYPE', 'ok');
202+
$request->attributes->set('input_format', 'format');
203+
$provider->provide($operation, ['id' => 1], ['request' => $request]);
204+
}
205+
206+
public function testDeserializeDoesNotSetObjectToPopulateWhenContextIsFalse(): void
207+
{
208+
$objectToPopulate = new \stdClass();
209+
$serializerContext = [DeserializeProvider::ASSIGN_OBJECT_TO_POPULATE => false];
210+
$operation = new Post(deserialize: true, class: 'Test');
211+
$decorated = $this->createStub(ProviderInterface::class);
212+
$decorated->method('provide')->willReturn($objectToPopulate);
213+
214+
$serializerContextBuilder = $this->createMock(SerializerContextBuilderInterface::class);
215+
$serializerContextBuilder->method('createFromRequest')->willReturn($serializerContext);
216+
217+
$serializer = $this->createMock(SerializerInterface::class);
218+
$serializer->expects($this->once())->method('deserialize')->with(
219+
'test',
220+
'Test',
221+
'format',
222+
$this->callback(function (array $context) {
223+
$this->assertArrayNotHasKey(AbstractNormalizer::OBJECT_TO_POPULATE, $context);
224+
225+
return true;
226+
})
227+
)->willReturn(new \stdClass());
228+
229+
$provider = new DeserializeProvider($decorated, $serializer, $serializerContextBuilder);
230+
$request = new Request(content: 'test');
231+
$request->headers->set('CONTENT_TYPE', 'ok');
232+
$request->attributes->set('input_format', 'format');
233+
$provider->provide($operation, ['id' => 1], ['request' => $request]);
234+
}
131235
}

src/Symfony/Controller/MainController.php

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use ApiPlatform\Metadata\UriVariablesConverterInterface;
2222
use ApiPlatform\State\ProcessorInterface;
2323
use ApiPlatform\State\ProviderInterface;
24+
use ApiPlatform\State\Provider\DeserializeProvider;
2425
use ApiPlatform\State\UriVariablesResolverTrait;
2526
use ApiPlatform\State\Util\OperationRequestInitiatorTrait;
2627
use Psr\Log\LoggerInterface;
@@ -48,8 +49,8 @@ public function __invoke(Request $request): Response
4849
{
4950
$operation = $this->initializeOperation($request);
5051

51-
if (!$operation) {
52-
throw new RuntimeException('Not an API operation.');
52+
if (!$operation || !$operation instanceof HttpOperation) {
53+
throw new RuntimeException('Not an HTTP API operation.');
5354
}
5455

5556
$uriVariables = [];
@@ -72,14 +73,24 @@ public function __invoke(Request $request): Response
7273
$operation = $operation->withValidate(!$request->isMethodSafe() && !$request->isMethod('DELETE'));
7374
}
7475

75-
if (null === $operation->canRead() && $operation instanceof HttpOperation) {
76+
if (null === $operation->canRead()) {
7677
$operation = $operation->withRead($operation->getUriVariables() || $request->isMethodSafe());
7778
}
7879

79-
if (null === $operation->canDeserialize() && $operation instanceof HttpOperation) {
80+
if (null === $operation->canDeserialize()) {
8081
$operation = $operation->withDeserialize(\in_array($operation->getMethod(), ['POST', 'PUT', 'PATCH'], true));
8182
}
8283

84+
$denormalizationContext = $operation->getDenormalizationContext() ?? [];
85+
if ($operation->canDeserialize() && !isset($denormalizationContext[DeserializeProvider::ASSIGN_OBJECT_TO_POPULATE])) {
86+
$method = $operation->getMethod();
87+
$assignObjectToPopulate = 'POST' === $method
88+
|| 'PATCH' === $method
89+
|| ('PUT' === $method && !($operation->getExtraProperties()['standard_put'] ?? true));
90+
91+
$operation = $operation->withDenormalizationContext($denormalizationContext + [DeserializeProvider::ASSIGN_OBJECT_TO_POPULATE => $assignObjectToPopulate]);
92+
}
93+
8394
$body = $this->provider->provide($operation, $uriVariables, $context);
8495

8596
// The provider can change the Operation, extract it again from the Request attributes

src/Symfony/EventListener/DeserializeListener.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
use ApiPlatform\Metadata\HttpOperation;
1717
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
18+
use ApiPlatform\State\Provider\DeserializeProvider;
1819
use ApiPlatform\State\ProviderInterface;
1920
use ApiPlatform\State\Util\OperationRequestInitiatorTrait;
2021
use ApiPlatform\State\Util\RequestAttributesExtractor;
@@ -65,6 +66,16 @@ public function onKernelRequest(RequestEvent $event): void
6566
$operation = $operation->withDeserialize(\in_array($method, ['POST', 'PUT', 'PATCH'], true));
6667
}
6768

69+
$denormalizationContext = $operation->getDenormalizationContext() ?? [];
70+
if ($operation->canDeserialize() && !isset($denormalizationContext[DeserializeProvider::ASSIGN_OBJECT_TO_POPULATE])) {
71+
$method = $operation->getMethod();
72+
$assignObjectToPopulate = 'POST' === $method
73+
|| 'PATCH' === $method
74+
|| ('PUT' === $method && !($operation->getExtraProperties()['standard_put'] ?? true));
75+
76+
$operation = $operation->withDenormalizationContext($denormalizationContext + [DeserializeProvider::ASSIGN_OBJECT_TO_POPULATE => $assignObjectToPopulate]);
77+
}
78+
6879
if (!$operation->canDeserialize()) {
6980
return;
7081
}

0 commit comments

Comments
 (0)