Skip to content

Commit 456e74f

Browse files
committed
Merge 3.2
2 parents 40de9d1 + af8726a commit 456e74f

File tree

9 files changed

+267
-7
lines changed

9 files changed

+267
-7
lines changed

phpunit.xml.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
<server name="KERNEL_DIR" value="tests/Fixtures/app/" />
1212
<server name="KERNEL_CLASS" value="AppKernel" />
1313
<env name="APP_ENV" value="test" />
14+
<env name="SYMFONY_PHPUNIT_REQUIRE" value="nikic/php-parser:^4.16"/>
1415
</php>
1516

1617
<testsuites>

src/Serializer/AbstractItemNormalizer.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -630,9 +630,10 @@ protected function getFactoryOptions(array $context): array
630630
$options['serializer_groups'] = (array) $context[self::GROUPS];
631631
}
632632

633-
$operationCacheKey = ($context['resource_class'] ?? '').($context['operation_name'] ?? '').($context['api_normalize'] ?? '');
634-
if ($operationCacheKey && isset($this->localFactoryOptionsCache[$operationCacheKey])) {
635-
return $options + $this->localFactoryOptionsCache[$operationCacheKey];
633+
$operationCacheKey = ($context['resource_class'] ?? '').($context['operation_name'] ?? '').($context['root_operation_name'] ?? '');
634+
$suffix = ($context['api_normalize'] ?? '') ? 'n' : '';
635+
if ($operationCacheKey && isset($this->localFactoryOptionsCache[$operationCacheKey.$suffix])) {
636+
return $options + $this->localFactoryOptionsCache[$operationCacheKey.$suffix];
636637
}
637638

638639
// This is a hot spot
@@ -645,7 +646,7 @@ protected function getFactoryOptions(array $context): array
645646
}
646647
}
647648

648-
return $options + $this->localFactoryOptionsCache[$operationCacheKey] = $options;
649+
return $options + $this->localFactoryOptionsCache[$operationCacheKey.$suffix] = $options;
649650
}
650651

651652
/**

src/Serializer/Tests/AbstractItemNormalizerTest.php

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1545,6 +1545,84 @@ public function testDenormalizeObjectWithNullDisabledTypeEnforcement(): void
15451545
$this->assertInstanceOf(DtoWithNullValue::class, $actual);
15461546
$this->assertEquals(new DtoWithNullValue(), $actual);
15471547
}
1548+
1549+
public function testCacheKey(): void
1550+
{
1551+
$relatedDummy = new RelatedDummy();
1552+
1553+
$dummy = new Dummy();
1554+
$dummy->setName('foo');
1555+
$dummy->setAlias('ignored');
1556+
$dummy->setRelatedDummy($relatedDummy);
1557+
$dummy->relatedDummies->add(new RelatedDummy());
1558+
1559+
$relatedDummies = new ArrayCollection([$relatedDummy]);
1560+
1561+
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
1562+
$propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::type('array'))->willReturn(new PropertyNameCollection(['name', 'alias', 'relatedDummy', 'relatedDummies']));
1563+
1564+
$relatedDummyType = new Type(Type::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class);
1565+
$relatedDummiesType = new Type(Type::BUILTIN_TYPE_OBJECT, false, ArrayCollection::class, true, new Type(Type::BUILTIN_TYPE_INT), $relatedDummyType);
1566+
1567+
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
1568+
$propertyMetadataFactoryProphecy->create(Dummy::class, 'name', Argument::type('array'))->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true));
1569+
$propertyMetadataFactoryProphecy->create(Dummy::class, 'alias', Argument::type('array'))->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true));
1570+
$propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', Argument::type('array'))->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummyType])->withDescription('')->withReadable(true)->withWritable(false)->withReadableLink(false));
1571+
$propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', Argument::type('array'))->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummiesType])->withReadable(true)->withWritable(false)->withReadableLink(false));
1572+
1573+
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
1574+
$iriConverterProphecy->getIriFromResource($dummy, Argument::cetera())->willReturn('/dummies/1');
1575+
$iriConverterProphecy->getIriFromResource($relatedDummy, Argument::cetera())->willReturn('/dummies/2');
1576+
1577+
$propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class);
1578+
$propertyAccessorProphecy->getValue($dummy, 'name')->willReturn('foo');
1579+
$propertyAccessorProphecy->getValue($dummy, 'relatedDummy')->willReturn($relatedDummy);
1580+
$propertyAccessorProphecy->getValue($dummy, 'relatedDummies')->willReturn($relatedDummies);
1581+
1582+
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
1583+
$resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class);
1584+
$resourceClassResolverProphecy->getResourceClass($dummy, null)->willReturn(Dummy::class);
1585+
$resourceClassResolverProphecy->getResourceClass($relatedDummy, RelatedDummy::class)->willReturn(RelatedDummy::class);
1586+
$resourceClassResolverProphecy->getResourceClass($relatedDummies, RelatedDummy::class)->willReturn(RelatedDummy::class);
1587+
$resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true);
1588+
$resourceClassResolverProphecy->isResourceClass(RelatedDummy::class)->willReturn(true);
1589+
1590+
$serializerProphecy = $this->prophesize(SerializerInterface::class);
1591+
$serializerProphecy->willImplement(NormalizerInterface::class);
1592+
$serializerProphecy->normalize('foo', null, Argument::type('array'))->willReturn('foo');
1593+
$serializerProphecy->normalize(['/dummies/2'], null, Argument::type('array'))->willReturn(['/dummies/2']);
1594+
1595+
$normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [
1596+
$propertyNameCollectionFactoryProphecy->reveal(),
1597+
$propertyMetadataFactoryProphecy->reveal(),
1598+
$iriConverterProphecy->reveal(),
1599+
$resourceClassResolverProphecy->reveal(),
1600+
$propertyAccessorProphecy->reveal(),
1601+
null,
1602+
null,
1603+
[],
1604+
null,
1605+
null,
1606+
]);
1607+
$normalizer->setSerializer($serializerProphecy->reveal());
1608+
1609+
$expected = [
1610+
'name' => 'foo',
1611+
'relatedDummy' => '/dummies/2',
1612+
'relatedDummies' => ['/dummies/2'],
1613+
];
1614+
$this->assertSame($expected, $normalizer->normalize($dummy, null, [
1615+
'resources' => [],
1616+
'groups' => ['group'],
1617+
'ignored_attributes' => ['alias'],
1618+
'operation_name' => 'operation_name',
1619+
'root_operation_name' => 'root_operation_name',
1620+
]));
1621+
1622+
$operationCacheKey = (new \ReflectionClass($normalizer))->getProperty('localFactoryOptionsCache')->getValue($normalizer);
1623+
$this->assertEquals(array_keys($operationCacheKey), [sprintf('%s%s%s%s', Dummy::class, 'operation_name', 'root_operation_name', 'n')]);
1624+
$this->assertEquals(current($operationCacheKey), ['serializer_groups' => ['group']]);
1625+
}
15481626
}
15491627

15501628
class ObjectWithBasicProperties

src/State/Provider/ContentNegotiationProvider.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ private function flattenMimeTypes(array $formats): array
9797
*/
9898
private function getInputFormat(HttpOperation $operation, Request $request): ?string
9999
{
100-
if (null === ($contentType = $request->headers->get('CONTENT_TYPE'))) {
100+
$contentType = $request->headers->get('CONTENT_TYPE');
101+
if (null === $contentType || '' === $contentType) {
101102
return null;
102103
}
103104

src/State/Provider/DeserializeProvider.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
5656
}
5757

5858
$contentType = $request->headers->get('CONTENT_TYPE');
59-
if (null === $contentType) {
59+
if (null === $contentType || '' === $contentType) {
6060
throw new UnsupportedMediaTypeHttpException('The "Content-Type" header must exist.');
6161
}
6262

src/Symfony/EventListener/DeserializeListener.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ private function getFormat(Request $request, array $formats): string
132132
{
133133
/** @var ?string $contentType */
134134
$contentType = $request->headers->get('CONTENT_TYPE');
135-
if (null === $contentType) {
135+
if (null === $contentType || '' === $contentType) {
136136
throw new UnsupportedMediaTypeHttpException('The "Content-Type" header must exist.');
137137
}
138138

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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\Tests\State\Provider;
15+
16+
use ApiPlatform\Metadata\Post;
17+
use ApiPlatform\State\Provider\ContentNegotiationProvider;
18+
use ApiPlatform\State\ProviderInterface;
19+
use Negotiation\Negotiator;
20+
use PHPUnit\Framework\TestCase;
21+
use Prophecy\Argument;
22+
use Prophecy\PhpUnit\ProphecyTrait;
23+
use Symfony\Component\HttpFoundation\Request;
24+
25+
class ContentNegotiationProviderTest extends TestCase
26+
{
27+
use ProphecyTrait;
28+
29+
public function testRequestWithEmptyContentType(): void
30+
{
31+
$expectedResult = new \stdClass();
32+
33+
$decorated = $this->prophesize(ProviderInterface::class);
34+
$decorated->provide(Argument::cetera())->willReturn($expectedResult);
35+
36+
$negotiator = new Negotiator();
37+
$formats = ['jsonld' => ['application/ld+json']];
38+
$errorFormats = ['jsonld' => ['application/ld+json']];
39+
40+
$provider = new ContentNegotiationProvider($decorated->reveal(), $negotiator, $formats, $errorFormats);
41+
42+
// in Symfony (at least up to 7.0.2, 6.4.2, 6.3.11, 5.4.34), a request
43+
// without a content-type and content-length header will result in the
44+
// variables set to an empty string, not null
45+
46+
$request = new Request(
47+
server: [
48+
'REQUEST_METHOD' => 'POST',
49+
'REQUEST_URI' => '/',
50+
'CONTENT_TYPE' => '',
51+
'CONTENT_LENGTH' => '',
52+
],
53+
content: ''
54+
);
55+
56+
$operation = new Post();
57+
$context = ['request' => $request];
58+
59+
$result = $provider->provide($operation, [], $context);
60+
61+
$this->assertSame($expectedResult, $result);
62+
}
63+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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\Tests\State\Provider;
15+
16+
use ApiPlatform\Metadata\Post;
17+
use ApiPlatform\Serializer\SerializerContextBuilderInterface;
18+
use ApiPlatform\State\Provider\DeserializeProvider;
19+
use ApiPlatform\State\ProviderInterface;
20+
use PHPUnit\Framework\TestCase;
21+
use Prophecy\Argument;
22+
use Prophecy\PhpUnit\ProphecyTrait;
23+
use Symfony\Component\HttpFoundation\Request;
24+
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
25+
use Symfony\Component\Serializer\SerializerInterface;
26+
27+
class DeserializeProviderTest extends TestCase
28+
{
29+
use ProphecyTrait;
30+
31+
public function testRequestWithEmptyContentType(): void
32+
{
33+
$expectedResult = new \stdClass();
34+
35+
$decorated = $this->prophesize(ProviderInterface::class);
36+
$decorated->provide(Argument::cetera())->willReturn($expectedResult);
37+
38+
$serializer = $this->prophesize(SerializerInterface::class);
39+
$serializerContextBuilder = $this->prophesize(SerializerContextBuilderInterface::class);
40+
41+
$provider = new DeserializeProvider($decorated->reveal(), $serializer->reveal(), $serializerContextBuilder->reveal());
42+
43+
// in Symfony (at least up to 7.0.2, 6.4.2, 6.3.11, 5.4.34), a request
44+
// without a content-type and content-length header will result in the
45+
// variables set to an empty string, not null
46+
47+
$request = new Request(
48+
server: [
49+
'REQUEST_METHOD' => 'POST',
50+
'REQUEST_URI' => '/',
51+
'CONTENT_TYPE' => '',
52+
'CONTENT_LENGTH' => '',
53+
],
54+
content: ''
55+
);
56+
57+
$operation = new Post(deserialize: true);
58+
$context = ['request' => $request];
59+
60+
$this->expectException(UnsupportedMediaTypeHttpException::class);
61+
$result = $provider->provide($operation, [], $context);
62+
}
63+
}

tests/Symfony/EventListener/DeserializeListenerTest.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
use Symfony\Component\HttpFoundation\Request;
2929
use Symfony\Component\HttpKernel\Event\RequestEvent;
3030
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
31+
use Symfony\Component\HttpKernel\HttpKernelInterface;
3132
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
3233
use Symfony\Component\Serializer\Exception\PartialDenormalizationException;
3334
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
@@ -369,4 +370,56 @@ public function testTurnPartialDenormalizationExceptionIntoValidationException()
369370
$this->assertSame($violation->getCode(), 'ba785a8c-82cb-4283-967c-3cf342181b40');
370371
}
371372
}
373+
374+
public function testRequestWithEmptyContentType(): void
375+
{
376+
$serializerProphecy = $this->prophesize(SerializerInterface::class);
377+
$serializerProphecy->deserialize(Argument::cetera())->shouldNotBeCalled();
378+
379+
$serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class);
380+
$serializerContextBuilderProphecy->createFromRequest(Argument::cetera())->willReturn([]);
381+
382+
$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class);
383+
$resourceMetadataFactoryProphecy->create(Argument::cetera())->willReturn(new ResourceMetadataCollection(Dummy::class, [
384+
new ApiResource(operations: [
385+
'post' => new Post(inputFormats: self::FORMATS),
386+
]),
387+
]))->shouldBeCalled();
388+
389+
$listener = new DeserializeListener(
390+
$serializerProphecy->reveal(),
391+
$serializerContextBuilderProphecy->reveal(),
392+
$resourceMetadataFactoryProphecy->reveal()
393+
);
394+
395+
// in Symfony (at least up to 7.0.2, 6.4.2, 6.3.11, 5.4.34), a request
396+
// without a content-type and content-length header will result in the
397+
// variables set to an empty string, not null
398+
399+
$request = new Request(
400+
server: [
401+
'REQUEST_METHOD' => 'POST',
402+
'REQUEST_URI' => '/',
403+
'CONTENT_TYPE' => '',
404+
'CONTENT_LENGTH' => '',
405+
],
406+
attributes: [
407+
'_api_resource_class' => Dummy::class,
408+
'_api_operation_name' => 'post',
409+
'_api_receive' => true,
410+
],
411+
content: ''
412+
);
413+
414+
$event = new RequestEvent(
415+
$this->prophesize(HttpKernelInterface::class)->reveal(),
416+
$request,
417+
\defined(HttpKernelInterface::class.'::MAIN_REQUEST') ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::MASTER_REQUEST,
418+
);
419+
420+
$this->expectException(UnsupportedMediaTypeHttpException::class);
421+
$this->expectExceptionMessage('The "Content-Type" header must exist.');
422+
423+
$listener->onKernelRequest($event);
424+
}
372425
}

0 commit comments

Comments
 (0)