Skip to content

Commit

Permalink
Merge 3.2
Browse files Browse the repository at this point in the history
  • Loading branch information
soyuka committed Jan 10, 2024
2 parents 40de9d1 + af8726a commit 456e74f
Show file tree
Hide file tree
Showing 9 changed files with 267 additions and 7 deletions.
1 change: 1 addition & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<server name="KERNEL_DIR" value="tests/Fixtures/app/" />
<server name="KERNEL_CLASS" value="AppKernel" />
<env name="APP_ENV" value="test" />
<env name="SYMFONY_PHPUNIT_REQUIRE" value="nikic/php-parser:^4.16"/>
</php>

<testsuites>
Expand Down
9 changes: 5 additions & 4 deletions src/Serializer/AbstractItemNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -630,9 +630,10 @@ protected function getFactoryOptions(array $context): array
$options['serializer_groups'] = (array) $context[self::GROUPS];
}

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

// This is a hot spot
Expand All @@ -645,7 +646,7 @@ protected function getFactoryOptions(array $context): array
}
}

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

/**
Expand Down
78 changes: 78 additions & 0 deletions src/Serializer/Tests/AbstractItemNormalizerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1545,6 +1545,84 @@ public function testDenormalizeObjectWithNullDisabledTypeEnforcement(): void
$this->assertInstanceOf(DtoWithNullValue::class, $actual);
$this->assertEquals(new DtoWithNullValue(), $actual);
}

public function testCacheKey(): void
{
$relatedDummy = new RelatedDummy();

$dummy = new Dummy();
$dummy->setName('foo');
$dummy->setAlias('ignored');
$dummy->setRelatedDummy($relatedDummy);
$dummy->relatedDummies->add(new RelatedDummy());

$relatedDummies = new ArrayCollection([$relatedDummy]);

$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
$propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::type('array'))->willReturn(new PropertyNameCollection(['name', 'alias', 'relatedDummy', 'relatedDummies']));

$relatedDummyType = new Type(Type::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class);
$relatedDummiesType = new Type(Type::BUILTIN_TYPE_OBJECT, false, ArrayCollection::class, true, new Type(Type::BUILTIN_TYPE_INT), $relatedDummyType);

$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
$propertyMetadataFactoryProphecy->create(Dummy::class, 'name', Argument::type('array'))->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true));
$propertyMetadataFactoryProphecy->create(Dummy::class, 'alias', Argument::type('array'))->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true));
$propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', Argument::type('array'))->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummyType])->withDescription('')->withReadable(true)->withWritable(false)->withReadableLink(false));
$propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', Argument::type('array'))->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummiesType])->withReadable(true)->withWritable(false)->withReadableLink(false));

$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
$iriConverterProphecy->getIriFromResource($dummy, Argument::cetera())->willReturn('/dummies/1');
$iriConverterProphecy->getIriFromResource($relatedDummy, Argument::cetera())->willReturn('/dummies/2');

$propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class);
$propertyAccessorProphecy->getValue($dummy, 'name')->willReturn('foo');
$propertyAccessorProphecy->getValue($dummy, 'relatedDummy')->willReturn($relatedDummy);
$propertyAccessorProphecy->getValue($dummy, 'relatedDummies')->willReturn($relatedDummies);

$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
$resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class);
$resourceClassResolverProphecy->getResourceClass($dummy, null)->willReturn(Dummy::class);
$resourceClassResolverProphecy->getResourceClass($relatedDummy, RelatedDummy::class)->willReturn(RelatedDummy::class);
$resourceClassResolverProphecy->getResourceClass($relatedDummies, RelatedDummy::class)->willReturn(RelatedDummy::class);
$resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true);
$resourceClassResolverProphecy->isResourceClass(RelatedDummy::class)->willReturn(true);

$serializerProphecy = $this->prophesize(SerializerInterface::class);
$serializerProphecy->willImplement(NormalizerInterface::class);
$serializerProphecy->normalize('foo', null, Argument::type('array'))->willReturn('foo');
$serializerProphecy->normalize(['/dummies/2'], null, Argument::type('array'))->willReturn(['/dummies/2']);

$normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [
$propertyNameCollectionFactoryProphecy->reveal(),
$propertyMetadataFactoryProphecy->reveal(),
$iriConverterProphecy->reveal(),
$resourceClassResolverProphecy->reveal(),
$propertyAccessorProphecy->reveal(),
null,
null,
[],
null,
null,
]);
$normalizer->setSerializer($serializerProphecy->reveal());

$expected = [
'name' => 'foo',
'relatedDummy' => '/dummies/2',
'relatedDummies' => ['/dummies/2'],
];
$this->assertSame($expected, $normalizer->normalize($dummy, null, [
'resources' => [],
'groups' => ['group'],
'ignored_attributes' => ['alias'],
'operation_name' => 'operation_name',
'root_operation_name' => 'root_operation_name',
]));

$operationCacheKey = (new \ReflectionClass($normalizer))->getProperty('localFactoryOptionsCache')->getValue($normalizer);
$this->assertEquals(array_keys($operationCacheKey), [sprintf('%s%s%s%s', Dummy::class, 'operation_name', 'root_operation_name', 'n')]);
$this->assertEquals(current($operationCacheKey), ['serializer_groups' => ['group']]);
}
}

class ObjectWithBasicProperties
Expand Down
3 changes: 2 additions & 1 deletion src/State/Provider/ContentNegotiationProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ private function flattenMimeTypes(array $formats): array
*/
private function getInputFormat(HttpOperation $operation, Request $request): ?string
{
if (null === ($contentType = $request->headers->get('CONTENT_TYPE'))) {
$contentType = $request->headers->get('CONTENT_TYPE');
if (null === $contentType || '' === $contentType) {
return null;
}

Expand Down
2 changes: 1 addition & 1 deletion src/State/Provider/DeserializeProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
}

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

Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/EventListener/DeserializeListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ private function getFormat(Request $request, array $formats): string
{
/** @var ?string $contentType */
$contentType = $request->headers->get('CONTENT_TYPE');
if (null === $contentType) {
if (null === $contentType || '' === $contentType) {
throw new UnsupportedMediaTypeHttpException('The "Content-Type" header must exist.');
}

Expand Down
63 changes: 63 additions & 0 deletions tests/State/Provider/ContentNegotiationProviderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Tests\State\Provider;

use ApiPlatform\Metadata\Post;
use ApiPlatform\State\Provider\ContentNegotiationProvider;
use ApiPlatform\State\ProviderInterface;
use Negotiation\Negotiator;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\HttpFoundation\Request;

class ContentNegotiationProviderTest extends TestCase
{
use ProphecyTrait;

public function testRequestWithEmptyContentType(): void
{
$expectedResult = new \stdClass();

$decorated = $this->prophesize(ProviderInterface::class);
$decorated->provide(Argument::cetera())->willReturn($expectedResult);

$negotiator = new Negotiator();
$formats = ['jsonld' => ['application/ld+json']];
$errorFormats = ['jsonld' => ['application/ld+json']];

$provider = new ContentNegotiationProvider($decorated->reveal(), $negotiator, $formats, $errorFormats);

// in Symfony (at least up to 7.0.2, 6.4.2, 6.3.11, 5.4.34), a request
// without a content-type and content-length header will result in the
// variables set to an empty string, not null

$request = new Request(
server: [
'REQUEST_METHOD' => 'POST',
'REQUEST_URI' => '/',
'CONTENT_TYPE' => '',
'CONTENT_LENGTH' => '',
],
content: ''
);

$operation = new Post();
$context = ['request' => $request];

$result = $provider->provide($operation, [], $context);

$this->assertSame($expectedResult, $result);
}
}
63 changes: 63 additions & 0 deletions tests/State/Provider/DeserializeProviderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace ApiPlatform\Tests\State\Provider;

use ApiPlatform\Metadata\Post;
use ApiPlatform\Serializer\SerializerContextBuilderInterface;
use ApiPlatform\State\Provider\DeserializeProvider;
use ApiPlatform\State\ProviderInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
use Symfony\Component\Serializer\SerializerInterface;

class DeserializeProviderTest extends TestCase
{
use ProphecyTrait;

public function testRequestWithEmptyContentType(): void
{
$expectedResult = new \stdClass();

$decorated = $this->prophesize(ProviderInterface::class);
$decorated->provide(Argument::cetera())->willReturn($expectedResult);

$serializer = $this->prophesize(SerializerInterface::class);
$serializerContextBuilder = $this->prophesize(SerializerContextBuilderInterface::class);

$provider = new DeserializeProvider($decorated->reveal(), $serializer->reveal(), $serializerContextBuilder->reveal());

// in Symfony (at least up to 7.0.2, 6.4.2, 6.3.11, 5.4.34), a request
// without a content-type and content-length header will result in the
// variables set to an empty string, not null

$request = new Request(
server: [
'REQUEST_METHOD' => 'POST',
'REQUEST_URI' => '/',
'CONTENT_TYPE' => '',
'CONTENT_LENGTH' => '',
],
content: ''
);

$operation = new Post(deserialize: true);
$context = ['request' => $request];

$this->expectException(UnsupportedMediaTypeHttpException::class);
$result = $provider->provide($operation, [], $context);
}
}
53 changes: 53 additions & 0 deletions tests/Symfony/EventListener/DeserializeListenerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
use Symfony\Component\Serializer\Exception\PartialDenormalizationException;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
Expand Down Expand Up @@ -369,4 +370,56 @@ public function testTurnPartialDenormalizationExceptionIntoValidationException()
$this->assertSame($violation->getCode(), 'ba785a8c-82cb-4283-967c-3cf342181b40');
}
}

public function testRequestWithEmptyContentType(): void
{
$serializerProphecy = $this->prophesize(SerializerInterface::class);
$serializerProphecy->deserialize(Argument::cetera())->shouldNotBeCalled();

$serializerContextBuilderProphecy = $this->prophesize(SerializerContextBuilderInterface::class);
$serializerContextBuilderProphecy->createFromRequest(Argument::cetera())->willReturn([]);

$resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class);
$resourceMetadataFactoryProphecy->create(Argument::cetera())->willReturn(new ResourceMetadataCollection(Dummy::class, [
new ApiResource(operations: [
'post' => new Post(inputFormats: self::FORMATS),
]),
]))->shouldBeCalled();

$listener = new DeserializeListener(
$serializerProphecy->reveal(),
$serializerContextBuilderProphecy->reveal(),
$resourceMetadataFactoryProphecy->reveal()
);

// in Symfony (at least up to 7.0.2, 6.4.2, 6.3.11, 5.4.34), a request
// without a content-type and content-length header will result in the
// variables set to an empty string, not null

$request = new Request(
server: [
'REQUEST_METHOD' => 'POST',
'REQUEST_URI' => '/',
'CONTENT_TYPE' => '',
'CONTENT_LENGTH' => '',
],
attributes: [
'_api_resource_class' => Dummy::class,
'_api_operation_name' => 'post',
'_api_receive' => true,
],
content: ''
);

$event = new RequestEvent(
$this->prophesize(HttpKernelInterface::class)->reveal(),
$request,
\defined(HttpKernelInterface::class.'::MAIN_REQUEST') ? HttpKernelInterface::MAIN_REQUEST : HttpKernelInterface::MASTER_REQUEST,
);

$this->expectException(UnsupportedMediaTypeHttpException::class);
$this->expectExceptionMessage('The "Content-Type" header must exist.');

$listener->onKernelRequest($event);
}
}

0 comments on commit 456e74f

Please sign in to comment.