Upstream api-platform/core state of the touched files before 5e95dd3773
This commit is contained in:
1 parent
167625fa31
commit
b2a14e558d
5 files changed
+968
No files matched your search
@@ -0,0 +1,204 @@
|
||||
parameters:
|
||||
level: 6
|
||||
paths:
|
||||
- src
|
||||
- tests
|
||||
- tests/Fixtures/app/console
|
||||
inferPrivatePropertyTypeFromConstructor: true
|
||||
treatPhpDocTypesAsCertain: false
|
||||
symfony:
|
||||
containerXmlPath: tests/Fixtures/app/var/cache/test/AppKernelTestDebugContainer.xml
|
||||
constantHassers: false
|
||||
doctrine:
|
||||
objectManagerLoader: tests/Fixtures/app/object-manager.php
|
||||
bootstrapFiles:
|
||||
# We're aliasing classes for phpunit in this file, it needs to be added here see phpstan/#2194
|
||||
- src/Symfony/Bundle/Test/Constraint/ArraySubset.php
|
||||
- tests/Fixtures/app/AppKernel.php
|
||||
|
||||
excludePaths:
|
||||
# uses larastan
|
||||
- src/Laravel
|
||||
# Symfony config
|
||||
- tests/Fixtures/app/config/config_swagger.php
|
||||
# Symfony cache
|
||||
- tests/Fixtures/app/var/
|
||||
- tests/Fixtures/Symfony/Maker
|
||||
# The Symfony Configuration API isn't good enough to be analysed
|
||||
- src/Symfony/Bundle/DependencyInjection/Configuration.php
|
||||
# Templates for Maker
|
||||
- src/Symfony/Maker/Resources/skeleton
|
||||
# subtree split
|
||||
- src/Doctrine/Orm/Tests/var/*
|
||||
- src/Doctrine/Odm/Tests/var/*
|
||||
- src/Doctrine/*/vendor/*
|
||||
- src/*/vendor/*
|
||||
# Symfony 6 support
|
||||
- src/Symfony/Bundle/ArgumentResolver/CompatibleValueResolverInterface.php
|
||||
- tests/Fixtures/app/config/reference.php
|
||||
earlyTerminatingMethodCalls:
|
||||
PHPUnit\Framework\Constraint\Constraint:
|
||||
- fail
|
||||
ApiPlatform\Metadata\Resource\ResourceMetadataCollection:
|
||||
- handleNotFound
|
||||
ignoreErrors:
|
||||
# ignore type todo remove in 5.0
|
||||
-
|
||||
message: '#Symfony\\Component\\PropertyInfo\\Type#'
|
||||
identifier: class.notFound
|
||||
# False positives
|
||||
- message: '#Call to an undefined method Negotiation\\AcceptHeader::getType\(\).#'
|
||||
-
|
||||
identifier: doctrine.columnType
|
||||
paths:
|
||||
- tests/Fixtures/TestBundle/Entity/
|
||||
-
|
||||
identifier: doctrine.associationType
|
||||
paths:
|
||||
- tests/Fixtures/TestBundle/Entity/
|
||||
- '#Cannot call method add\(\) on iterable.#'
|
||||
-
|
||||
message: '#is never read, only written.#'
|
||||
paths:
|
||||
- tests/Fixtures/TestBundle/Document/
|
||||
- tests/Fixtures/TestBundle/Entity/
|
||||
- src/OpenApi/Factory/OpenApiFactory.php
|
||||
- src/JsonLd/Serializer/ObjectNormalizer.php
|
||||
-
|
||||
message: '#is never assigned .* so it can be removed from the property type.#'
|
||||
paths:
|
||||
- src/Doctrine/Common/Tests/Fixtures/
|
||||
- src/Doctrine/Orm/Tests/Fixtures/
|
||||
- src/Doctrine/Odm/Tests/Fixtures/
|
||||
- src/Elasticsearch/Tests/Fixtures/
|
||||
- src/GraphQl/Tests/Fixtures/
|
||||
- src/JsonSchema/Tests/Fixtures/
|
||||
- src/Metadata/Tests/Fixtures/
|
||||
- src/Serializer/Tests/Fixtures/
|
||||
- tests/Fixtures/
|
||||
-
|
||||
message: '#is never written, only read.#'
|
||||
paths:
|
||||
- tests/Fixtures/TestBundle/Document/
|
||||
- src/Metadata/Tests/Fixtures/ApiResource/
|
||||
- '#Access to an undefined property Prophecy\\Prophecy\\ObjectProphecy<(\\?[a-zA-Z0-9_]+)+>::\$[a-zA-Z0-9_]+#'
|
||||
# ORM 2 and 3 disagree on the shape of ClassMetadata::$associationMappings
|
||||
# (raw arrays vs mapping objects). The extension reads them through
|
||||
# ArrayAccess, so array fixtures are valid for both; composer still
|
||||
# supports doctrine/orm ^2.17.
|
||||
-
|
||||
identifier: assign.propertyType
|
||||
path: src/Doctrine/Orm/Tests/Extension/EagerLoadingExtensionTest.php
|
||||
# https://github.com/phpstan/phpstan-symfony/issues/76
|
||||
-
|
||||
message: '#Service "test" is not registered in the container\.#'
|
||||
path: src/GraphQl/Tests/Type/TypesContainerTest.php
|
||||
|
||||
# Expected, due to backward compatibility
|
||||
- '#Access to an undefined property GraphQL\\Type\\Definition\\NamedType&GraphQL\\Type\\Definition\\Type::\$name\.#'
|
||||
- "#Call to function method_exists\\(\\) with 'Symfony\\\\\\\\Component\\\\\\\\PropertyInfo\\\\\\\\PropertyInfoExtractor' and 'getType' will always evaluate to true\\.#"
|
||||
- "#Call to function method_exists\\(\\) with 'Symfony\\\\\\\\Component\\\\\\\\HttpFoundation\\\\\\\\Request' and 'getContentTypeFormat' will always evaluate to true\\.#"
|
||||
- '#Call to an undefined method Symfony\\Component\\HttpFoundation\\Request::getContentType\(\)\.#'
|
||||
- "#Call to function method_exists\\(\\) with 'Symfony\\\\\\\\Component\\\\\\\\Serializer\\\\\\\\Serializer' and 'getSupportedTypes' will always evaluate to true\\.#"
|
||||
- "#Call to function method_exists\\(\\) with Symfony\\\\Component\\\\Serializer\\\\Normalizer\\\\NormalizerInterface and 'getSupportedTypes' will always evaluate to true\\.#"
|
||||
- "#Call to function method_exists\\(\\) with Doctrine\\\\ODM\\\\MongoDB\\\\Mapping\\\\ClassMetadata\\|Doctrine\\\\ORM\\\\Mapping\\\\ClassMetadata and 'isChangeTrackingDef…' will always evaluate to true\\.#"
|
||||
|
||||
# See https://github.com/phpstan/phpstan-symfony/issues/27
|
||||
-
|
||||
message: '#^Service "[^"]+" is private.$#'
|
||||
path: src
|
||||
|
||||
|
||||
# Allow extra assertions in tests: https://github.com/phpstan/phpstan-strict-rules/issues/130
|
||||
- '#^Call to (static )?method PHPUnit\\Framework\\Assert::.* will always evaluate to true\.$#'
|
||||
|
||||
# Level 6
|
||||
-
|
||||
identifier: missingType.iterableValue
|
||||
paths:
|
||||
- src/Doctrine/Common/Tests
|
||||
- src/Doctrine/Odm/Tests
|
||||
- src/Doctrine/Orm/Tests
|
||||
- src/Elasticsearch/Tests
|
||||
- src/GraphQl/Tests
|
||||
- src/HttpCache/Tests
|
||||
- src/Hydra/Tests
|
||||
- src/JsonApi/Tests
|
||||
- src/JsonSchema/Tests
|
||||
- src/Metadata/Tests
|
||||
- src/OpenApi/Tests
|
||||
- src/Serializer/Tests
|
||||
- src/State/Tests
|
||||
- src/Symfony/Tests
|
||||
- tests
|
||||
- src # TODO
|
||||
-
|
||||
identifier: missingType.generics
|
||||
-
|
||||
identifier: missingType.property
|
||||
paths:
|
||||
- src/Doctrine/Common/Tests
|
||||
- src/Doctrine/Odm/Tests
|
||||
- src/Doctrine/Orm/Tests
|
||||
- src/Elasticsearch/Tests
|
||||
- src/Hydra/Tests
|
||||
- src/JsonApi/Tests
|
||||
- src/JsonSchema/Tests
|
||||
- src/Metadata/Tests
|
||||
- src/Serializer/Tests
|
||||
- src/Symfony/Tests
|
||||
- tests
|
||||
-
|
||||
identifier: missingType.parameter
|
||||
paths:
|
||||
- src/Doctrine/Common/Tests
|
||||
- src/Doctrine/Odm/Tests
|
||||
- src/Doctrine/Orm/Tests
|
||||
- src/Elasticsearch/Tests
|
||||
- src/GraphQl/Tests
|
||||
- src/Hydra/Tests
|
||||
- src/JsonApi/Tests
|
||||
- src/JsonSchema/Tests
|
||||
- src/Metadata/Tests
|
||||
- src/OpenApi/Tests
|
||||
- src/Serializer/Tests
|
||||
- src/Symfony/Bundle/Test
|
||||
- src/Symfony/Tests
|
||||
- tests
|
||||
-
|
||||
identifier: missingType.return
|
||||
paths:
|
||||
- src/Doctrine/Common/Tests
|
||||
- src/Doctrine/Odm/Tests
|
||||
- src/Doctrine/Orm/Tests
|
||||
- src/Elasticsearch/Tests
|
||||
- src/GraphQl/Tests
|
||||
- src/Hydra/Tests
|
||||
- src/JsonApi/Tests
|
||||
- src/JsonSchema/Tests
|
||||
- src/Metadata/Tests
|
||||
- src/OpenApi/Tests
|
||||
- src/Serializer/Tests
|
||||
- src/Symfony/Tests
|
||||
- tests
|
||||
-
|
||||
identifier: argument.templateType
|
||||
paths:
|
||||
- src/Symfony/Bundle/Test
|
||||
- tests
|
||||
- src # TODO
|
||||
|
||||
-
|
||||
identifier: method.notFound
|
||||
paths:
|
||||
- tests/Functional/JsonStreamerTest.php
|
||||
|
||||
# Symfony Console 7.4 added Application::addCommand(); 8.1 removed add(). One branch is unreachable depending on installed version.
|
||||
-
|
||||
message: '#Call to an undefined method Symfony\\Component\\Console\\Application::(add|addCommand)\(\)\.#'
|
||||
path: tests/Symfony/Bundle/Command/DebugResourceCommandTest.php
|
||||
reportUnmatched: false
|
||||
-
|
||||
identifier: function.alreadyNarrowedType
|
||||
path: tests/Symfony/Bundle/Command/DebugResourceCommandTest.php
|
||||
reportUnmatched: false
|
||||
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the API Platform project.
|
||||
*
|
||||
* (c) Kévin Dunglas <dunglas@gmail.com>
|
||||
*
|
||||
* 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\State\Provider;
|
||||
|
||||
use ApiPlatform\Metadata\HttpOperation;
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use ApiPlatform\State\SerializerContextBuilderInterface;
|
||||
use ApiPlatform\State\StopwatchAwareInterface;
|
||||
use ApiPlatform\State\StopwatchAwareTrait;
|
||||
use ApiPlatform\Validator\Exception\ValidationException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
|
||||
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
|
||||
use Symfony\Component\Serializer\Exception\PartialDenormalizationException;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
use Symfony\Component\Validator\Constraints\Type;
|
||||
use Symfony\Component\Validator\ConstraintViolation;
|
||||
use Symfony\Component\Validator\ConstraintViolationList;
|
||||
use Symfony\Contracts\Translation\LocaleAwareInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorTrait;
|
||||
|
||||
final class DeserializeProvider implements ProviderInterface, StopwatchAwareInterface
|
||||
{
|
||||
use StopwatchAwareTrait;
|
||||
|
||||
public function __construct(
|
||||
private readonly ?ProviderInterface $decorated,
|
||||
private readonly SerializerInterface $serializer,
|
||||
private readonly SerializerContextBuilderInterface $serializerContextBuilder,
|
||||
private ?TranslatorInterface $translator = null,
|
||||
) {
|
||||
if (null === $this->translator) {
|
||||
$this->translator = new class implements TranslatorInterface, LocaleAwareInterface {
|
||||
use TranslatorTrait;
|
||||
};
|
||||
$this->translator->setLocale('en');
|
||||
}
|
||||
}
|
||||
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
|
||||
{
|
||||
// We need request content
|
||||
if (!$operation instanceof HttpOperation || !($request = $context['request'] ?? null)) {
|
||||
return $this->decorated?->provide($operation, $uriVariables, $context);
|
||||
}
|
||||
|
||||
$data = $this->decorated ? $this->decorated->provide($operation, $uriVariables, $context) : $request->attributes->get('data');
|
||||
|
||||
if (!$operation->canDeserialize() || $context['request']->attributes->has('deserialized')) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$this->stopwatch?->start('api_platform.provider.deserialize');
|
||||
|
||||
$contentType = $request->headers->get('CONTENT_TYPE');
|
||||
if (null === $contentType || '' === $contentType) {
|
||||
throw new UnsupportedMediaTypeHttpException('The "Content-Type" header must exist.');
|
||||
}
|
||||
|
||||
$serializerContext = $this->serializerContextBuilder->createFromRequest($request, false, [
|
||||
'resource_class' => $operation->getClass(),
|
||||
'operation' => $operation,
|
||||
]);
|
||||
|
||||
$serializerContext['uri_variables'] = $uriVariables;
|
||||
|
||||
if (!$format = $request->attributes->get('input_format') ?? null) {
|
||||
throw new UnsupportedMediaTypeHttpException('Format not supported.');
|
||||
}
|
||||
|
||||
if (null === ($serializerContext[SerializerContextBuilderInterface::ASSIGN_OBJECT_TO_POPULATE] ?? null)) {
|
||||
$method = $operation->getMethod();
|
||||
$assignObjectToPopulate = 'POST' === $method
|
||||
|| 'PATCH' === $method
|
||||
|| ('PUT' === $method && !($operation->getExtraProperties()['standard_put'] ?? true));
|
||||
|
||||
if ($assignObjectToPopulate) {
|
||||
$serializerContext[SerializerContextBuilderInterface::ASSIGN_OBJECT_TO_POPULATE] = true;
|
||||
trigger_deprecation('api-platform/core', '4.4', 'To assign an object to populate you should set "%s" in your denormalizationContext, not defining it is deprecated.', SerializerContextBuilderInterface::ASSIGN_OBJECT_TO_POPULATE);
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $data && ($serializerContext[SerializerContextBuilderInterface::ASSIGN_OBJECT_TO_POPULATE] ?? false)) {
|
||||
$serializerContext[AbstractNormalizer::OBJECT_TO_POPULATE] = $data;
|
||||
}
|
||||
|
||||
unset($serializerContext[SerializerContextBuilderInterface::ASSIGN_OBJECT_TO_POPULATE]);
|
||||
|
||||
try {
|
||||
$data = $this->serializer->deserialize((string) $request->getContent(), $serializerContext['deserializer_type'] ?? $operation->getClass(), $format, $serializerContext);
|
||||
} catch (PartialDenormalizationException $e) {
|
||||
if (!class_exists(ConstraintViolationList::class)) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$violations = new ConstraintViolationList();
|
||||
foreach ($e->getErrors() as $exception) {
|
||||
if (!$exception instanceof NotNormalizableValueException) {
|
||||
continue;
|
||||
}
|
||||
$violations->add($this->createViolationFromException($exception));
|
||||
}
|
||||
if (0 !== \count($violations)) {
|
||||
throw new ValidationException($violations);
|
||||
}
|
||||
} catch (NotNormalizableValueException $e) {
|
||||
// BackedEnum denormalization errors should surface as validation violations (422)
|
||||
// rather than denormalization errors (400). See https://github.com/api-platform/core/issues/8183.
|
||||
if (!class_exists(ConstraintViolationList::class) || !$this->isBackedEnumException($e)) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$violations = new ConstraintViolationList();
|
||||
$violations->add($this->createViolationFromException($e));
|
||||
throw new ValidationException($violations);
|
||||
}
|
||||
|
||||
$this->stopwatch?->stop('api_platform.provider.deserialize');
|
||||
|
||||
$request->attributes->set('data', $data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function normalizeExpectedTypes(?array $expectedTypes = null): array
|
||||
{
|
||||
$normalizedTypes = [];
|
||||
|
||||
foreach ($expectedTypes ?? [] as $expectedType) {
|
||||
$normalizedType = $expectedType;
|
||||
|
||||
if (class_exists($expectedType) || interface_exists($expectedType)) {
|
||||
// A backed enum is sent over the wire as its backing scalar (e.g. "string"), not as the
|
||||
// PHP enum class, so report the JSON-visible type rather than the internal FQCN (#8388).
|
||||
if (is_subclass_of($expectedType, \BackedEnum::class) && ($backingType = (new \ReflectionEnum($expectedType))->getBackingType())) {
|
||||
$normalizedType = (string) $backingType;
|
||||
} else {
|
||||
$normalizedType = (new \ReflectionClass($expectedType))->getShortName();
|
||||
}
|
||||
}
|
||||
|
||||
$normalizedTypes[] = $normalizedType;
|
||||
}
|
||||
|
||||
return array_values(array_unique($normalizedTypes));
|
||||
}
|
||||
|
||||
private function createViolationFromException(NotNormalizableValueException $exception): ConstraintViolation
|
||||
{
|
||||
$expectedTypes = $this->normalizeExpectedTypes($exception->getExpectedTypes());
|
||||
$parameters = [];
|
||||
if ($exception->canUseMessageForUser()) {
|
||||
$parameters['hint'] = $exception->getMessage();
|
||||
}
|
||||
|
||||
if (!$expectedTypes && $exception->canUseMessageForUser()) {
|
||||
$violationMessage = $exception->getMessage();
|
||||
|
||||
return new ConstraintViolation($violationMessage, $violationMessage, $parameters, null, $exception->getPath(), null, null, (string) Type::INVALID_TYPE_ERROR);
|
||||
}
|
||||
|
||||
$message = (new Type($expectedTypes))->message;
|
||||
|
||||
return new ConstraintViolation($this->translator->trans($message, ['{{ type }}' => implode('|', $expectedTypes)], 'validators'), $message, $parameters, null, $exception->getPath(), null, null, (string) Type::INVALID_TYPE_ERROR);
|
||||
}
|
||||
|
||||
private function isBackedEnumException(NotNormalizableValueException $exception): bool
|
||||
{
|
||||
foreach ($exception->getExpectedTypes() ?? [] as $expectedType) {
|
||||
if (\is_string($expectedType) && (class_exists($expectedType) || interface_exists($expectedType)) && is_subclass_of($expectedType, \BackedEnum::class)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for ($previous = $exception->getPrevious(); $previous instanceof \Throwable; $previous = $previous->getPrevious()) {
|
||||
if (!$previous instanceof NotNormalizableValueException) {
|
||||
continue;
|
||||
}
|
||||
foreach ($previous->getExpectedTypes() ?? [] as $expectedType) {
|
||||
if (\is_string($expectedType) && (class_exists($expectedType) || interface_exists($expectedType)) && is_subclass_of($expectedType, \BackedEnum::class)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the API Platform project.
|
||||
*
|
||||
* (c) Kévin Dunglas <dunglas@gmail.com>
|
||||
*
|
||||
* 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\State\Tests\Provider;
|
||||
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\HttpOperation;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use ApiPlatform\State\Provider\DeserializeProvider;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use ApiPlatform\State\SerializerContextBuilderInterface;
|
||||
use ApiPlatform\Validator\Exception\ValidationException;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\IgnoreDeprecations;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
|
||||
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
|
||||
use Symfony\Component\Serializer\Exception\PartialDenormalizationException;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
use Symfony\Component\Validator\Constraints\Type;
|
||||
|
||||
class DeserializeProviderTest extends TestCase
|
||||
{
|
||||
#[IgnoreDeprecations]
|
||||
public function testDeserialize(): void
|
||||
{
|
||||
$this->expectUserDeprecationMessage('Since api-platform/core 4.4: To assign an object to populate you should set "api_assign_object_to_populate" in your denormalizationContext, not defining it is deprecated.');
|
||||
$objectToPopulate = new \stdClass();
|
||||
$serializerContext = [];
|
||||
$operation = new Post(deserialize: true, class: \stdClass::class);
|
||||
$decorated = $this->createStub(ProviderInterface::class);
|
||||
$decorated->method('provide')->willReturn($objectToPopulate);
|
||||
|
||||
$serializerContextBuilder = $this->createMock(SerializerContextBuilderInterface::class);
|
||||
$serializerContextBuilder->expects($this->once())->method('createFromRequest')->willReturn($serializerContext);
|
||||
$serializer = $this->createMock(SerializerInterface::class);
|
||||
$serializer->expects($this->once())->method('deserialize')->with('test', \stdClass::class, 'format', ['uri_variables' => ['id' => 1], AbstractNormalizer::OBJECT_TO_POPULATE => $objectToPopulate] + $serializerContext)->willReturn(new \stdClass());
|
||||
|
||||
$provider = new DeserializeProvider($decorated, $serializer, $serializerContextBuilder);
|
||||
$request = new Request(content: 'test');
|
||||
$request->headers->set('CONTENT_TYPE', 'ok');
|
||||
$request->attributes->set('input_format', 'format');
|
||||
$provider->provide($operation, ['id' => 1], ['request' => $request]);
|
||||
}
|
||||
|
||||
public function testDeserializeNoContentType(): void
|
||||
{
|
||||
$this->expectException(UnsupportedMediaTypeHttpException::class);
|
||||
$operation = new Get(deserialize: true, class: \stdClass::class);
|
||||
$decorated = $this->createStub(ProviderInterface::class);
|
||||
$decorated->method('provide')->willReturn(null);
|
||||
|
||||
$serializerContextBuilder = $this->createMock(SerializerContextBuilderInterface::class);
|
||||
$serializer = $this->createMock(SerializerInterface::class);
|
||||
|
||||
$provider = new DeserializeProvider($decorated, $serializer, $serializerContextBuilder);
|
||||
$request = new Request(content: 'test');
|
||||
$request->attributes->set('input_format', 'format');
|
||||
$provider->provide($operation, ['id' => 1], ['request' => $request]);
|
||||
}
|
||||
|
||||
public function testDeserializeNoInput(): void
|
||||
{
|
||||
$this->expectException(UnsupportedMediaTypeHttpException::class);
|
||||
$operation = new Get(deserialize: true, class: \stdClass::class);
|
||||
$decorated = $this->createStub(ProviderInterface::class);
|
||||
$decorated->method('provide')->willReturn(null);
|
||||
|
||||
$serializerContextBuilder = $this->createMock(SerializerContextBuilderInterface::class);
|
||||
$serializer = $this->createMock(SerializerInterface::class);
|
||||
|
||||
$provider = new DeserializeProvider($decorated, $serializer, $serializerContextBuilder);
|
||||
$request = new Request(content: 'test');
|
||||
$request->headers->set('CONTENT_TYPE', 'ok');
|
||||
$provider->provide($operation, ['id' => 1], ['request' => $request]);
|
||||
}
|
||||
|
||||
public function testDeserializeWithContextClass(): void
|
||||
{
|
||||
$serializerContext = ['deserializer_type' => 'Test'];
|
||||
$operation = new Get(deserialize: true);
|
||||
$decorated = $this->createStub(ProviderInterface::class);
|
||||
$decorated->method('provide')->willReturn(null);
|
||||
|
||||
$serializerContextBuilder = $this->createMock(SerializerContextBuilderInterface::class);
|
||||
$serializerContextBuilder->expects($this->once())->method('createFromRequest')->willReturn($serializerContext);
|
||||
$serializer = $this->createMock(SerializerInterface::class);
|
||||
$serializer->expects($this->once())->method('deserialize')->with('test', 'Test', 'format', ['uri_variables' => ['id' => 1]] + $serializerContext)->willReturn(new \stdClass());
|
||||
|
||||
$provider = new DeserializeProvider($decorated, $serializer, $serializerContextBuilder);
|
||||
$request = new Request(content: 'test');
|
||||
$request->headers->set('CONTENT_TYPE', 'ok');
|
||||
$request->attributes->set('input_format', 'format');
|
||||
$provider->provide($operation, ['id' => 1], ['request' => $request]);
|
||||
}
|
||||
|
||||
public function testRequestWithEmptyContentType(): void
|
||||
{
|
||||
$expectedResult = new \stdClass();
|
||||
$decorated = $this->createMock(ProviderInterface::class);
|
||||
$decorated->method('provide')->willReturn($expectedResult);
|
||||
|
||||
$serializer = $this->createStub(SerializerInterface::class);
|
||||
$serializerContextBuilder = $this->createStub(SerializerContextBuilderInterface::class);
|
||||
|
||||
$provider = new DeserializeProvider($decorated, $serializer, $serializerContextBuilder);
|
||||
|
||||
// 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);
|
||||
$provider->provide($operation, [], $context);
|
||||
}
|
||||
|
||||
#[DataProvider('provideMethodsTriggeringDeprecation')]
|
||||
#[IgnoreDeprecations]
|
||||
public function testDeserializeTriggersDeprecationWhenContextNotSet(HttpOperation $operation): void
|
||||
{
|
||||
$this->expectUserDeprecationMessage('Since api-platform/core 4.4: To assign an object to populate you should set "api_assign_object_to_populate" in your denormalizationContext, not defining it is deprecated.');
|
||||
|
||||
$objectToPopulate = new \stdClass();
|
||||
$serializerContext = [];
|
||||
$decorated = $this->createStub(ProviderInterface::class);
|
||||
$decorated->method('provide')->willReturn($objectToPopulate);
|
||||
|
||||
$serializerContextBuilder = $this->createMock(SerializerContextBuilderInterface::class);
|
||||
$serializerContextBuilder->method('createFromRequest')->willReturn($serializerContext);
|
||||
|
||||
$serializer = $this->createMock(SerializerInterface::class);
|
||||
$serializer->expects($this->once())->method('deserialize')->with(
|
||||
'test',
|
||||
\stdClass::class,
|
||||
'format',
|
||||
['uri_variables' => ['id' => 1], 'object_to_populate' => $objectToPopulate] + $serializerContext
|
||||
)->willReturn(new \stdClass());
|
||||
|
||||
$provider = new DeserializeProvider($decorated, $serializer, $serializerContextBuilder);
|
||||
$request = new Request(content: 'test');
|
||||
$request->headers->set('CONTENT_TYPE', 'ok');
|
||||
$request->attributes->set('input_format', 'format');
|
||||
$provider->provide($operation, ['id' => 1], ['request' => $request]);
|
||||
}
|
||||
|
||||
public static function provideMethodsTriggeringDeprecation(): iterable
|
||||
{
|
||||
yield 'POST method' => [new Post(deserialize: true, class: \stdClass::class)];
|
||||
yield 'PATCH method' => [new Patch(deserialize: true, class: \stdClass::class)];
|
||||
yield 'PUT method (non-standard)' => [new Put(deserialize: true, class: \stdClass::class, extraProperties: ['standard_put' => false])];
|
||||
}
|
||||
|
||||
public function testDeserializeSetsObjectToPopulateWhenContextIsTrue(): void
|
||||
{
|
||||
$objectToPopulate = new \stdClass();
|
||||
$serializerContext = [SerializerContextBuilderInterface::ASSIGN_OBJECT_TO_POPULATE => true];
|
||||
$operation = new Post(deserialize: true, class: \stdClass::class);
|
||||
$decorated = $this->createStub(ProviderInterface::class);
|
||||
$decorated->method('provide')->willReturn($objectToPopulate);
|
||||
|
||||
$serializerContextBuilder = $this->createMock(SerializerContextBuilderInterface::class);
|
||||
$serializerContextBuilder->method('createFromRequest')->willReturn($serializerContext);
|
||||
|
||||
$serializer = $this->createMock(SerializerInterface::class);
|
||||
$serializer->expects($this->once())->method('deserialize')->with(
|
||||
'test',
|
||||
\stdClass::class,
|
||||
'format',
|
||||
$this->callback(function (array $context) use ($objectToPopulate) {
|
||||
$this->assertArrayHasKey(AbstractNormalizer::OBJECT_TO_POPULATE, $context);
|
||||
$this->assertSame($objectToPopulate, $context[AbstractNormalizer::OBJECT_TO_POPULATE]);
|
||||
|
||||
return true;
|
||||
})
|
||||
)->willReturn(new \stdClass());
|
||||
|
||||
$provider = new DeserializeProvider($decorated, $serializer, $serializerContextBuilder);
|
||||
$request = new Request(content: 'test');
|
||||
$request->headers->set('CONTENT_TYPE', 'ok');
|
||||
$request->attributes->set('input_format', 'format');
|
||||
$provider->provide($operation, ['id' => 1], ['request' => $request]);
|
||||
}
|
||||
|
||||
#[IgnoreDeprecations]
|
||||
public function testDeserializeKeepsTypeMessageWhenExpectedTypesAreSet(): void
|
||||
{
|
||||
$operation = new Post(deserialize: true, class: \stdClass::class);
|
||||
$decorated = $this->createStub(ProviderInterface::class);
|
||||
$decorated->method('provide')->willReturn(null);
|
||||
|
||||
$exception = NotNormalizableValueException::createForUnexpectedDataType(
|
||||
'The data must belong to a backed enumeration of type Suit.',
|
||||
'invalid',
|
||||
['string'],
|
||||
'status',
|
||||
true,
|
||||
);
|
||||
$partialException = new PartialDenormalizationException('Denormalization failed.', [$exception]);
|
||||
|
||||
$serializerContextBuilder = $this->createMock(SerializerContextBuilderInterface::class);
|
||||
$serializerContextBuilder->method('createFromRequest')->willReturn([]);
|
||||
$serializer = $this->createMock(SerializerInterface::class);
|
||||
$serializer->method('deserialize')->willThrowException($partialException);
|
||||
|
||||
$provider = new DeserializeProvider($decorated, $serializer, $serializerContextBuilder);
|
||||
$request = new Request(content: '{"status":"invalid"}');
|
||||
$request->headers->set('CONTENT_TYPE', 'application/json');
|
||||
$request->attributes->set('input_format', 'json');
|
||||
|
||||
try {
|
||||
$provider->provide($operation, [], ['request' => $request]);
|
||||
$this->fail('Expected ValidationException');
|
||||
} catch (ValidationException $e) {
|
||||
$violations = $e->getConstraintViolationList();
|
||||
$this->assertCount(1, $violations);
|
||||
$this->assertSame('This value should be of type string.', $violations[0]->getMessage());
|
||||
$this->assertSame('status', $violations[0]->getPropertyPath());
|
||||
$this->assertSame((string) Type::INVALID_TYPE_ERROR, $violations[0]->getCode());
|
||||
$this->assertSame('The data must belong to a backed enumeration of type Suit.', $violations[0]->getParameters()['hint'] ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates Symfony 8.1 BackedEnumNormalizer behavior (symfony/serializer PR #62574):
|
||||
* when a value has the right type but is not a valid enum case, the exception
|
||||
* is created with expectedTypes=null and a user-friendly message listing valid values.
|
||||
*/
|
||||
#[IgnoreDeprecations]
|
||||
public function testDeserializeUsesExceptionMessageWhenExpectedTypesIsNull(): void
|
||||
{
|
||||
$operation = new Post(deserialize: true, class: \stdClass::class);
|
||||
$decorated = $this->createStub(ProviderInterface::class);
|
||||
$decorated->method('provide')->willReturn(null);
|
||||
|
||||
$ctor = new \ReflectionMethod(NotNormalizableValueException::class, '__construct');
|
||||
if ($ctor->getNumberOfParameters() <= 3) {
|
||||
$this->markTestSkipped('NotNormalizableValueException does not support extended constructor parameters.');
|
||||
}
|
||||
|
||||
$exception = new NotNormalizableValueException(
|
||||
"The data must be one of the following values: 'hearts', 'diamonds', 'clubs', 'spades'",
|
||||
0,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
'suit',
|
||||
true,
|
||||
);
|
||||
$partialException = new PartialDenormalizationException('Denormalization failed.', [$exception]);
|
||||
|
||||
$serializerContextBuilder = $this->createMock(SerializerContextBuilderInterface::class);
|
||||
$serializerContextBuilder->method('createFromRequest')->willReturn([]);
|
||||
$serializer = $this->createMock(SerializerInterface::class);
|
||||
$serializer->method('deserialize')->willThrowException($partialException);
|
||||
|
||||
$provider = new DeserializeProvider($decorated, $serializer, $serializerContextBuilder);
|
||||
$request = new Request(content: '{"suit":"invalid"}');
|
||||
$request->headers->set('CONTENT_TYPE', 'application/json');
|
||||
$request->attributes->set('input_format', 'json');
|
||||
|
||||
try {
|
||||
$provider->provide($operation, [], ['request' => $request]);
|
||||
$this->fail('Expected ValidationException');
|
||||
} catch (ValidationException $e) {
|
||||
$violations = $e->getConstraintViolationList();
|
||||
$this->assertCount(1, $violations);
|
||||
$this->assertSame("The data must be one of the following values: 'hearts', 'diamonds', 'clubs', 'spades'", $violations[0]->getMessage());
|
||||
$this->assertSame("The data must be one of the following values: 'hearts', 'diamonds', 'clubs', 'spades'", $violations[0]->getMessageTemplate());
|
||||
$this->assertSame('suit', $violations[0]->getPropertyPath());
|
||||
$this->assertSame((string) Type::INVALID_TYPE_ERROR, $violations[0]->getCode());
|
||||
}
|
||||
}
|
||||
|
||||
#[IgnoreDeprecations]
|
||||
public function testDeserializeUsesTypeMessageWhenCannotUseMessageForUser(): void
|
||||
{
|
||||
$operation = new Post(deserialize: true, class: \stdClass::class);
|
||||
$decorated = $this->createStub(ProviderInterface::class);
|
||||
$decorated->method('provide')->willReturn(null);
|
||||
|
||||
$exception = NotNormalizableValueException::createForUnexpectedDataType(
|
||||
'Internal error detail',
|
||||
42,
|
||||
['string'],
|
||||
'name',
|
||||
false,
|
||||
);
|
||||
$partialException = new PartialDenormalizationException('Denormalization failed.', [$exception]);
|
||||
|
||||
$serializerContextBuilder = $this->createMock(SerializerContextBuilderInterface::class);
|
||||
$serializerContextBuilder->method('createFromRequest')->willReturn([]);
|
||||
$serializer = $this->createMock(SerializerInterface::class);
|
||||
$serializer->method('deserialize')->willThrowException($partialException);
|
||||
|
||||
$provider = new DeserializeProvider($decorated, $serializer, $serializerContextBuilder);
|
||||
$request = new Request(content: '{"name":42}');
|
||||
$request->headers->set('CONTENT_TYPE', 'application/json');
|
||||
$request->attributes->set('input_format', 'json');
|
||||
|
||||
try {
|
||||
$provider->provide($operation, [], ['request' => $request]);
|
||||
$this->fail('Expected ValidationException');
|
||||
} catch (ValidationException $e) {
|
||||
$violations = $e->getConstraintViolationList();
|
||||
$this->assertCount(1, $violations);
|
||||
$this->assertStringContainsString('string', $violations[0]->getMessage());
|
||||
$this->assertSame('name', $violations[0]->getPropertyPath());
|
||||
$this->assertSame((string) Type::INVALID_TYPE_ERROR, $violations[0]->getCode());
|
||||
$this->assertArrayNotHasKey('hint', $violations[0]->getParameters());
|
||||
}
|
||||
}
|
||||
|
||||
public function testDeserializeDoesNotSetObjectToPopulateWhenContextIsFalse(): void
|
||||
{
|
||||
$objectToPopulate = new \stdClass();
|
||||
$serializerContext = [SerializerContextBuilderInterface::ASSIGN_OBJECT_TO_POPULATE => false];
|
||||
$operation = new Post(deserialize: true, class: \stdClass::class);
|
||||
$decorated = $this->createStub(ProviderInterface::class);
|
||||
$decorated->method('provide')->willReturn($objectToPopulate);
|
||||
|
||||
$serializerContextBuilder = $this->createMock(SerializerContextBuilderInterface::class);
|
||||
$serializerContextBuilder->method('createFromRequest')->willReturn($serializerContext);
|
||||
|
||||
$serializer = $this->createMock(SerializerInterface::class);
|
||||
$serializer->expects($this->once())->method('deserialize')->with(
|
||||
'test',
|
||||
\stdClass::class,
|
||||
'format',
|
||||
$this->callback(function (array $context) {
|
||||
$this->assertArrayNotHasKey(AbstractNormalizer::OBJECT_TO_POPULATE, $context);
|
||||
|
||||
return true;
|
||||
})
|
||||
)->willReturn(new \stdClass());
|
||||
|
||||
$provider = new DeserializeProvider($decorated, $serializer, $serializerContextBuilder);
|
||||
$request = new Request(content: 'test');
|
||||
$request->headers->set('CONTENT_TYPE', 'ok');
|
||||
$request->attributes->set('input_format', 'format');
|
||||
$provider->provide($operation, ['id' => 1], ['request' => $request]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the API Platform project.
|
||||
*
|
||||
* (c) Kévin Dunglas <dunglas@gmail.com>
|
||||
*
|
||||
* 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\EnumValidationResource;
|
||||
use ApiPlatform\Tests\SetupClassResourcesTrait;
|
||||
use Composer\InstalledVersions;
|
||||
use Composer\Semver\VersionParser;
|
||||
use PHPUnit\Framework\Attributes\IgnoreDeprecations;
|
||||
|
||||
/**
|
||||
* @see https://github.com/api-platform/core/issues/8183
|
||||
*/
|
||||
final class EnumDenormalizationValidationTest extends ApiTestCase
|
||||
{
|
||||
use SetupClassResourcesTrait;
|
||||
|
||||
protected static ?bool $alwaysBootKernel = false;
|
||||
|
||||
/**
|
||||
* @return class-string[]
|
||||
*/
|
||||
public static function getResources(): array
|
||||
{
|
||||
return [EnumValidationResource::class];
|
||||
}
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
// On symfony/property-info < 7.1, nullable backed enums resolve to a
|
||||
// single legacy Type instead of a UnionType. AbstractItemNormalizer
|
||||
// then does not re-wrap the BackedEnumNormalizer exception with the
|
||||
// enum FQCN, and DeserializeProvider cannot detect it as a BackedEnum
|
||||
// failure. The 400 response is expected on those versions.
|
||||
if (version_compare(ltrim(InstalledVersions::getPrettyVersion('symfony/property-info') ?? '0', 'v'), '7.1.0', '<')) {
|
||||
$this->markTestSkipped('Requires symfony/property-info >= 7.1 for BackedEnum type detection.');
|
||||
}
|
||||
}
|
||||
|
||||
public function testInvalidBackedEnumValueProducesValidationViolation(): void
|
||||
{
|
||||
$response = static::createClient()->request('POST', '/enum_validation_resources', [
|
||||
'headers' => ['Content-Type' => 'application/ld+json'],
|
||||
'json' => ['gender' => 'unknown'],
|
||||
]);
|
||||
|
||||
$this->assertResponseStatusCodeSame(422);
|
||||
$this->assertResponseHeaderSame('content-type', 'application/problem+json');
|
||||
|
||||
$content = $response->toArray(false);
|
||||
$this->assertArrayHasKey('violations', $content);
|
||||
$this->assertNotEmpty($content['violations']);
|
||||
|
||||
$genderViolation = $this->findViolation($content['violations'], 'gender');
|
||||
$this->assertNotNull($genderViolation, 'Expected a constraint violation on "gender" property.');
|
||||
}
|
||||
|
||||
#[IgnoreDeprecations]
|
||||
public function testInvalidBackedEnumValueWithCollectDenormalizationErrors(): void
|
||||
{
|
||||
if (InstalledVersions::satisfies(new VersionParser(), 'symfony/serializer', '>=8.1')) {
|
||||
$this->expectUserDeprecationMessage('Since symfony/serializer 8.1: The "Symfony\Component\Serializer\Exception\PartialDenormalizationException::getErrors()" method is deprecated, use "Symfony\Component\Serializer\Exception\PartialDenormalizationException::getNotNormalizableValueErrors()" instead.');
|
||||
}
|
||||
|
||||
$response = static::createClient()->request('POST', '/enum_validation_resources_collect', [
|
||||
'headers' => ['Content-Type' => 'application/ld+json'],
|
||||
'json' => ['gender' => 'unknown'],
|
||||
]);
|
||||
|
||||
$this->assertResponseStatusCodeSame(422);
|
||||
$content = $response->toArray(false);
|
||||
$this->assertNotNull($this->findViolation($content['violations'] ?? [], 'gender'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://github.com/api-platform/core/issues/8388
|
||||
*/
|
||||
#[IgnoreDeprecations]
|
||||
public function testWrongTypeForBackedEnumReportsAcceptedScalarTypes(): void
|
||||
{
|
||||
if (InstalledVersions::satisfies(new VersionParser(), 'symfony/serializer', '>=8.1')) {
|
||||
$this->expectUserDeprecationMessage('Since symfony/serializer 8.1: The "Symfony\Component\Serializer\Exception\PartialDenormalizationException::getErrors()" method is deprecated, use "Symfony\Component\Serializer\Exception\PartialDenormalizationException::getNotNormalizableValueErrors()" instead.');
|
||||
}
|
||||
|
||||
$response = static::createClient()->request('POST', '/enum_validation_resources_collect', [
|
||||
'headers' => ['Content-Type' => 'application/ld+json'],
|
||||
'json' => ['gender' => true],
|
||||
]);
|
||||
|
||||
$this->assertResponseStatusCodeSame(422);
|
||||
|
||||
$content = $response->toArray(false);
|
||||
$genderViolation = $this->findViolation($content['violations'] ?? [], 'gender');
|
||||
$this->assertNotNull($genderViolation);
|
||||
// The message must name what a JSON consumer can actually send: the enum's backing scalar
|
||||
// ("string") and, since the property is nullable, "null" — never the internal PHP type
|
||||
// "GenderTypeEnum|null", which appears nowhere in the OpenAPI schema.
|
||||
$this->assertSame('This value should be of type string|null.', $genderViolation['message']);
|
||||
$this->assertStringNotContainsString('GenderTypeEnum', $genderViolation['message']);
|
||||
$this->assertStringContainsString('enumeration case of type', $genderViolation['hint'] ?? '');
|
||||
}
|
||||
|
||||
private function findViolation(array $violations, string $propertyPath): ?array
|
||||
{
|
||||
foreach ($violations as $violation) {
|
||||
if (($violation['propertyPath'] ?? null) === $propertyPath) {
|
||||
return $violation;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the API Platform project.
|
||||
*
|
||||
* (c) Kévin Dunglas <dunglas@gmail.com>
|
||||
*
|
||||
* 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\NullOnNonNullableProperty\NullOnNonNullableResource;
|
||||
use ApiPlatform\Tests\SetupClassResourcesTrait;
|
||||
use Composer\InstalledVersions;
|
||||
use Composer\Semver\VersionParser;
|
||||
use PHPUnit\Framework\Attributes\IgnoreDeprecations;
|
||||
|
||||
/** @see https://github.com/symfony/symfony/issues/64159 */
|
||||
final class NullOnNonNullablePropertyTest extends ApiTestCase
|
||||
{
|
||||
use SetupClassResourcesTrait;
|
||||
|
||||
protected static ?bool $alwaysBootKernel = false;
|
||||
|
||||
/**
|
||||
* @return class-string[]
|
||||
*/
|
||||
public static function getResources(): array
|
||||
{
|
||||
return [NullOnNonNullableResource::class];
|
||||
}
|
||||
|
||||
public function testNullOnNonNullablePropertyReturns400(): void
|
||||
{
|
||||
$response = self::createClient()->request('POST', '/null_on_non_nullable_resources', [
|
||||
'headers' => ['Content-Type' => 'application/ld+json'],
|
||||
'json' => ['name' => null],
|
||||
]);
|
||||
|
||||
$this->assertResponseStatusCodeSame(400);
|
||||
$this->assertResponseHeaderSame('content-type', 'application/problem+json');
|
||||
$body = $response->toArray(false);
|
||||
$this->assertStringContainsString('Expected argument of type "string", "null" given at property path "name"', $body['hydra:description'] ?? $body['detail'] ?? '');
|
||||
}
|
||||
|
||||
#[IgnoreDeprecations]
|
||||
public function testNullOnNonNullablePropertyReturns422WhenCollectingErrors(): void
|
||||
{
|
||||
if (InstalledVersions::satisfies(new VersionParser(), 'symfony/serializer', '>=8.1')) {
|
||||
$this->expectUserDeprecationMessage('Since symfony/serializer 8.1: The "Symfony\Component\Serializer\Exception\PartialDenormalizationException::getErrors()" method is deprecated, use "Symfony\Component\Serializer\Exception\PartialDenormalizationException::getNotNormalizableValueErrors()" instead.');
|
||||
}
|
||||
|
||||
$response = self::createClient()->request('POST', '/null_on_non_nullable_resources_collect', [
|
||||
'headers' => ['Content-Type' => 'application/ld+json'],
|
||||
'json' => ['name' => null],
|
||||
]);
|
||||
|
||||
$this->assertResponseStatusCodeSame(422);
|
||||
$this->assertResponseHeaderSame('content-type', 'application/problem+json');
|
||||
|
||||
$content = $response->toArray(false);
|
||||
$this->assertArrayHasKey('violations', $content);
|
||||
$this->assertSame('name', $content['violations'][0]['propertyPath']);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user