From 4dd9d2b3fbdd574ec72ecc236848112332f3f7e2 Mon Sep 17 00:00:00 2001 From: David Gebler Date: Mon, 16 Jun 2025 14:18:13 +0100 Subject: [PATCH 1/7] add handling for throw_on_access_denied extra property to optionally throw AccessDeniedException when security conditions for an operation or property aren't met on denormalize --- src/Serializer/AbstractItemNormalizer.php | 16 ++ .../Tests/AbstractItemNormalizerTest.php | 169 ++++++++++++++++++ 2 files changed, 185 insertions(+) diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index acef563e3d..11070bf727 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -18,6 +18,7 @@ use ApiPlatform\Metadata\Exception\InvalidArgumentException; use ApiPlatform\Metadata\Exception\ItemNotFoundException; use ApiPlatform\Metadata\IriConverterInterface; +use ApiPlatform\Metadata\Metadata; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; @@ -26,6 +27,7 @@ use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\Metadata\Util\ClassInfoTrait; use ApiPlatform\Metadata\Util\CloneTrait; +use ApiPlatform\Symfony\Security\Exception\AccessDeniedException; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; @@ -266,14 +268,28 @@ public function denormalize(mixed $data, string $class, ?string $format = null, $options = $this->getFactoryOptions($context); $propertyNames = iterator_to_array($this->propertyNameCollectionFactory->create($resourceClass, $options)); + $operation = $context['operation'] ?? null; + $extraProperties = $operation instanceof Metadata ? + $operation->getExtraProperties() : + ($context['extra_properties'] ?? []); + + $throwOnAccessDenied = $extraProperties['throw_on_access_denied'] ?? false; + $securityMessage = $operation?->getSecurityMessage() ?? null; + // Revert attributes that aren't allowed to be changed after a post-denormalize check foreach (array_keys($data) as $attribute) { $attribute = $this->nameConverter ? $this->nameConverter->denormalize((string) $attribute) : $attribute; + $attributeMeta = $this->propertyMetadataFactory->create($resourceClass, $attribute, $options); + $attributeExtraProperties = $attributeMeta->getExtraProperties() ?? []; + $throwOnAccessDenied = (bool) ($attributeExtraProperties['throw_on_access_denied'] ?? $throwOnAccessDenied); if (!\in_array($attribute, $propertyNames, true)) { continue; } if (!$this->canAccessAttributePostDenormalize($object, $previousObject, $attribute, $context)) { + if ($throwOnAccessDenied) { + throw new AccessDeniedException($securityMessage ?? 'Access denied'); + } if (null !== $previousObject) { $this->setValue($object, $attribute, $this->propertyAccessor->getValue($previousObject, $attribute)); } else { diff --git a/src/Serializer/Tests/AbstractItemNormalizerTest.php b/src/Serializer/Tests/AbstractItemNormalizerTest.php index f3263e8b95..3524f7e499 100644 --- a/src/Serializer/Tests/AbstractItemNormalizerTest.php +++ b/src/Serializer/Tests/AbstractItemNormalizerTest.php @@ -21,6 +21,7 @@ use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Operations; +use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Property\PropertyNameCollection; @@ -40,6 +41,7 @@ use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\PropertyCollectionIriOnlyRelation; use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\RelatedDummy; use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\SecuredDummy; +use ApiPlatform\Symfony\Security\Exception\AccessDeniedException; use Doctrine\Common\Collections\ArrayCollection; use PHPUnit\Framework\TestCase; use Prophecy\Argument; @@ -306,6 +308,173 @@ public function testNormalizePropertyAsIriWithUriTemplate(): void ])); } + public function testDenormalizeWithSecuredPropertyAndThrowOnAccessDeniedExtraPropertyInAttributeMetaThrowsAccessDeniedExceptionWithSecurityMessage(): void + { + $data = [ + 'title' => 'foo', + 'adminOnlyProperty' => 'secret', + ]; + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(SecuredDummy::class, [])->willReturn(new PropertyNameCollection(['title', 'adminOnlyProperty'])); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'adminOnlyProperty', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)->withSecurityPostDenormalize('is_granted(\'ROLE_ADMIN\')')->withExtraProperties(['throw_on_access_denied' => true])); + } else { + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', [])->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'adminOnlyProperty', [])->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(true)->withSecurityPostDenormalize('is_granted(\'ROLE_ADMIN\')')->withExtraProperties(['throw_on_access_denied' => true])); + } + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass(null, SecuredDummy::class)->willReturn(SecuredDummy::class); + $resourceClassResolverProphecy->isResourceClass(SecuredDummy::class)->willReturn(true); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(NormalizerInterface::class); + + $resourceAccessChecker = $this->prophesize(ResourceAccessCheckerInterface::class); + $resourceAccessChecker->isGranted( + SecuredDummy::class, + 'is_granted(\'ROLE_ADMIN\')', + Argument::that(function (array $context) { + return array_key_exists('property', $context) + && array_key_exists('object', $context) + && array_key_exists('previous_object', $context) + && $context['property'] === 'adminOnlyProperty' + && $context['previous_object'] === null + && $context['object'] instanceof SecuredDummy; + }) + )->willReturn(false); + + $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), null, null, [], null, $resourceAccessChecker->reveal()) extends AbstractItemNormalizer {}; + $normalizer->setSerializer($serializerProphecy->reveal()); + + $this->expectException(AccessDeniedException::class); + $this->expectExceptionMessage('Access denied'); + + $normalizer->denormalize($data, SecuredDummy::class); + } + + public function testDenormalizeWithSecuredPropertyAndThrowOnAccessDeniedExtraPropertyInOperationAndSecurityMessageInOperationThrowsAccessDeniedExceptionWithSecurityMessage(): void + { + $data = [ + 'title' => 'foo', + 'adminOnlyProperty' => 'secret', + ]; + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(SecuredDummy::class, [])->willReturn(new PropertyNameCollection(['title', 'adminOnlyProperty'])); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'adminOnlyProperty', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)->withSecurityPostDenormalize('is_granted(\'ROLE_ADMIN\')')); + } else { + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', [])->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'adminOnlyProperty', [])->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(true)->withSecurityPostDenormalize('is_granted(\'ROLE_ADMIN\')')); + } + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass(null, SecuredDummy::class)->willReturn(SecuredDummy::class); + $resourceClassResolverProphecy->isResourceClass(SecuredDummy::class)->willReturn(true); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(NormalizerInterface::class); + + $resourceAccessChecker = $this->prophesize(ResourceAccessCheckerInterface::class); + $resourceAccessChecker->isGranted( + SecuredDummy::class, + 'is_granted(\'ROLE_ADMIN\')', + Argument::that(function (array $context) { + return array_key_exists('property', $context) + && array_key_exists('object', $context) + && array_key_exists('previous_object', $context) + && $context['property'] === 'adminOnlyProperty' + && $context['previous_object'] === null + && $context['object'] instanceof SecuredDummy; + }) + )->willReturn(false); + + $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), null, null, [], null, $resourceAccessChecker->reveal()) extends AbstractItemNormalizer {}; + $normalizer->setSerializer($serializerProphecy->reveal()); + + $this->expectException(AccessDeniedException::class); + $this->expectExceptionMessage('Custom access denied message'); + + $operation = new Patch(securityMessage: 'Custom access denied message', extraProperties: ['throw_on_access_denied' => true]); + + $normalizer->denormalize($data, SecuredDummy::class, 'json', [ + 'operation' => $operation + ]); + } + + public function testDenormalizeWithSecuredPropertyAndThrowOnAccessDeniedExtraPropertyInOperationThrowsAccessDeniedException(): void + { + $data = [ + 'title' => 'foo', + 'adminOnlyProperty' => 'secret', + ]; + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(SecuredDummy::class, [])->willReturn(new PropertyNameCollection(['title', 'adminOnlyProperty'])); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + + if (!method_exists(PropertyInfoExtractor::class, 'getType')) { + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'adminOnlyProperty', [])->willReturn((new ApiProperty())->withBuiltinTypes([new LegacyType(LegacyType::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)->withSecurityPostDenormalize('is_granted(\'ROLE_ADMIN\')')); + } else { + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', [])->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'adminOnlyProperty', [])->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('')->withReadable(true)->withSecurityPostDenormalize('is_granted(\'ROLE_ADMIN\')')); + } + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass(null, SecuredDummy::class)->willReturn(SecuredDummy::class); + $resourceClassResolverProphecy->isResourceClass(SecuredDummy::class)->willReturn(true); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(NormalizerInterface::class); + + $resourceAccessChecker = $this->prophesize(ResourceAccessCheckerInterface::class); + $resourceAccessChecker->isGranted( + SecuredDummy::class, + 'is_granted(\'ROLE_ADMIN\')', + Argument::that(function (array $context) { + return array_key_exists('property', $context) + && array_key_exists('object', $context) + && array_key_exists('previous_object', $context) + && $context['property'] === 'adminOnlyProperty' + && $context['previous_object'] === null + && $context['object'] instanceof SecuredDummy; + }) + )->willReturn(false); + + $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), null, null, [], null, $resourceAccessChecker->reveal()) extends AbstractItemNormalizer {}; + $normalizer->setSerializer($serializerProphecy->reveal()); + + $this->expectException(AccessDeniedException::class); + $this->expectExceptionMessage('Access denied'); + + $operation = new Patch(extraProperties: ['throw_on_access_denied' => true]); + + $normalizer->denormalize($data, SecuredDummy::class, 'json', [ + 'operation' => $operation + ]); + } + public function testDenormalizeWithSecuredProperty(): void { $data = [ From 3e8dab20b1585884ebbde8f0bab73618624c73f4 Mon Sep 17 00:00:00 2001 From: David Gebler Date: Mon, 16 Jun 2025 15:00:45 +0100 Subject: [PATCH 2/7] lint fix --- .../Tests/AbstractItemNormalizerTest.php | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Serializer/Tests/AbstractItemNormalizerTest.php b/src/Serializer/Tests/AbstractItemNormalizerTest.php index 3524f7e499..8ef542015f 100644 --- a/src/Serializer/Tests/AbstractItemNormalizerTest.php +++ b/src/Serializer/Tests/AbstractItemNormalizerTest.php @@ -343,9 +343,9 @@ public function testDenormalizeWithSecuredPropertyAndThrowOnAccessDeniedExtraPro SecuredDummy::class, 'is_granted(\'ROLE_ADMIN\')', Argument::that(function (array $context) { - return array_key_exists('property', $context) - && array_key_exists('object', $context) - && array_key_exists('previous_object', $context) + return \array_key_exists('property', $context) + && \array_key_exists('object', $context) + && \array_key_exists('previous_object', $context) && $context['property'] === 'adminOnlyProperty' && $context['previous_object'] === null && $context['object'] instanceof SecuredDummy; @@ -396,9 +396,9 @@ public function testDenormalizeWithSecuredPropertyAndThrowOnAccessDeniedExtraPro SecuredDummy::class, 'is_granted(\'ROLE_ADMIN\')', Argument::that(function (array $context) { - return array_key_exists('property', $context) - && array_key_exists('object', $context) - && array_key_exists('previous_object', $context) + return \array_key_exists('property', $context) + && \array_key_exists('object', $context) + && \array_key_exists('previous_object', $context) && $context['property'] === 'adminOnlyProperty' && $context['previous_object'] === null && $context['object'] instanceof SecuredDummy; @@ -414,7 +414,7 @@ public function testDenormalizeWithSecuredPropertyAndThrowOnAccessDeniedExtraPro $operation = new Patch(securityMessage: 'Custom access denied message', extraProperties: ['throw_on_access_denied' => true]); $normalizer->denormalize($data, SecuredDummy::class, 'json', [ - 'operation' => $operation + 'operation' => $operation, ]); } @@ -453,9 +453,9 @@ public function testDenormalizeWithSecuredPropertyAndThrowOnAccessDeniedExtraPro SecuredDummy::class, 'is_granted(\'ROLE_ADMIN\')', Argument::that(function (array $context) { - return array_key_exists('property', $context) - && array_key_exists('object', $context) - && array_key_exists('previous_object', $context) + return \array_key_exists('property', $context) + && \array_key_exists('object', $context) + && \array_key_exists('previous_object', $context) && $context['property'] === 'adminOnlyProperty' && $context['previous_object'] === null && $context['object'] instanceof SecuredDummy; @@ -471,7 +471,7 @@ public function testDenormalizeWithSecuredPropertyAndThrowOnAccessDeniedExtraPro $operation = new Patch(extraProperties: ['throw_on_access_denied' => true]); $normalizer->denormalize($data, SecuredDummy::class, 'json', [ - 'operation' => $operation + 'operation' => $operation, ]); } From c2e46c70c14e9863d76446b37be82abf5628dda7 Mon Sep 17 00:00:00 2001 From: David Gebler Date: Mon, 16 Jun 2025 15:05:24 +0100 Subject: [PATCH 3/7] lint fix --- src/Serializer/Tests/AbstractItemNormalizerTest.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Serializer/Tests/AbstractItemNormalizerTest.php b/src/Serializer/Tests/AbstractItemNormalizerTest.php index 8ef542015f..77f04ad75d 100644 --- a/src/Serializer/Tests/AbstractItemNormalizerTest.php +++ b/src/Serializer/Tests/AbstractItemNormalizerTest.php @@ -346,8 +346,8 @@ public function testDenormalizeWithSecuredPropertyAndThrowOnAccessDeniedExtraPro return \array_key_exists('property', $context) && \array_key_exists('object', $context) && \array_key_exists('previous_object', $context) - && $context['property'] === 'adminOnlyProperty' - && $context['previous_object'] === null + && 'adminOnlyProperty' === $context['property'] + && null === $context['previous_object'] && $context['object'] instanceof SecuredDummy; }) )->willReturn(false); @@ -399,8 +399,8 @@ public function testDenormalizeWithSecuredPropertyAndThrowOnAccessDeniedExtraPro return \array_key_exists('property', $context) && \array_key_exists('object', $context) && \array_key_exists('previous_object', $context) - && $context['property'] === 'adminOnlyProperty' - && $context['previous_object'] === null + && 'adminOnlyProperty' === $context['property'] + && null === $context['previous_object'] && $context['object'] instanceof SecuredDummy; }) )->willReturn(false); @@ -456,8 +456,8 @@ public function testDenormalizeWithSecuredPropertyAndThrowOnAccessDeniedExtraPro return \array_key_exists('property', $context) && \array_key_exists('object', $context) && \array_key_exists('previous_object', $context) - && $context['property'] === 'adminOnlyProperty' - && $context['previous_object'] === null + && 'adminOnlyProperty' === $context['property'] + && null === $context['previous_object'] && $context['object'] instanceof SecuredDummy; }) )->willReturn(false); From 07c093d0f54774aa441160d59b66eadc6b7155bb Mon Sep 17 00:00:00 2001 From: David Gebler Date: Fri, 20 Jun 2025 09:58:36 +0100 Subject: [PATCH 4/7] replace symfony accessdeniedexception with metadata accessdeniedexception --- src/Serializer/AbstractItemNormalizer.php | 2 +- src/Serializer/Tests/AbstractItemNormalizerTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index 09bcced96a..7502c4f544 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -27,7 +27,7 @@ use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\Metadata\Util\ClassInfoTrait; use ApiPlatform\Metadata\Util\CloneTrait; -use ApiPlatform\Symfony\Security\Exception\AccessDeniedException; +use ApiPlatform\Metadata\Exception\AccessDeniedException; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; diff --git a/src/Serializer/Tests/AbstractItemNormalizerTest.php b/src/Serializer/Tests/AbstractItemNormalizerTest.php index 035b1e5b39..f6cf75702c 100644 --- a/src/Serializer/Tests/AbstractItemNormalizerTest.php +++ b/src/Serializer/Tests/AbstractItemNormalizerTest.php @@ -41,7 +41,7 @@ use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\PropertyCollectionIriOnlyRelation; use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\RelatedDummy; use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\SecuredDummy; -use ApiPlatform\Symfony\Security\Exception\AccessDeniedException; +use ApiPlatform\Metadata\Exception\AccessDeniedException; use Doctrine\Common\Collections\ArrayCollection; use PHPUnit\Framework\TestCase; use Prophecy\Argument; From 702203fd95066ea9fe9e644f0f6b213433d85e6d Mon Sep 17 00:00:00 2001 From: David Gebler Date: Fri, 20 Jun 2025 10:03:19 +0100 Subject: [PATCH 5/7] lint fix --- src/Serializer/Tests/AbstractItemNormalizerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Serializer/Tests/AbstractItemNormalizerTest.php b/src/Serializer/Tests/AbstractItemNormalizerTest.php index f6cf75702c..422e56fd25 100644 --- a/src/Serializer/Tests/AbstractItemNormalizerTest.php +++ b/src/Serializer/Tests/AbstractItemNormalizerTest.php @@ -15,6 +15,7 @@ use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Exception\AccessDeniedException; use ApiPlatform\Metadata\Exception\InvalidArgumentException; use ApiPlatform\Metadata\Exception\ItemNotFoundException; use ApiPlatform\Metadata\Get; @@ -41,7 +42,6 @@ use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\PropertyCollectionIriOnlyRelation; use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\RelatedDummy; use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\SecuredDummy; -use ApiPlatform\Metadata\Exception\AccessDeniedException; use Doctrine\Common\Collections\ArrayCollection; use PHPUnit\Framework\TestCase; use Prophecy\Argument; From 8b8024d5504ac8cd84afbdd74c357a10e934304f Mon Sep 17 00:00:00 2001 From: David Gebler Date: Fri, 20 Jun 2025 10:23:39 +0100 Subject: [PATCH 6/7] update prophecy in normalizer test --- src/Serializer/Tests/ItemNormalizerTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Serializer/Tests/ItemNormalizerTest.php b/src/Serializer/Tests/ItemNormalizerTest.php index 6028b7d68a..d5232dbfcb 100644 --- a/src/Serializer/Tests/ItemNormalizerTest.php +++ b/src/Serializer/Tests/ItemNormalizerTest.php @@ -323,6 +323,7 @@ public function testDenormalizeWithWrongId(): void $propertyMetadata = (new ApiProperty())->withReadable(true)->withWritable(true); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn($propertyMetadata)->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'id', [])->willReturn($propertyMetadata)->shouldBeCalled(); $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); $iriConverterProphecy->getResourceFromIri('fail', $context + ['fetch_data' => true])->willThrow(new InvalidArgumentException()); From 9ee746468e1d3006a48ef50b55c6f154fa9aaae2 Mon Sep 17 00:00:00 2001 From: David Gebler Date: Fri, 20 Jun 2025 10:40:02 +0100 Subject: [PATCH 7/7] lint fix --- src/Serializer/AbstractItemNormalizer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index 7502c4f544..852972b931 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -15,6 +15,7 @@ use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\CollectionOperationInterface; +use ApiPlatform\Metadata\Exception\AccessDeniedException; use ApiPlatform\Metadata\Exception\InvalidArgumentException; use ApiPlatform\Metadata\Exception\ItemNotFoundException; use ApiPlatform\Metadata\IriConverterInterface; @@ -27,7 +28,6 @@ use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\Metadata\Util\ClassInfoTrait; use ApiPlatform\Metadata\Util\CloneTrait; -use ApiPlatform\Metadata\Exception\AccessDeniedException; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface;