Skip to content

Commit 2a13100

Browse files
authored
feat(serializer): (un)set object-to-populate through denormalization context (#7124)
1 parent 073c0e2 commit 2a13100

File tree

5 files changed

+148
-13
lines changed

5 files changed

+148
-13
lines changed

src/State/Provider/DeserializeProvider.php

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,19 +75,24 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
7575
throw new UnsupportedMediaTypeHttpException('Format not supported.');
7676
}
7777

78-
$method = $operation->getMethod();
79-
80-
if (
81-
null !== $data
82-
&& (
83-
'POST' === $method
78+
if ($operation instanceof HttpOperation && null === ($serializerContext[SerializerContextBuilderInterface::ASSIGN_OBJECT_TO_POPULATE] ?? null)) {
79+
$method = $operation->getMethod();
80+
$assignObjectToPopulate = 'POST' === $method
8481
|| 'PATCH' === $method
85-
|| ('PUT' === $method && !($operation->getExtraProperties()['standard_put'] ?? true))
86-
)
87-
) {
82+
|| ('PUT' === $method && !($operation->getExtraProperties()['standard_put'] ?? true));
83+
84+
if ($assignObjectToPopulate) {
85+
$serializerContext[SerializerContextBuilderInterface::ASSIGN_OBJECT_TO_POPULATE] = true;
86+
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.', SerializerContextBuilderInterface::ASSIGN_OBJECT_TO_POPULATE);
87+
}
88+
}
89+
90+
if (null !== $data && ($serializerContext[SerializerContextBuilderInterface::ASSIGN_OBJECT_TO_POPULATE] ?? false)) {
8891
$serializerContext[AbstractNormalizer::OBJECT_TO_POPULATE] = $data;
8992
}
9093

94+
unset($serializerContext[SerializerContextBuilderInterface::ASSIGN_OBJECT_TO_POPULATE]);
95+
9196
try {
9297
return $this->serializer->deserialize((string) $request->getContent(), $serializerContext['deserializer_type'] ?? $operation->getClass(), $format, $serializerContext);
9398
} catch (PartialDenormalizationException $e) {

src/State/SerializerContextBuilderInterface.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
*/
2525
interface SerializerContextBuilderInterface
2626
{
27+
// @see ApiPlatform\Symfony\Controller\MainController and ApiPlatform\State\Provider\DeserializerProvider
28+
public const ASSIGN_OBJECT_TO_POPULATE = 'api_assign_object_to_populate';
29+
2730
/**
2831
* Creates a serialization context from a Request.
2932
*
@@ -51,6 +54,7 @@ interface SerializerContextBuilderInterface
5154
* api_included?: bool,
5255
* attributes?: string[],
5356
* deserializer_type?: string,
57+
* api_assign_object_to_populate?: bool,
5458
* }
5559
*/
5660
public function createFromRequest(Request $request, bool $normalization, ?array $extractedAttributes = null): array;

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 = [SerializerContextBuilderInterface::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 = [SerializerContextBuilderInterface::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\SerializerContextBuilderInterface;
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[SerializerContextBuilderInterface::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 + [SerializerContextBuilderInterface::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
@@ -16,6 +16,7 @@
1616
use ApiPlatform\Metadata\HttpOperation;
1717
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
1818
use ApiPlatform\State\ProviderInterface;
19+
use ApiPlatform\State\SerializerContextBuilderInterface;
1920
use ApiPlatform\State\Util\OperationRequestInitiatorTrait;
2021
use ApiPlatform\State\Util\RequestAttributesExtractor;
2122
use Symfony\Component\HttpKernel\Event\RequestEvent;
@@ -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[SerializerContextBuilderInterface::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 + [SerializerContextBuilderInterface::ASSIGN_OBJECT_TO_POPULATE => $assignObjectToPopulate]);
77+
}
78+
6879
if (!$operation->canDeserialize()) {
6980
return;
7081
}

0 commit comments

Comments
 (0)