diff --git a/CHANGELOG.md b/CHANGELOG.md
index 900ea88885b..0da49629e96 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -187,6 +187,20 @@ Notes:
* [0d5f35683](https://github.com/api-platform/core/commit/0d5f356839eb6aa9f536044abe4affa736553e76) feat(laravel): laravel component (#5882)
+## v3.4.6
+
+### Bug fixes
+
+* [17c916c3a](https://github.com/api-platform/core/commit/17c916c3a1bcc837c9bc842dc48390dbeb043450) fix(symfony): service typo fix BackedEnumProvider for autowiring (#6769)
+* [216d9ccaa](https://github.com/api-platform/core/commit/216d9ccaacf7845daaaeab30f3a58bb5567430fe) fix(serializer): fetch type on normalization error when possible (#6761)
+* [2f967d934](https://github.com/api-platform/core/commit/2f967d9345004779f409b9ce1b5d0cbba84c7132) fix(doctrine): throw an exception when a filter is not found in a parameter (#6767)
+* [736ca045e](https://github.com/api-platform/core/commit/736ca045e6832f04aaa002ddd7b85c55df4696bb) fix(validator): allow to pass both a ConstraintViolationList and a previous exception (#6762)
+* [a98332d99](https://github.com/api-platform/core/commit/a98332d99a43338fa3bc0fd6b20f82ac58d1c397) fix(metadata): name convert parameter property (#6766)
+* [aa1667de1](https://github.com/api-platform/core/commit/aa1667de116fa9a40842f1480fc90ab49c7c2784) fix(state): empty result when the array paginator is out of bound (#6785)
+* [ab88353a3](https://github.com/api-platform/core/commit/ab88353a32f94146b01c34bae377ec5a735846db) fix(hal): detecting and handling circular reference (#6752)
+* [bba030614](https://github.com/api-platform/core/commit/bba030614b96887fea4f5c177e3137378ccae8a5) fix: properly support phpstan/phpdoc-parser 2 (#6789)
+* [bec147b91](https://github.com/api-platform/core/commit/bec147b916c29e346a698b28ddd4493bf305d9a0) fix(state): do not check content type if no input (#6794)
+
## v3.4.5
### Bug fixes
diff --git a/composer.json b/composer.json
index 39314dc5890..290828ac782 100644
--- a/composer.json
+++ b/composer.json
@@ -145,7 +145,7 @@
"orchestra/testbench": "^9.1",
"phpspec/prophecy-phpunit": "^2.2",
"phpstan/extension-installer": "^1.1",
- "phpstan/phpdoc-parser": "^1.13",
+ "phpstan/phpdoc-parser": "^1.13|^2.0",
"phpstan/phpstan": "^1.10",
"phpstan/phpstan-doctrine": "^1.0",
"phpstan/phpstan-phpunit": "^1.0",
diff --git a/src/Hal/Serializer/ItemNormalizer.php b/src/Hal/Serializer/ItemNormalizer.php
index f128e150909..24748804177 100644
--- a/src/Hal/Serializer/ItemNormalizer.php
+++ b/src/Hal/Serializer/ItemNormalizer.php
@@ -13,14 +13,26 @@
namespace ApiPlatform\Hal\Serializer;
+use ApiPlatform\Metadata\IriConverterInterface;
+use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
+use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
+use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
+use ApiPlatform\Metadata\ResourceAccessCheckerInterface;
+use ApiPlatform\Metadata\ResourceClassResolverInterface;
use ApiPlatform\Metadata\UrlGeneratorInterface;
use ApiPlatform\Metadata\Util\ClassInfoTrait;
use ApiPlatform\Serializer\AbstractItemNormalizer;
use ApiPlatform\Serializer\CacheKeyTrait;
use ApiPlatform\Serializer\ContextTrait;
+use ApiPlatform\Serializer\TagCollectorInterface;
+use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
+use Symfony\Component\Serializer\Exception\CircularReferenceException;
use Symfony\Component\Serializer\Exception\LogicException;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface;
+use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
+use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
+use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
/**
* Converts between objects and array including HAL metadata.
@@ -35,9 +47,25 @@ final class ItemNormalizer extends AbstractItemNormalizer
public const FORMAT = 'jsonhal';
+ protected const HAL_CIRCULAR_REFERENCE_LIMIT_COUNTERS = 'hal_circular_reference_limit_counters';
+
private array $componentsCache = [];
private array $attributesMetadataCache = [];
+ public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ?PropertyAccessorInterface $propertyAccessor = null, ?NameConverterInterface $nameConverter = null, ?ClassMetadataFactoryInterface $classMetadataFactory = null, array $defaultContext = [], ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, ?ResourceAccessCheckerInterface $resourceAccessChecker = null, ?TagCollectorInterface $tagCollector = null)
+ {
+ $defaultContext[AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER] = function ($object): ?array {
+ $iri = $this->iriConverter->getIriFromResource($object);
+ if (null === $iri) {
+ return null;
+ }
+
+ return ['_links' => ['self' => ['href' => $iri]]];
+ };
+
+ parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $defaultContext, $resourceMetadataCollectionFactory, $resourceAccessChecker, $tagCollector);
+ }
+
/**
* {@inheritdoc}
*/
@@ -216,6 +244,10 @@ private function populateRelation(array $data, object $object, ?string $format,
{
$class = $this->getObjectClass($object);
+ if ($this->isHalCircularReference($object, $context)) {
+ return $this->handleHalCircularReference($object, $format, $context);
+ }
+
$attributesMetadata = \array_key_exists($class, $this->attributesMetadataCache) ?
$this->attributesMetadataCache[$class] :
$this->attributesMetadataCache[$class] = $this->classMetadataFactory ? $this->classMetadataFactory->getMetadataFor($class)->getAttributesMetadata() : null;
@@ -319,4 +351,49 @@ private function isMaxDepthReached(array $attributesMetadata, string $class, str
return false;
}
+
+ /**
+ * Detects if the configured circular reference limit is reached.
+ *
+ * @throws CircularReferenceException
+ */
+ protected function isHalCircularReference(object $object, array &$context): bool
+ {
+ $objectHash = spl_object_hash($object);
+
+ $circularReferenceLimit = $context[AbstractNormalizer::CIRCULAR_REFERENCE_LIMIT] ?? $this->defaultContext[AbstractNormalizer::CIRCULAR_REFERENCE_LIMIT];
+ if (isset($context[self::HAL_CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash])) {
+ if ($context[self::HAL_CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash] >= $circularReferenceLimit) {
+ unset($context[self::HAL_CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash]);
+
+ return true;
+ }
+
+ ++$context[self::HAL_CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash];
+ } else {
+ $context[self::HAL_CIRCULAR_REFERENCE_LIMIT_COUNTERS][$objectHash] = 1;
+ }
+
+ return false;
+ }
+
+ /**
+ * Handles a circular reference.
+ *
+ * If a circular reference handler is set, it will be called. Otherwise, a
+ * {@class CircularReferenceException} will be thrown.
+ *
+ * @final
+ *
+ * @throws CircularReferenceException
+ */
+ protected function handleHalCircularReference(object $object, ?string $format = null, array $context = []): mixed
+ {
+ $circularReferenceHandler = $context[AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER] ?? $this->defaultContext[AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER];
+ if ($circularReferenceHandler) {
+ return $circularReferenceHandler($object, $format, $context);
+ }
+
+ throw new CircularReferenceException(\sprintf('A circular reference has been detected when serializing the object of class "%s" (configured limit: %d).', get_debug_type($object), $context[AbstractNormalizer::CIRCULAR_REFERENCE_LIMIT] ?? $this->defaultContext[AbstractNormalizer::CIRCULAR_REFERENCE_LIMIT]));
+ }
}
diff --git a/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php
index 066244428d1..f0eda744432 100644
--- a/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php
+++ b/src/Metadata/Resource/Factory/ParameterResourceMetadataCollectionFactory.php
@@ -200,6 +200,10 @@ private function setDefaults(string $key, Parameter $parameter, string $resource
$currentKey = $nameConvertedKey;
}
+ if ($this->nameConverter && $property = $parameter->getProperty()) {
+ $parameter = $parameter->withProperty($this->nameConverter->normalize($property));
+ }
+
if (isset($properties[$currentKey]) && ($eloquentRelation = ($properties[$currentKey]->getExtraProperties()['eloquent_relation'] ?? null)) && isset($eloquentRelation['foreign_key'])) {
$parameter = $parameter->withExtraProperties(['_query_property' => $eloquentRelation['foreign_key']] + $parameter->getExtraProperties());
}
diff --git a/src/Metadata/Resource/Factory/PhpDocResourceMetadataCollectionFactory.php b/src/Metadata/Resource/Factory/PhpDocResourceMetadataCollectionFactory.php
index 2a5163f424c..b7c1378b57b 100644
--- a/src/Metadata/Resource/Factory/PhpDocResourceMetadataCollectionFactory.php
+++ b/src/Metadata/Resource/Factory/PhpDocResourceMetadataCollectionFactory.php
@@ -25,6 +25,7 @@
use PHPStan\PhpDocParser\Parser\PhpDocParser;
use PHPStan\PhpDocParser\Parser\TokenIterator;
use PHPStan\PhpDocParser\Parser\TypeParser;
+use PHPStan\PhpDocParser\ParserConfig;
/**
* Extracts descriptions from PHPDoc.
@@ -58,9 +59,13 @@ public function __construct(private readonly ResourceMetadataCollectionFactoryIn
}
$phpDocParser = null;
$lexer = null;
- if (class_exists(PhpDocParser::class)) {
- $phpDocParser = new PhpDocParser(new TypeParser(new ConstExprParser()), new ConstExprParser());
- $lexer = new Lexer();
+ if (class_exists(PhpDocParser::class) && class_exists(ParserConfig::class)) {
+ $config = new ParserConfig([]);
+ $phpDocParser = new PhpDocParser($config, new TypeParser($config, new ConstExprParser($config)), new ConstExprParser($config));
+ $lexer = new Lexer($config);
+ } elseif (class_exists(PhpDocParser::class)) {
+ $phpDocParser = new PhpDocParser(new TypeParser(new ConstExprParser()), new ConstExprParser()); // @phpstan-ignore-line
+ $lexer = new Lexer(); // @phpstan-ignore-line
}
$this->phpDocParser = $phpDocParser;
$this->lexer = $lexer;
diff --git a/src/State/Pagination/ArrayPaginator.php b/src/State/Pagination/ArrayPaginator.php
index 3de9ab6e8f1..8c50186eb62 100644
--- a/src/State/Pagination/ArrayPaginator.php
+++ b/src/State/Pagination/ArrayPaginator.php
@@ -27,14 +27,15 @@ final class ArrayPaginator implements \IteratorAggregate, PaginatorInterface, Ha
public function __construct(array $results, int $firstResult, int $maxResults)
{
- if ($maxResults > 0) {
+ $this->firstResult = $firstResult;
+ $this->maxResults = $maxResults;
+ $this->totalItems = \count($results);
+
+ if ($maxResults > 0 && $firstResult < $this->totalItems) {
$this->iterator = new \LimitIterator(new \ArrayIterator($results), $firstResult, $maxResults);
} else {
$this->iterator = new \EmptyIterator();
}
- $this->firstResult = $firstResult;
- $this->maxResults = $maxResults;
- $this->totalItems = \count($results);
}
/**
diff --git a/src/State/Provider/ContentNegotiationProvider.php b/src/State/Provider/ContentNegotiationProvider.php
index f01b3e268d0..02f28f30121 100644
--- a/src/State/Provider/ContentNegotiationProvider.php
+++ b/src/State/Provider/ContentNegotiationProvider.php
@@ -92,6 +92,14 @@ private function flattenMimeTypes(array $formats): array
*/
private function getInputFormat(HttpOperation $operation, Request $request): ?string
{
+ if (
+ false === ($input = $operation->getInput())
+ || (\is_array($input) && null === $input['class'])
+ || false === $operation->canDeserialize()
+ ) {
+ return null;
+ }
+
$contentType = $request->headers->get('CONTENT_TYPE');
if (null === $contentType || '' === $contentType) {
return null;
@@ -103,14 +111,14 @@ private function getInputFormat(HttpOperation $operation, Request $request): ?st
return $format;
}
- $supportedMimeTypes = [];
- foreach ($formats as $mimeTypes) {
- foreach ($mimeTypes as $mimeType) {
- $supportedMimeTypes[] = $mimeType;
+ if (!$request->isMethodSafe() && 'DELETE' !== $request->getMethod()) {
+ $supportedMimeTypes = [];
+ foreach ($formats as $mimeTypes) {
+ foreach ($mimeTypes as $mimeType) {
+ $supportedMimeTypes[] = $mimeType;
+ }
}
- }
- if (!$request->isMethodSafe() && 'DELETE' !== $request->getMethod()) {
throw new UnsupportedMediaTypeHttpException(\sprintf('The content-type "%s" is not supported. Supported MIME types are "%s".', $contentType, implode('", "', $supportedMimeTypes)));
}
diff --git a/src/Symfony/Bundle/Resources/config/metadata/resource.xml b/src/Symfony/Bundle/Resources/config/metadata/resource.xml
index 59b9422a9df..ea88caba300 100644
--- a/src/Symfony/Bundle/Resources/config/metadata/resource.xml
+++ b/src/Symfony/Bundle/Resources/config/metadata/resource.xml
@@ -83,7 +83,8 @@
-
+
+
diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue4358/ResourceA.php b/tests/Fixtures/TestBundle/ApiResource/Issue4358/ResourceA.php
new file mode 100644
index 00000000000..a86482bc2ea
--- /dev/null
+++ b/tests/Fixtures/TestBundle/ApiResource/Issue4358/ResourceA.php
@@ -0,0 +1,64 @@
+
+ *
+ * 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\Fixtures\TestBundle\ApiResource\Issue4358;
+
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\Get;
+use Symfony\Component\Serializer\Annotation\Groups;
+use Symfony\Component\Serializer\Annotation\MaxDepth;
+
+#[Get(uriTemplate: 'resource_a',
+ formats: ['jsonhal'],
+ outputFormats: ['jsonhal'],
+ normalizationContext: ['groups' => ['ResourceA:read'], 'enable_max_depth' => true],
+ provider: [self::class, 'provide'])]
+final class ResourceA
+{
+ private static ?ResourceA $resourceA = null;
+
+ #[ApiProperty(readableLink: true)]
+ #[Groups(['ResourceA:read', 'ResourceB:read'])]
+ #[MaxDepth(6)]
+ public ResourceB $b;
+
+ public function __construct(?ResourceB $b = null)
+ {
+ if (null !== $b) {
+ $this->b = $b;
+ }
+ }
+
+ public static function provide(): self
+ {
+ return self::provideWithResource();
+ }
+
+ public static function provideWithResource(?ResourceB $b = null): self
+ {
+ if (!isset(self::$resourceA)) {
+ self::$resourceA = new self($b);
+
+ if (null === ResourceB::getInstance()) {
+ self::$resourceA->b = ResourceB::provideWithResource(self::$resourceA);
+ }
+ }
+
+ return self::$resourceA;
+ }
+
+ public static function getInstance(): ?self
+ {
+ return self::$resourceA;
+ }
+}
diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue4358/ResourceB.php b/tests/Fixtures/TestBundle/ApiResource/Issue4358/ResourceB.php
new file mode 100644
index 00000000000..cd5ba29d3c5
--- /dev/null
+++ b/tests/Fixtures/TestBundle/ApiResource/Issue4358/ResourceB.php
@@ -0,0 +1,64 @@
+
+ *
+ * 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\Fixtures\TestBundle\ApiResource\Issue4358;
+
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\Get;
+use Symfony\Component\Serializer\Annotation\Groups;
+use Symfony\Component\Serializer\Annotation\MaxDepth;
+
+#[Get(uriTemplate: 'resource_b',
+ formats: ['jsonhal'],
+ outputFormats: ['jsonhal'],
+ normalizationContext: ['groups' => ['ResourceB:read'], 'enable_max_depth' => true],
+ provider: [self::class, 'provide'])]
+final class ResourceB
+{
+ private static ?ResourceB $resourceB = null;
+
+ #[ApiProperty(readableLink: true)]
+ #[Groups(['ResourceA:read', 'ResourceB:read'])]
+ #[MaxDepth(6)]
+ public ResourceA $a;
+
+ public function __construct(?ResourceA $a = null)
+ {
+ if (null !== $a) {
+ $this->a = $a;
+ }
+ }
+
+ public static function provide(): self
+ {
+ return self::provideWithResource();
+ }
+
+ public static function provideWithResource(?ResourceA $a = null): self
+ {
+ if (!isset(self::$resourceB)) {
+ self::$resourceB = new self($a);
+
+ if (null === ResourceA::getInstance()) {
+ self::$resourceB->a = ResourceA::provideWithResource(self::$resourceB);
+ }
+ }
+
+ return self::$resourceB;
+ }
+
+ public static function getInstance(): ?self
+ {
+ return self::$resourceB;
+ }
+}
diff --git a/tests/Fixtures/TestBundle/Document/DummyProduct.php b/tests/Fixtures/TestBundle/Document/DummyProduct.php
index 0887236029b..cdd001ca68f 100644
--- a/tests/Fixtures/TestBundle/Document/DummyProduct.php
+++ b/tests/Fixtures/TestBundle/Document/DummyProduct.php
@@ -23,7 +23,7 @@
/**
* Dummy Product.
*
- * https://github.com/api-platform/core/issues/1107.
+ * @see https://github.com/api-platform/core/issues/1107
*
* @author Antoine Bluchet
*/
diff --git a/tests/Fixtures/TestBundle/Entity/DummyProduct.php b/tests/Fixtures/TestBundle/Entity/DummyProduct.php
index 0f34fe8174d..d6e428f8a02 100644
--- a/tests/Fixtures/TestBundle/Entity/DummyProduct.php
+++ b/tests/Fixtures/TestBundle/Entity/DummyProduct.php
@@ -23,7 +23,7 @@
/**
* Dummy Product.
*
- * https://github.com/api-platform/core/issues/1107.
+ * @see https://github.com/api-platform/core/issues/1107
*
* @author Antoine Bluchet
*/
diff --git a/tests/Functional/HALCircularReference.php b/tests/Functional/HALCircularReference.php
new file mode 100644
index 00000000000..eda2afffaa2
--- /dev/null
+++ b/tests/Functional/HALCircularReference.php
@@ -0,0 +1,33 @@
+
+ *
+ * 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\Functional;
+
+use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
+use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue4358\ResourceA;
+use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue4358\ResourceB;
+
+class HALCircularReference extends ApiTestCase
+{
+ public function testIssue4358(): void
+ {
+ $r1 = self::createClient()->request('GET', '/resource_a', ['headers' => ['Accept' => 'application/hal+json']]);
+ self::assertResponseIsSuccessful();
+ self::assertEquals('{"_links":{"self":{"href":"\/resource_a"},"b":{"href":"\/resource_b"}},"_embedded":{"b":{"_links":{"self":{"href":"\/resource_b"},"a":{"href":"\/resource_a"}},"_embedded":{"a":{"_links":{"self":{"href":"\/resource_a"}}}}}}}', $r1->getContent());
+ }
+
+ public static function getResources(): array
+ {
+ return [ResourceA::class, ResourceB::class];
+ }
+}
diff --git a/tests/State/Pagination/ArrayPaginatorTest.php b/tests/State/Pagination/ArrayPaginatorTest.php
index 317ad6055e0..ba96f7bcbd3 100644
--- a/tests/State/Pagination/ArrayPaginatorTest.php
+++ b/tests/State/Pagination/ArrayPaginatorTest.php
@@ -41,6 +41,7 @@ public static function initializeProvider(): array
'Second of two pages of 3 items for the first page and 2 for the second' => [[0, 1, 2, 3, 4], 3, 3, 2, 5, 2, 2, false],
'Empty results' => [[], 0, 2, 0, 0, 1, 1, false],
'0 for max results' => [[0, 1, 2, 3], 2, 0, 0, 4, 1, 1, false],
+ 'First result greater than total items' => [[0, 1], 2, 1, 0, 2, 3, 2, false],
];
}
}
diff --git a/tests/State/Provider/ContentNegotiationProviderTest.php b/tests/State/Provider/ContentNegotiationProviderTest.php
index e4eb99ba904..dd610bd4b8d 100644
--- a/tests/State/Provider/ContentNegotiationProviderTest.php
+++ b/tests/State/Provider/ContentNegotiationProviderTest.php
@@ -21,6 +21,7 @@
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
class ContentNegotiationProviderTest extends TestCase
{
@@ -60,4 +61,63 @@ public function testRequestWithEmptyContentType(): void
$this->assertSame($expectedResult, $result);
}
+
+ public function testRequestWhenNoInput(): 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);
+
+ $request = new Request(
+ server: [
+ 'REQUEST_METHOD' => 'POST',
+ 'REQUEST_URI' => '/',
+ 'CONTENT_TYPE' => 'some-not-supported/content-type',
+ ],
+ content: ''
+ );
+
+ $operation = new Post();
+ $operation = $operation->withDeserialize(false);
+ $context = ['request' => $request];
+
+ $result = $provider->provide($operation, [], $context);
+
+ $this->assertSame($expectedResult, $result);
+ }
+
+ public function testRequestWithInput(): void
+ {
+ $this->expectException(UnsupportedMediaTypeHttpException::class);
+
+ $decorated = $this->prophesize(ProviderInterface::class);
+
+ $negotiator = new Negotiator();
+ $formats = ['jsonld' => ['application/ld+json']];
+ $errorFormats = ['jsonld' => ['application/ld+json']];
+
+ $provider = new ContentNegotiationProvider($decorated->reveal(), $negotiator, $formats, $errorFormats);
+
+ $request = new Request(
+ server: [
+ 'REQUEST_METHOD' => 'POST',
+ 'REQUEST_URI' => '/',
+ 'CONTENT_TYPE' => 'some-not-supported/content-type',
+ ],
+ content: ''
+ );
+
+ $operation = new Post();
+ $operation = $operation->withDeserialize();
+ $context = ['request' => $request];
+
+ $provider->provide($operation, [], $context);
+ }
}