From d5b0759ef9c61d6427a1abc41f993ec1d4960e2d Mon Sep 17 00:00:00 2001 From: soyuka Date: Thu, 10 Aug 2023 11:41:02 +0200 Subject: [PATCH] chore(serializer): api-platform/serializer --- .gitignore | 3 + AbstractItemNormalizer.php | 9 +- Filter/FilterInterface.php | 2 +- ItemNormalizer.php | 11 +- LICENSE | 21 + README.md | 3 + SerializerContextBuilder.php | 4 +- SerializerFilterContextBuilder.php | 4 +- Tests/AbstractItemNormalizerTest.php | 1442 +++++++++++++++++ Tests/Filter/GroupFilterTest.php | 150 ++ Tests/Filter/PropertyFilterTest.php | 245 +++ .../Fixtures/ApiResource/DtoWithNullValue.php | 26 + Tests/Fixtures/ApiResource/Dummy.php | 250 +++ Tests/Fixtures/ApiResource/DummyGroup.php | 57 + Tests/Fixtures/ApiResource/DummyProperty.php | 65 + .../ApiResource/DummyTableInheritance.php | 60 + .../DummyTableInheritanceChild.php | 37 + .../DummyTableInheritanceRelated.php | 71 + .../ApiResource/NonCloneableDummy.php | 63 + Tests/Fixtures/ApiResource/RelatedDummy.php | 134 ++ Tests/Fixtures/ApiResource/SecuredDummy.php | 184 +++ .../NameConverter/CustomConverter.php | 33 + Tests/ItemNormalizerTest.php | 357 ++++ Tests/JsonEncoderTest.php | 68 + Tests/ResourceListTest.php | 32 + Tests/SerializerContextBuilderTest.php | 131 ++ Tests/SerializerFilterContextBuilderTest.php | 189 +++ composer.json | 78 + phpunit.xml.dist | 30 + 29 files changed, 3745 insertions(+), 14 deletions(-) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 Tests/AbstractItemNormalizerTest.php create mode 100644 Tests/Filter/GroupFilterTest.php create mode 100644 Tests/Filter/PropertyFilterTest.php create mode 100644 Tests/Fixtures/ApiResource/DtoWithNullValue.php create mode 100644 Tests/Fixtures/ApiResource/Dummy.php create mode 100644 Tests/Fixtures/ApiResource/DummyGroup.php create mode 100644 Tests/Fixtures/ApiResource/DummyProperty.php create mode 100644 Tests/Fixtures/ApiResource/DummyTableInheritance.php create mode 100644 Tests/Fixtures/ApiResource/DummyTableInheritanceChild.php create mode 100644 Tests/Fixtures/ApiResource/DummyTableInheritanceRelated.php create mode 100644 Tests/Fixtures/ApiResource/NonCloneableDummy.php create mode 100644 Tests/Fixtures/ApiResource/RelatedDummy.php create mode 100644 Tests/Fixtures/ApiResource/SecuredDummy.php create mode 100644 Tests/Fixtures/Serializer/NameConverter/CustomConverter.php create mode 100644 Tests/ItemNormalizerTest.php create mode 100644 Tests/JsonEncoderTest.php create mode 100644 Tests/ResourceListTest.php create mode 100644 Tests/SerializerContextBuilderTest.php create mode 100644 Tests/SerializerFilterContextBuilderTest.php create mode 100644 composer.json create mode 100644 phpunit.xml.dist diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb0a8e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/composer.lock +/vendor +/.phpunit.result.cache diff --git a/AbstractItemNormalizer.php b/AbstractItemNormalizer.php index a0a31c7..1420153 100644 --- a/AbstractItemNormalizer.php +++ b/AbstractItemNormalizer.php @@ -13,20 +13,20 @@ namespace ApiPlatform\Serializer; -use ApiPlatform\Api\IriConverterInterface; -use ApiPlatform\Api\ResourceClassResolverInterface; -use ApiPlatform\Api\UrlGeneratorInterface; use ApiPlatform\Exception\InvalidArgumentException; use ApiPlatform\Exception\ItemNotFoundException; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\CollectionOperationInterface; use ApiPlatform\Metadata\Exception\OperationNotFoundException; +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\ResourceClassResolverInterface; +use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\Metadata\Util\ClassInfoTrait; +use ApiPlatform\Metadata\Util\CloneTrait; use ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface; -use ApiPlatform\Util\CloneTrait; use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; @@ -43,6 +43,7 @@ use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\Serializer; /** * Base item normalizer. diff --git a/Filter/FilterInterface.php b/Filter/FilterInterface.php index 7093ad2..2286887 100644 --- a/Filter/FilterInterface.php +++ b/Filter/FilterInterface.php @@ -13,7 +13,7 @@ namespace ApiPlatform\Serializer\Filter; -use ApiPlatform\Api\FilterInterface as BaseFilterInterface; +use ApiPlatform\Metadata\FilterInterface as BaseFilterInterface; use Symfony\Component\HttpFoundation\Request; /** diff --git a/ItemNormalizer.php b/ItemNormalizer.php index e8c719d..e93d5c7 100644 --- a/ItemNormalizer.php +++ b/ItemNormalizer.php @@ -13,14 +13,15 @@ namespace ApiPlatform\Serializer; -use ApiPlatform\Api\IriConverterInterface; -use ApiPlatform\Api\ResourceClassResolverInterface; -use ApiPlatform\Api\UrlGeneratorInterface; -use ApiPlatform\Exception\InvalidArgumentException; +use ApiPlatform\Exception\InvalidArgumentException as LegacyInvalidArgumentException; +use ApiPlatform\Metadata\Exception\InvalidArgumentException; +use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Link; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface; use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; @@ -75,7 +76,7 @@ private function updateObjectToPopulate(array $data, array &$context): void { try { $context[self::OBJECT_TO_POPULATE] = $this->iriConverter->getResourceFromIri((string) $data['id'], $context + ['fetch_data' => true]); - } catch (InvalidArgumentException) { + } catch (LegacyInvalidArgumentException|InvalidArgumentException) { $operation = $this->resourceMetadataCollectionFactory->create($context['resource_class'])->getOperation(); $uriVariables = $this->getContextUriVariables($data, $operation, $context); $iri = $this->iriConverter->getIriFromResource($context['resource_class'], UrlGeneratorInterface::ABS_PATH, $operation, ['uri_variables' => $uriVariables]); diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1ca98ee --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT license + +Copyright (c) 2015-present Kévin Dunglas + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..10b0b8c --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# API Platform - GraphQL + +Build GraphQL API endpoints diff --git a/SerializerContextBuilder.php b/SerializerContextBuilder.php index 2fa07c3..1ed459f 100644 --- a/SerializerContextBuilder.php +++ b/SerializerContextBuilder.php @@ -13,9 +13,9 @@ namespace ApiPlatform\Serializer; -use ApiPlatform\Exception\RuntimeException; +use ApiPlatform\Metadata\Exception\RuntimeException; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Util\RequestAttributesExtractor; +use ApiPlatform\Symfony\Util\RequestAttributesExtractor; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Serializer\Encoder\CsvEncoder; use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; diff --git a/SerializerFilterContextBuilder.php b/SerializerFilterContextBuilder.php index cf5246a..452fe39 100644 --- a/SerializerFilterContextBuilder.php +++ b/SerializerFilterContextBuilder.php @@ -13,10 +13,10 @@ namespace ApiPlatform\Serializer; -use ApiPlatform\Exception\RuntimeException; +use ApiPlatform\Metadata\Exception\RuntimeException; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Serializer\Filter\FilterInterface; -use ApiPlatform\Util\RequestAttributesExtractor; +use ApiPlatform\Symfony\Util\RequestAttributesExtractor; use Psr\Container\ContainerInterface; use Symfony\Component\HttpFoundation\Request; diff --git a/Tests/AbstractItemNormalizerTest.php b/Tests/AbstractItemNormalizerTest.php new file mode 100644 index 0000000..8bfea62 --- /dev/null +++ b/Tests/AbstractItemNormalizerTest.php @@ -0,0 +1,1442 @@ + + * + * 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\Serializer\Tests; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\IriConverterInterface; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Metadata\Property\PropertyNameCollection; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Serializer\AbstractItemNormalizer; +use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\DtoWithNullValue; +use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\Dummy; +use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\DummyTableInheritance; +use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\DummyTableInheritanceChild; +use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\DummyTableInheritanceRelated; +use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\NonCloneableDummy; +use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\RelatedDummy; +use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\SecuredDummy; +use ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface; +use Doctrine\Common\Collections\ArrayCollection; +use PHPUnit\Framework\TestCase; +use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\Serializer\Exception\NotNormalizableValueException; +use Symfony\Component\Serializer\Exception\UnexpectedValueException; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\Serializer; +use Symfony\Component\Serializer\SerializerInterface; + +/** + * @author Amrouche Hamza + * @author Kévin Dunglas + */ +class AbstractItemNormalizerTest extends TestCase +{ + use ExpectDeprecationTrait; + use ProphecyTrait; + + /** + * @group legacy + */ + public function testSupportNormalizationAndSupportDenormalization(): void + { + $std = new \stdClass(); + $dummy = new Dummy(); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + $resourceClassResolverProphecy->isResourceClass(\stdClass::class)->willReturn(false); + + $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $propertyAccessorProphecy->reveal(), + null, + null, + [], + null, + null, + ]); + + $this->assertTrue($normalizer->supportsNormalization($dummy)); + $this->assertFalse($normalizer->supportsNormalization($std)); + $this->assertTrue($normalizer->supportsDenormalization($dummy, Dummy::class)); + $this->assertFalse($normalizer->supportsDenormalization($std, \stdClass::class)); + $this->assertFalse($normalizer->supportsNormalization([])); + $this->assertSame(['object' => true], $normalizer->getSupportedTypes('any')); + + if (!method_exists(Serializer::class, 'getSupportedTypes')) { + $this->assertTrue($normalizer->hasCacheableSupportsMethod()); + } + } + + public function testNormalize(): 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, [])->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', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'alias', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummyType])->withDescription('')->withReadable(true)->withWritable(false)->withReadableLink(false)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', [])->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' => [], + 'ignored_attributes' => ['alias'], + ])); + } + + public function testNormalizeWithSecuredProperty(): void + { + $dummy = new SecuredDummy(); + $dummy->setTitle('myPublicTitle'); + $dummy->setAdminOnlyProperty('secret'); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(SecuredDummy::class, [])->willReturn(new PropertyNameCollection(['title', 'adminOnlyProperty'])); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)); + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'adminOnlyProperty', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)->withSecurity('is_granted(\'ROLE_ADMIN\')')); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getIriFromResource($dummy, Argument::cetera())->willReturn('/secured_dummies/1'); + + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + $propertyAccessorProphecy->getValue($dummy, 'title')->willReturn('myPublicTitle'); + $propertyAccessorProphecy->getValue($dummy, 'adminOnlyProperty')->willReturn('secret'); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass($dummy, null)->willReturn(SecuredDummy::class); + $resourceClassResolverProphecy->getResourceClass(null, SecuredDummy::class)->willReturn(SecuredDummy::class); + $resourceClassResolverProphecy->isResourceClass(SecuredDummy::class)->willReturn(true); + + $resourceAccessChecker = $this->prophesize(ResourceAccessCheckerInterface::class); + $resourceAccessChecker->isGranted( + SecuredDummy::class, + 'is_granted(\'ROLE_ADMIN\')', + ['object' => $dummy] + )->willReturn(false); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(NormalizerInterface::class); + $serializerProphecy->normalize('myPublicTitle', null, Argument::type('array'))->willReturn('myPublicTitle'); + + $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $propertyAccessorProphecy->reveal(), + null, + null, + [], + null, + $resourceAccessChecker->reveal(), + ]); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $expected = [ + 'title' => 'myPublicTitle', + ]; + $this->assertSame($expected, $normalizer->normalize($dummy, null, [ + 'resources' => [], + ])); + } + + public function testDenormalizeWithSecuredProperty(): 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); + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'adminOnlyProperty', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)->withSecurity('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\')', + ['object' => null] + )->willReturn(false); + + $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $propertyAccessorProphecy->reveal(), + null, + null, + [], + null, + $resourceAccessChecker->reveal(), + ]); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $actual = $normalizer->denormalize($data, SecuredDummy::class); + + $this->assertInstanceOf(SecuredDummy::class, $actual); + + $propertyAccessorProphecy->setValue($actual, 'title', 'foo')->shouldHaveBeenCalled(); + $propertyAccessorProphecy->setValue($actual, 'adminOnlyProperty', 'secret')->shouldNotHaveBeenCalled(); + } + + public function testDenormalizeCreateWithDeniedPostDenormalizeSecuredProperty(): void + { + $data = [ + 'title' => 'foo', + 'ownerOnlyProperty' => 'should not be set', + ]; + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(SecuredDummy::class, [])->willReturn(new PropertyNameCollection(['title', 'ownerOnlyProperty'])); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'ownerOnlyProperty', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)->withWritable(true)->withSecurityPostDenormalize('false')->withDefault('')); + + $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, + 'false', + Argument::any() + )->willReturn(false); + + $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $propertyAccessorProphecy->reveal(), + null, + null, + [], + null, + $resourceAccessChecker->reveal(), + ]); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $actual = $normalizer->denormalize($data, SecuredDummy::class); + + $this->assertInstanceOf(SecuredDummy::class, $actual); + + $propertyAccessorProphecy->setValue($actual, 'title', 'foo')->shouldHaveBeenCalled(); + $propertyAccessorProphecy->setValue($actual, 'ownerOnlyProperty', 'should not be set')->shouldHaveBeenCalled(); + $propertyAccessorProphecy->setValue($actual, 'ownerOnlyProperty', '')->shouldHaveBeenCalled(); + } + + public function testDenormalizeUpdateWithSecuredProperty(): void + { + $dummy = new SecuredDummy(); + + $data = [ + 'title' => 'foo', + 'ownerOnlyProperty' => 'secret', + ]; + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(SecuredDummy::class, [])->willReturn(new PropertyNameCollection(['title', 'ownerOnlyProperty'])); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'ownerOnlyProperty', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)->withWritable(true)->withSecurity('true')); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass($dummy, SecuredDummy::class)->willReturn(SecuredDummy::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, + 'true', + ['object' => null] + )->willReturn(true); + $resourceAccessChecker->isGranted( + SecuredDummy::class, + 'true', + ['object' => $dummy] + )->willReturn(true); + + $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $propertyAccessorProphecy->reveal(), + null, + null, + [], + null, + $resourceAccessChecker->reveal(), + ]); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $context = [AbstractItemNormalizer::OBJECT_TO_POPULATE => $dummy]; + $actual = $normalizer->denormalize($data, SecuredDummy::class, null, $context); + + $this->assertInstanceOf(SecuredDummy::class, $actual); + + $propertyAccessorProphecy->setValue($actual, 'title', 'foo')->shouldHaveBeenCalled(); + $propertyAccessorProphecy->setValue($actual, 'ownerOnlyProperty', 'secret')->shouldHaveBeenCalled(); + } + + public function testDenormalizeUpdateWithDeniedSecuredProperty(): void + { + $dummy = new SecuredDummy(); + $dummy->setOwnerOnlyProperty('secret'); + + $data = [ + 'title' => 'foo', + 'ownerOnlyProperty' => 'should not be set', + ]; + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(SecuredDummy::class, [])->willReturn(new PropertyNameCollection(['title', 'ownerOnlyProperty'])); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'ownerOnlyProperty', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)->withWritable(true)->withSecurity('false')); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass(null, SecuredDummy::class)->willReturn(SecuredDummy::class); + $resourceClassResolverProphecy->getResourceClass($dummy, 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, + 'false', + ['object' => null] + )->willReturn(false); + $resourceAccessChecker->isGranted( + SecuredDummy::class, + 'false', + ['object' => $dummy] + )->willReturn(false); + + $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $propertyAccessorProphecy->reveal(), + null, + null, + [], + null, + $resourceAccessChecker->reveal(), + ]); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $context = [AbstractItemNormalizer::OBJECT_TO_POPULATE => $dummy]; + $actual = $normalizer->denormalize($data, SecuredDummy::class, null, $context); + + $this->assertInstanceOf(SecuredDummy::class, $actual); + + $propertyAccessorProphecy->setValue($actual, 'title', 'foo')->shouldHaveBeenCalled(); + $propertyAccessorProphecy->setValue($actual, 'ownerOnlyProperty', 'should not be set')->shouldNotHaveBeenCalled(); + } + + public function testDenormalizeUpdateWithDeniedPostDenormalizeSecuredProperty(): void + { + $dummy = new SecuredDummy(); + $dummy->setOwnerOnlyProperty('secret'); + + $data = [ + 'title' => 'foo', + 'ownerOnlyProperty' => 'should not be set', + ]; + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(SecuredDummy::class, [])->willReturn(new PropertyNameCollection(['title', 'ownerOnlyProperty'])); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'title', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(SecuredDummy::class, 'ownerOnlyProperty', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(true)->withWritable(true)->withSecurityPostDenormalize('false')); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + $propertyAccessorProphecy->getValue($dummy, 'ownerOnlyProperty')->willReturn('secret'); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass($dummy, SecuredDummy::class)->willReturn(SecuredDummy::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, + 'false', + ['object' => $dummy, 'previous_object' => $dummy] + )->willReturn(false); + + $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $propertyAccessorProphecy->reveal(), + null, + null, + [], + null, + $resourceAccessChecker->reveal(), + ]); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $context = [AbstractItemNormalizer::OBJECT_TO_POPULATE => $dummy]; + $actual = $normalizer->denormalize($data, SecuredDummy::class, null, $context); + + $this->assertInstanceOf(SecuredDummy::class, $actual); + + $propertyAccessorProphecy->setValue($actual, 'title', 'foo')->shouldHaveBeenCalled(); + $propertyAccessorProphecy->setValue($actual, 'ownerOnlyProperty', 'should not be set')->shouldHaveBeenCalled(); + $propertyAccessorProphecy->setValue($actual, 'ownerOnlyProperty', 'secret')->shouldHaveBeenCalled(); + } + + public function testNormalizeReadableLinks(): void + { + $relatedDummy = new RelatedDummy(); + + $dummy = new Dummy(); + $dummy->setRelatedDummy($relatedDummy); + $dummy->relatedDummies->add(new RelatedDummy()); + + $relatedDummies = new ArrayCollection([$relatedDummy]); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn(new PropertyNameCollection(['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, 'relatedDummy', [])->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummyType])->withReadable(true)->withWritable(false)->withReadableLink(true)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', [])->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummiesType])->withReadable(true)->withWritable(false)->withReadableLink(true)); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getIriFromResource($dummy, Argument::cetera())->willReturn('/dummies/1'); + + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + $propertyAccessorProphecy->getValue($dummy, 'relatedDummy')->willReturn($relatedDummy); + $propertyAccessorProphecy->getValue($dummy, 'relatedDummies')->willReturn($relatedDummies); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass($dummy, null)->willReturn(Dummy::class); + $resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->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); + $relatedDummyChildContext = Argument::allOf( + Argument::type('array'), + Argument::withEntry('resource_class', RelatedDummy::class), + Argument::not(Argument::withKey('iri')), + Argument::not(Argument::withKey('force_resource_class')) + ); + $serializerProphecy->normalize($relatedDummy, null, $relatedDummyChildContext)->willReturn(['foo' => 'hello']); + $serializerProphecy->normalize(['foo' => 'hello'], null, Argument::type('array'))->willReturn(['foo' => 'hello']); + $serializerProphecy->normalize([['foo' => 'hello']], null, Argument::type('array'))->willReturn([['foo' => 'hello']]); + + $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $propertyAccessorProphecy->reveal(), + null, + null, + [], + null, + null, + ]); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $expected = [ + 'relatedDummy' => ['foo' => 'hello'], + 'relatedDummies' => [['foo' => 'hello']], + ]; + $this->assertSame($expected, $normalizer->normalize($dummy, null, [ + 'resources' => [], + 'force_resource_class' => Dummy::class, + ])); + } + + public function testNormalizePolymorphicRelations(): void + { + $concreteDummy = new DummyTableInheritanceChild(); + + $dummy = new DummyTableInheritanceRelated(); + $dummy->addChild($concreteDummy); + + $abstractDummies = new ArrayCollection([$concreteDummy]); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(DummyTableInheritanceRelated::class, [])->willReturn(new PropertyNameCollection(['children'])); + + $abstractDummyType = new Type(Type::BUILTIN_TYPE_OBJECT, false, DummyTableInheritance::class); + $abstractDummiesType = new Type(Type::BUILTIN_TYPE_OBJECT, false, ArrayCollection::class, true, new Type(Type::BUILTIN_TYPE_INT), $abstractDummyType); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(DummyTableInheritanceRelated::class, 'children', [])->willReturn((new ApiProperty())->withBuiltinTypes([$abstractDummiesType])->withReadable(true)->withWritable(false)->withReadableLink(true)); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getIriFromResource($dummy, Argument::cetera())->willReturn('/dummies/1'); + + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + $propertyAccessorProphecy->getValue($dummy, 'children')->willReturn($abstractDummies); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass($dummy, null)->willReturn(DummyTableInheritanceRelated::class); + $resourceClassResolverProphecy->getResourceClass(null, DummyTableInheritanceRelated::class)->willReturn(DummyTableInheritanceRelated::class); + $resourceClassResolverProphecy->getResourceClass($concreteDummy, DummyTableInheritance::class)->willReturn(DummyTableInheritanceChild::class); + $resourceClassResolverProphecy->getResourceClass($abstractDummies, DummyTableInheritance::class)->willReturn(DummyTableInheritance::class); + $resourceClassResolverProphecy->isResourceClass(DummyTableInheritanceRelated::class)->willReturn(true); + $resourceClassResolverProphecy->isResourceClass(DummyTableInheritance::class)->willReturn(true); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(NormalizerInterface::class); + $concreteDummyChildContext = Argument::allOf( + Argument::type('array'), + Argument::withEntry('resource_class', DummyTableInheritanceChild::class), + Argument::not(Argument::withKey('iri')) + ); + $serializerProphecy->normalize($concreteDummy, null, $concreteDummyChildContext)->willReturn(['foo' => 'concrete']); + $serializerProphecy->normalize([['foo' => 'concrete']], null, Argument::type('array'))->willReturn([['foo' => 'concrete']]); + + $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $propertyAccessorProphecy->reveal(), + null, + null, + [], + null, + null, + ]); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $expected = [ + 'children' => [['foo' => 'concrete']], + ]; + $this->assertSame($expected, $normalizer->normalize($dummy, null, [ + 'resources' => [], + ])); + } + + public function testDenormalize(): void + { + $data = [ + 'name' => 'foo', + 'relatedDummy' => '/dummies/1', + 'relatedDummies' => ['/dummies/2'], + ]; + + $relatedDummy1 = new RelatedDummy(); + $relatedDummy2 = new RelatedDummy(); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn(new PropertyNameCollection(['name', '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', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(false)->withWritable(true)); + + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummyType])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', [])->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummiesType])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false)); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getResourceFromIri('/dummies/1', Argument::type('array'))->willReturn($relatedDummy1); + $iriConverterProphecy->getResourceFromIri('/dummies/2', Argument::type('array'))->willReturn($relatedDummy2); + + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class); + $resourceClassResolverProphecy->getResourceClass(null, RelatedDummy::class)->willReturn(RelatedDummy::class); + $resourceClassResolverProphecy->isResourceClass(RelatedDummy::class)->willReturn(true); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(NormalizerInterface::class); + + $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $propertyAccessorProphecy->reveal(), + null, + null, + [], + null, + null, + ]); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $actual = $normalizer->denormalize($data, Dummy::class); + + $this->assertInstanceOf(Dummy::class, $actual); + + $propertyAccessorProphecy->setValue($actual, 'name', 'foo')->shouldHaveBeenCalled(); + $propertyAccessorProphecy->setValue($actual, 'relatedDummy', $relatedDummy1)->shouldHaveBeenCalled(); + $propertyAccessorProphecy->setValue($actual, 'relatedDummies', [$relatedDummy2])->shouldHaveBeenCalled(); + } + + public function testCanDenormalizeInputClassWithDifferentFieldsThanResourceClass(): void + { + $this->markTestSkipped('TODO: check why this test has been commented'); + + // $data = [ + // 'dummyName' => 'Dummy Name', + // ]; + // + // $context = [ + // 'resource_class' => DummyForAdditionalFields::class, + // 'input' => ['class' => DummyForAdditionalFieldsInput::class], + // 'output' => ['class' => DummyForAdditionalFields::class], + // ]; + // $augmentedContext = $context + ['api_denormalize' => true]; + // + // $preHydratedDummy = new DummyForAdditionalFieldsInput('Name Dummy'); + // $cleanedContext = array_diff_key($augmentedContext, [ + // 'input' => null, + // 'resource_class' => null, + // ]); + // $cleanedContextWithObjectToPopulate = array_merge($cleanedContext, [ + // AbstractObjectNormalizer::OBJECT_TO_POPULATE => $preHydratedDummy, + // AbstractObjectNormalizer::DEEP_OBJECT_TO_POPULATE => true, + // ]); + // + // $dummyInputDto = new DummyForAdditionalFieldsInput('Dummy Name'); + // $dummy = new DummyForAdditionalFields('Dummy Name', 'name-dummy'); + // + // $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + // + // $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + // + // $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + // + // $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + // $resourceClassResolverProphecy->getResourceClass(null, DummyForAdditionalFields::class)->willReturn(DummyForAdditionalFields::class); + // + // $inputDataTransformerProphecy = $this->prophesize(DataTransformerInitializerInterface::class); + // $inputDataTransformerProphecy->willImplement(DataTransformerInitializerInterface::class); + // $inputDataTransformerProphecy->initialize(DummyForAdditionalFieldsInput::class, $cleanedContext)->willReturn($preHydratedDummy); + // $inputDataTransformerProphecy->supportsTransformation($data, DummyForAdditionalFields::class, $augmentedContext)->willReturn(true); + // $inputDataTransformerProphecy->transform($dummyInputDto, DummyForAdditionalFields::class, $augmentedContext)->willReturn($dummy); + // + // $serializerProphecy = $this->prophesize(SerializerInterface::class); + // $serializerProphecy->willImplement(DenormalizerInterface::class); + // $serializerProphecy->denormalize($data, DummyForAdditionalFieldsInput::class, 'json', $cleanedContextWithObjectToPopulate)->willReturn($dummyInputDto); + // + // $normalizer = new class($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), null, null, null, [], null, null) extends AbstractItemNormalizer { + // }; + // $normalizer->setSerializer($serializerProphecy->reveal()); + // + // $actual = $normalizer->denormalize($data, DummyForAdditionalFields::class, 'json', $context); + // + // $this->assertInstanceOf(DummyForAdditionalFields::class, $actual); + // $this->assertSame('Dummy Name', $actual->getName()); + } + + public function testDenormalizeWritableLinks(): void + { + $data = [ + 'name' => 'foo', + 'relatedDummy' => ['foo' => 'bar'], + 'relatedDummies' => [['bar' => 'baz']], + ]; + + $relatedDummy1 = new RelatedDummy(); + $relatedDummy2 = new RelatedDummy(); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn(new PropertyNameCollection(['name', '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', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummyType])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(true)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', [])->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummiesType])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(true)); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class); + $resourceClassResolverProphecy->getResourceClass(null, RelatedDummy::class)->willReturn(RelatedDummy::class); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + $resourceClassResolverProphecy->isResourceClass(RelatedDummy::class)->willReturn(true); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(DenormalizerInterface::class); + $serializerProphecy->denormalize(['foo' => 'bar'], RelatedDummy::class, null, Argument::type('array'))->willReturn($relatedDummy1); + $serializerProphecy->denormalize(['bar' => 'baz'], RelatedDummy::class, null, Argument::type('array'))->willReturn($relatedDummy2); + + $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $propertyAccessorProphecy->reveal(), + null, + null, + [], + null, + null, + ]); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $actual = $normalizer->denormalize($data, Dummy::class); + + $this->assertInstanceOf(Dummy::class, $actual); + + $propertyAccessorProphecy->setValue($actual, 'name', 'foo')->shouldHaveBeenCalled(); + $propertyAccessorProphecy->setValue($actual, 'relatedDummy', $relatedDummy1)->shouldHaveBeenCalled(); + $propertyAccessorProphecy->setValue($actual, 'relatedDummies', [$relatedDummy2])->shouldHaveBeenCalled(); + } + + public function testBadRelationType(): void + { + $this->expectException(NotNormalizableValueException::class); + $this->expectExceptionMessage('The type of the "relatedDummy" attribute must be "array" (nested document) or "string" (IRI), "integer" given.'); + + $data = [ + 'relatedDummy' => 22, + ]; + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn(new PropertyNameCollection(['relatedDummy'])); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn( + (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class)])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false) + ); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class); + $resourceClassResolverProphecy->getResourceClass(null, RelatedDummy::class)->willReturn(RelatedDummy::class); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + $resourceClassResolverProphecy->isResourceClass(RelatedDummy::class)->willReturn(true); + + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(DenormalizerInterface::class); + + $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $propertyAccessorProphecy->reveal(), + null, + null, + [], + null, + null, + ]); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $normalizer->denormalize($data, Dummy::class); + } + + public function testInnerDocumentNotAllowed(): void + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('Nested documents for attribute "relatedDummy" are not allowed. Use IRIs instead.'); + + $data = [ + 'relatedDummy' => [ + 'foo' => 'bar', + ], + ]; + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn(new PropertyNameCollection(['relatedDummy'])); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn( + (new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class)])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false) + ); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class); + $resourceClassResolverProphecy->getResourceClass(null, RelatedDummy::class)->willReturn(RelatedDummy::class); + $resourceClassResolverProphecy->isResourceClass(RelatedDummy::class)->willReturn(true); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(DenormalizerInterface::class); + + $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $propertyAccessorProphecy->reveal(), + null, + null, + [], + null, + null, + ]); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $normalizer->denormalize($data, Dummy::class); + } + + public function testBadType(): void + { + $this->expectException(UnexpectedValueException::class); + $this->expectExceptionMessage('The type of the "foo" attribute must be "float", "integer" given.'); + + $data = [ + 'foo' => 42, + ]; + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn(new PropertyNameCollection(['foo'])); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'foo', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_FLOAT)])->withDescription('')->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false)); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(DenormalizerInterface::class); + + $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $propertyAccessorProphecy->reveal(), + null, + null, + [], + null, + null, + ]); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $normalizer->denormalize($data, Dummy::class); + } + + public function testTypeChecksCanBeDisabled(): void + { + $data = [ + 'foo' => 42, + ]; + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn(new PropertyNameCollection(['foo'])); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'foo', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_FLOAT)])->withDescription('')->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false)); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(DenormalizerInterface::class); + + $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $propertyAccessorProphecy->reveal(), + null, + null, + [], + null, + null, + ]); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $actual = $normalizer->denormalize($data, Dummy::class, null, ['disable_type_enforcement' => true]); + + $this->assertInstanceOf(Dummy::class, $actual); + + $propertyAccessorProphecy->setValue($actual, 'foo', 42)->shouldHaveBeenCalled(); + } + + public function testJsonAllowIntAsFloat(): void + { + $data = [ + 'foo' => 42, + ]; + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn(new PropertyNameCollection(['foo'])); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'foo', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_FLOAT)])->withDescription('')->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false)); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(DenormalizerInterface::class); + + $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $propertyAccessorProphecy->reveal(), + null, + null, + [], + null, + null, + ]); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $actual = $normalizer->denormalize($data, Dummy::class, 'jsonfoo'); + + $this->assertInstanceOf(Dummy::class, $actual); + + $propertyAccessorProphecy->setValue($actual, 'foo', 42)->shouldHaveBeenCalled(); + } + + public function testDenormalizeBadKeyType(): void + { + $this->expectException(NotNormalizableValueException::class); + $this->expectExceptionMessage('The type of the key "a" must be "int", "string" given.'); + + $data = [ + 'name' => 'foo', + 'relatedDummy' => [ + 'foo' => 'bar', + ], + 'relatedDummies' => [ + 'a' => [ + 'bar' => 'baz', + ], + ], + ]; + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn(new PropertyNameCollection(['relatedDummies'])); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class)])->withDescription('')->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false)); + + $type = new Type( + Type::BUILTIN_TYPE_OBJECT, + false, + ArrayCollection::class, + true, + new Type(Type::BUILTIN_TYPE_INT), + new Type(Type::BUILTIN_TYPE_OBJECT, false, RelatedDummy::class) + ); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', [])->willReturn((new ApiProperty())->withBuiltinTypes([$type])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(true)); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class); + $resourceClassResolverProphecy->getResourceClass(null, RelatedDummy::class)->willReturn(RelatedDummy::class); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + $resourceClassResolverProphecy->isResourceClass(RelatedDummy::class)->willReturn(true); + + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(DenormalizerInterface::class); + + $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $propertyAccessorProphecy->reveal(), + null, + null, + [], + null, + null, + ]); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $normalizer->denormalize($data, Dummy::class); + } + + public function testNullable(): void + { + $data = [ + 'name' => null, + ]; + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn(new PropertyNameCollection(['name'])); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING, true)])->withDescription('')->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(false)); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(DenormalizerInterface::class); + + $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $propertyAccessorProphecy->reveal(), + null, + null, + [], + null, + null, + ]); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $actual = $normalizer->denormalize($data, Dummy::class); + + $this->assertInstanceOf(Dummy::class, $actual); + + $propertyAccessorProphecy->setValue($actual, 'name', null)->shouldHaveBeenCalled(); + } + + public function testDenormalizeBasicTypePropertiesFromXml(): void + { + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(ObjectWithBasicProperties::class, [])->willReturn(new PropertyNameCollection([ + 'boolTrue1', + 'boolFalse1', + 'boolTrue2', + 'boolFalse2', + 'int1', + 'int2', + 'float1', + 'float2', + 'float3', + 'floatNaN', + 'floatInf', + 'floatNegInf', + ])); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'boolTrue1', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'boolFalse1', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'boolTrue2', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'boolFalse2', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_BOOL)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'int1', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'int2', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_INT)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'float1', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_FLOAT)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'float2', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_FLOAT)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'float3', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_FLOAT)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'floatNaN', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_FLOAT)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'floatInf', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_FLOAT)])->withDescription('')->withReadable(false)->withWritable(true)); + $propertyMetadataFactoryProphecy->create(ObjectWithBasicProperties::class, 'floatNegInf', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_FLOAT)])->withDescription('')->withReadable(false)->withWritable(true)); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + $propertyAccessorProphecy->setValue(Argument::type(ObjectWithBasicProperties::class), 'boolTrue1', true)->shouldBeCalled(); + $propertyAccessorProphecy->setValue(Argument::type(ObjectWithBasicProperties::class), 'boolFalse1', false)->shouldBeCalled(); + $propertyAccessorProphecy->setValue(Argument::type(ObjectWithBasicProperties::class), 'boolTrue2', true)->shouldBeCalled(); + $propertyAccessorProphecy->setValue(Argument::type(ObjectWithBasicProperties::class), 'boolFalse2', false)->shouldBeCalled(); + $propertyAccessorProphecy->setValue(Argument::type(ObjectWithBasicProperties::class), 'int1', 4711)->shouldBeCalled(); + $propertyAccessorProphecy->setValue(Argument::type(ObjectWithBasicProperties::class), 'int2', -4711)->shouldBeCalled(); + $propertyAccessorProphecy->setValue(Argument::type(ObjectWithBasicProperties::class), 'float1', Argument::approximate(123.456, 0))->shouldBeCalled(); + $propertyAccessorProphecy->setValue(Argument::type(ObjectWithBasicProperties::class), 'float2', Argument::approximate(-1.2344e56, 1))->shouldBeCalled(); + $propertyAccessorProphecy->setValue(Argument::type(ObjectWithBasicProperties::class), 'float3', Argument::approximate(45E-6, 1))->shouldBeCalled(); + $propertyAccessorProphecy->setValue(Argument::type(ObjectWithBasicProperties::class), 'floatNaN', Argument::that(static fn (float $arg) => is_nan($arg)))->shouldBeCalled(); + $propertyAccessorProphecy->setValue(Argument::type(ObjectWithBasicProperties::class), 'floatInf', \INF)->shouldBeCalled(); + $propertyAccessorProphecy->setValue(Argument::type(ObjectWithBasicProperties::class), 'floatNegInf', -\INF)->shouldBeCalled(); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass(null, ObjectWithBasicProperties::class)->willReturn(ObjectWithBasicProperties::class); + $resourceClassResolverProphecy->isResourceClass(ObjectWithBasicProperties::class)->willReturn(true); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(NormalizerInterface::class); + + $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $propertyAccessorProphecy->reveal(), + null, + null, + [], + null, + null, + ]); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $objectWithBasicProperties = $normalizer->denormalize( + [ + 'boolTrue1' => 'true', + 'boolFalse1' => 'false', + 'boolTrue2' => '1', + 'boolFalse2' => '0', + 'int1' => '4711', + 'int2' => '-4711', + 'float1' => '123.456', + 'float2' => '-1.2344e56', + 'float3' => '45E-6', + 'floatNaN' => 'NaN', + 'floatInf' => 'INF', + 'floatNegInf' => '-INF', + ], + ObjectWithBasicProperties::class, + 'xml' + ); + + $this->assertInstanceOf(ObjectWithBasicProperties::class, $objectWithBasicProperties); + } + + public function testDenormalizeCollectionDecodedFromXmlWithOneChild(): void + { + $data = [ + 'relatedDummies' => [ + 'name' => 'foo', + ], + ]; + + $relatedDummy = new RelatedDummy(); + $relatedDummy->setName('foo'); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn(new PropertyNameCollection(['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, 'relatedDummies', [])->willReturn((new ApiProperty())->withBuiltinTypes([$relatedDummiesType])->withReadable(false)->withWritable(true)->withReadableLink(false)->withWritableLink(true)); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + $propertyAccessorProphecy->setValue(Argument::type(Dummy::class), 'relatedDummies', Argument::type('array'))->shouldBeCalled(); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class); + $resourceClassResolverProphecy->getResourceClass(null, RelatedDummy::class)->willReturn(RelatedDummy::class); + $resourceClassResolverProphecy->isResourceClass(RelatedDummy::class)->willReturn(true); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(DenormalizerInterface::class); + $serializerProphecy->denormalize(['name' => 'foo'], RelatedDummy::class, 'xml', Argument::type('array'))->willReturn($relatedDummy); + + $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $propertyAccessorProphecy->reveal(), + null, + null, + [], + null, + null, + ]); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $normalizer->denormalize($data, Dummy::class, 'xml'); + } + + public function testDenormalizePopulatingNonCloneableObject(): void + { + $dummy = new NonCloneableDummy(); + $dummy->setName('foo'); + + $data = [ + 'name' => 'bar', + ]; + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(NonCloneableDummy::class, [])->willReturn(new PropertyNameCollection(['name'])); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(NonCloneableDummy::class, 'name', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_STRING)])->withDescription('')->withReadable(false)->withWritable(true)); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass(null, NonCloneableDummy::class)->willReturn(NonCloneableDummy::class); + $resourceClassResolverProphecy->getResourceClass($dummy, NonCloneableDummy::class)->willReturn(NonCloneableDummy::class); + $resourceClassResolverProphecy->isResourceClass(NonCloneableDummy::class)->willReturn(true); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(NormalizerInterface::class); + + $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $propertyAccessorProphecy->reveal(), + null, + null, + [], + null, + null, + ]); + $normalizer->setSerializer($serializerProphecy->reveal()); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $context = [AbstractItemNormalizer::OBJECT_TO_POPULATE => $dummy]; + $actual = $normalizer->denormalize($data, NonCloneableDummy::class, null, $context); + + $this->assertInstanceOf(NonCloneableDummy::class, $actual); + $this->assertSame($dummy, $actual); + $propertyAccessorProphecy->setValue($actual, 'name', 'bar')->shouldHaveBeenCalled(); + } + + public function testDenormalizeObjectWithNullDisabledTypeEnforcement(): void + { + $data = [ + 'dummy' => null, + ]; + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(DtoWithNullValue::class, [])->willReturn(new PropertyNameCollection(['dummy'])); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(DtoWithNullValue::class, 'dummy', [])->willReturn((new ApiProperty())->withBuiltinTypes([new Type(Type::BUILTIN_TYPE_OBJECT, nullable: true)])->withDescription('')->withReadable(true)->withWritable(true)); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass(null, DtoWithNullValue::class)->willReturn(DtoWithNullValue::class); + $resourceClassResolverProphecy->isResourceClass(DtoWithNullValue::class)->willReturn(true); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(NormalizerInterface::class); + + $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $propertyAccessorProphecy->reveal(), + null, + null, + [], + null, + null, + ]); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $context = [AbstractItemNormalizer::DISABLE_TYPE_ENFORCEMENT => true]; + $actual = $normalizer->denormalize($data, DtoWithNullValue::class, null, $context); + + $this->assertInstanceOf(DtoWithNullValue::class, $actual); + $this->assertEquals(new DtoWithNullValue(), $actual); + } +} + +class ObjectWithBasicProperties +{ + /** @var bool */ + public $boolTrue1; + + /** @var bool */ + public $boolFalse1; + + /** @var bool */ + public $boolTrue2; + + /** @var bool */ + public $boolFalse2; + + /** @var int */ + public $int1; + + /** @var int */ + public $int2; + + /** @var float */ + public $float1; + + /** @var float */ + public $float2; + + /** @var float */ + public $float3; + + /** @var float */ + public $floatNaN; + + /** @var float */ + public $floatInf; + + /** @var float */ + public $floatNegInf; +} diff --git a/Tests/Filter/GroupFilterTest.php b/Tests/Filter/GroupFilterTest.php new file mode 100644 index 0000000..fd8707d --- /dev/null +++ b/Tests/Filter/GroupFilterTest.php @@ -0,0 +1,150 @@ + + * + * 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\Serializer\Tests\Filter; + +use ApiPlatform\Serializer\Filter\GroupFilter; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyGroup; +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; + +/** + * @author Baptiste Meyer + */ +class GroupFilterTest extends TestCase +{ + public function testApply(): void + { + $request = new Request(['groups' => ['foo', 'bar', 'baz']]); + $context = [AbstractNormalizer::GROUPS => ['foo', 'qux']]; + + $groupFilter = new GroupFilter(); + $groupFilter->apply($request, true, [], $context); + + $this->assertEquals([AbstractNormalizer::GROUPS => ['foo', 'qux', 'foo', 'bar', 'baz']], $context); + } + + public function testApplyWithOverriding(): void + { + $request = new Request(['custom_groups' => ['foo', 'bar', 'baz']]); + $context = [AbstractNormalizer::GROUPS => ['foo', 'qux']]; + + $groupFilter = new GroupFilter('custom_groups', true); + $groupFilter->apply($request, false, [], $context); + + $this->assertEquals([AbstractNormalizer::GROUPS => ['foo', 'bar', 'baz']], $context); + } + + public function testApplyWithoutGroupsInRequest(): void + { + $context = [AbstractNormalizer::GROUPS => ['foo', 'bar']]; + + $groupFilter = new GroupFilter(); + $groupFilter->apply(new Request(), false, [], $context); + + $this->assertEquals([AbstractNormalizer::GROUPS => ['foo', 'bar']], $context); + } + + public function testApplyWithGroupsWhitelist(): void + { + $request = new Request(['groups' => ['foo', 'bar', 'baz']]); + $context = [AbstractNormalizer::GROUPS => 'qux']; + + $groupFilter = new GroupFilter('groups', false, ['foo', 'baz']); + $groupFilter->apply($request, true, [], $context); + + $this->assertEquals([AbstractNormalizer::GROUPS => ['qux', 'foo', 'baz']], $context); + } + + public function testApplyWithGroupsWhitelistWithOverriding(): void + { + $request = new Request(['groups' => ['foo', 'bar', 'baz']]); + $context = [AbstractNormalizer::GROUPS => 'qux']; + + $groupFilter = new GroupFilter('groups', true, ['foo', 'baz']); + $groupFilter->apply($request, true, [], $context); + + $this->assertEquals([AbstractNormalizer::GROUPS => ['foo', 'baz']], $context); + } + + public function testApplyWithGroupsInFilterAttribute(): void + { + $request = new Request(['groups' => ['foo', 'bar', 'baz']], [], ['_api_filters' => ['groups' => ['fooz']]]); + $context = ['groups' => ['foo', 'qux']]; + + $groupFilter = new GroupFilter(); + $groupFilter->apply($request, true, [], $context); + + $this->assertEquals(['groups' => ['foo', 'qux', 'fooz']], $context); + } + + public function testApplyWithInvalidGroupsInRequest(): void + { + $request = new Request(['groups' => 'foo,bar,baz']); + $context = [AbstractNormalizer::GROUPS => ['foo', 'bar']]; + + $groupFilter = new GroupFilter(); + $groupFilter->apply($request, true, [], $context); + + $this->assertEquals([AbstractNormalizer::GROUPS => ['foo', 'bar']], $context); + } + + public function testApplyWithInvalidGroupsInContext(): void + { + $request = new Request(['custom_groups' => ['foo', 'bar', 'baz']]); + $context = [AbstractNormalizer::GROUPS => 'qux']; + + $groupFilter = new GroupFilter('custom_groups'); + $groupFilter->apply($request, true, [], $context); + + $this->assertEquals([AbstractNormalizer::GROUPS => ['qux', 'foo', 'bar', 'baz']], $context); + } + + public function testGetDescription(): void + { + $groupFilter = new GroupFilter('custom_groups'); + $expectedDescription = [ + 'custom_groups[]' => [ + 'property' => null, + 'type' => 'string', + 'is_collection' => true, + 'required' => false, + ], + ]; + + $this->assertSame($expectedDescription, $groupFilter->getDescription(DummyGroup::class)); + } + + public function testGetDescriptionWithWhitelist(): void + { + $groupFilter = new GroupFilter('custom_groups', false, ['default_group', 'another_default_group']); + $expectedDescription = [ + 'custom_groups[]' => [ + 'property' => null, + 'type' => 'string', + 'is_collection' => true, + 'required' => false, + 'schema' => [ + 'type' => 'array', + 'items' => [ + 'type' => 'string', + 'enum' => ['default_group', 'another_default_group'], + ], + ], + ], + ]; + + $this->assertSame($expectedDescription, $groupFilter->getDescription(DummyGroup::class)); + } +} diff --git a/Tests/Filter/PropertyFilterTest.php b/Tests/Filter/PropertyFilterTest.php new file mode 100644 index 0000000..d529e5a --- /dev/null +++ b/Tests/Filter/PropertyFilterTest.php @@ -0,0 +1,245 @@ + + * + * 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\Serializer\Tests\Filter; + +use ApiPlatform\Serializer\Filter\PropertyFilter; +use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\DummyProperty; +use ApiPlatform\Serializer\Tests\Fixtures\Serializer\NameConverter\CustomConverter; +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Request; + +/** + * @author Baptiste Meyer + */ +class PropertyFilterTest extends TestCase +{ + public function testApply(): void + { + $request = new Request(['properties' => ['foo', 'bar', 'baz']]); + $context = ['attributes' => ['foo', 'qux']]; + + $propertyFilter = new PropertyFilter(); + $propertyFilter->apply($request, true, [], $context); + + $this->assertEquals(['attributes' => ['foo', 'qux', 'foo', 'bar', 'baz']], $context); + } + + public function testApplyWithOverriding(): void + { + $request = new Request(['custom_properties' => ['foo', 'bar', 'baz']]); + $context = ['attributes' => ['foo', 'qux']]; + + $propertyFilter = new PropertyFilter('custom_properties', true); + $propertyFilter->apply($request, false, [], $context); + + $this->assertEquals(['attributes' => ['foo', 'bar', 'baz']], $context); + } + + public function testApplyWithoutPropertiesInRequest(): void + { + $context = ['attributes' => ['foo', 'bar']]; + + $propertyFilter = new PropertyFilter(); + $propertyFilter->apply(new Request(), false, [], $context); + + $this->assertEquals(['attributes' => ['foo', 'bar']], $context); + } + + public function testApplyWithPropertiesWhitelist(): void + { + $request = new Request(['properties' => ['foo', 'bar', 'baz']]); + $context = ['attributes' => ['qux']]; + + $propertyFilter = new PropertyFilter('properties', false, ['bar', 'fuz', 'foo']); + $propertyFilter->apply($request, true, [], $context); + + $this->assertEquals(['attributes' => ['qux', 'foo', 'bar']], $context); + } + + public function testApplyWithPropertiesWhitelistAndNestedProperty(): void + { + $request = new Request(['properties' => ['foo', 'bar', 'group' => ['baz' => ['baz', 'qux'], 'qux']]]); + $context = ['attributes' => ['qux']]; + + $propertyFilter = new PropertyFilter('properties', false, ['foo' => null, 'group' => ['baz' => ['qux']]]); + $propertyFilter->apply($request, true, [], $context); + + $this->assertEquals(['attributes' => ['qux', 'foo', 'group' => ['baz' => ['qux']]]], $context); + } + + public function testApplyWithPropertiesWhitelistNotMatchingAnyProperty(): void + { + $request = new Request(['properties' => ['foo', 'bar', 'group' => ['baz' => ['baz', 'qux'], 'qux']]]); + $context = ['attributes' => ['qux']]; + + $propertyFilter = new PropertyFilter('properties', false, ['fuz', 'fiz']); + $propertyFilter->apply($request, true, [], $context); + + $this->assertEquals(['attributes' => ['qux']], $context); + } + + public function testApplyWithPropertiesWhitelistAndOverriding(): void + { + $request = new Request(['properties' => ['foo', 'bar', 'baz']]); + $context = ['attributes' => ['qux']]; + + $propertyFilter = new PropertyFilter('properties', true, ['foo', 'baz']); + $propertyFilter->apply($request, true, [], $context); + + $this->assertEquals(['attributes' => ['foo', 'baz']], $context); + } + + public function testApplyWithPropertiesInPropertyFilterAttribute(): void + { + $request = new Request(['properties' => ['foo', 'bar', 'baz']], [], ['_api_filters' => ['properties' => ['fooz']]]); + $context = ['attributes' => ['foo', 'qux']]; + + $propertyFilter = new PropertyFilter(); + $propertyFilter->apply($request, true, [], $context); + + $this->assertEquals(['attributes' => ['foo', 'qux', 'fooz']], $context); + } + + public function testApplyWithInvalidPropertiesInRequest(): void + { + $request = new Request(['properties' => 'foo,bar,baz']); + $context = ['attributes' => ['foo', 'bar']]; + + $propertyFilter = new PropertyFilter(); + $propertyFilter->apply($request, true, [], $context); + + $this->assertEquals(['attributes' => ['foo', 'bar']], $context); + } + + public function testApplyWithNameConverter(): void + { + $request = new Request(['properties' => ['foo', 'bar', 'name_converted']]); + $context = ['attributes' => ['foo', 'name_converted']]; + + $propertyFilter = new PropertyFilter('properties', false, null, new CustomConverter()); + $propertyFilter->apply($request, true, [], $context); + + $this->assertEquals(['attributes' => ['foo', 'name_converted', 'foo', 'bar', 'nameConverted']], $context); + } + + public function testApplyWithOverridingAndNameConverter(): void + { + $request = new Request(['custom_properties' => ['foo', 'bar', 'name_converted']]); + $context = ['attributes' => ['foo', 'qux']]; + + $propertyFilter = new PropertyFilter('custom_properties', true, null, new CustomConverter()); + $propertyFilter->apply($request, false, [], $context); + + $this->assertEquals(['attributes' => ['foo', 'bar', 'nameConverted']], $context); + } + + public function testApplyWithoutPropertiesInRequestAndNameConverter(): void + { + $context = ['attributes' => ['foo', 'name_converted']]; + + $propertyFilter = new PropertyFilter('properties', false, null, new CustomConverter()); + $propertyFilter->apply(new Request(), false, [], $context); + + $this->assertEquals(['attributes' => ['foo', 'name_converted']], $context); + } + + public function testApplyWithPropertiesWhitelistAndNameConverter(): void + { + $request = new Request(['properties' => ['foo', 'name_converted', 'baz']]); + $context = ['attributes' => ['qux']]; + + $propertyFilter = new PropertyFilter('properties', false, ['nameConverted', 'fuz', 'foo'], new CustomConverter()); + $propertyFilter->apply($request, true, [], $context); + + $this->assertEquals(['attributes' => ['qux', 'foo', 'nameConverted']], $context); + } + + public function testApplyWithPropertiesWhitelistWithNestedPropertyAndNameConverter(): void + { + $request = new Request(['properties' => ['foo', 'bar', 'name_converted' => ['baz' => ['baz', 'name_converted'], 'qux']]]); + $context = ['attributes' => ['qux']]; + + $propertyFilter = new PropertyFilter('properties', false, ['foo' => null, 'nameConverted' => ['baz' => ['nameConverted']]], new CustomConverter()); + $propertyFilter->apply($request, true, [], $context); + + $this->assertEquals(['attributes' => ['qux', 'foo', 'nameConverted' => ['baz' => ['nameConverted']]]], $context); + } + + public function testApplyWithPropertiesWhitelistNotMatchingAnyPropertyAndNameConverter(): void + { + $request = new Request(['properties' => ['foo', 'bar', 'name_converted' => ['baz' => ['baz', 'name_converted'], 'qux']]]); + $context = ['attributes' => ['qux']]; + + $propertyFilter = new PropertyFilter('properties', false, ['fuz', 'fiz'], new CustomConverter()); + $propertyFilter->apply($request, true, [], $context); + + $this->assertEquals(['attributes' => ['qux']], $context); + } + + public function testApplyWithPropertiesWhitelistAndOverridingAndNameConverter(): void + { + $request = new Request(['properties' => ['foo', 'bar', 'name_converted']]); + $context = ['attributes' => ['qux']]; + + $propertyFilter = new PropertyFilter('properties', true, ['foo', 'nameConverted'], new CustomConverter()); + $propertyFilter->apply($request, true, [], $context); + + $this->assertEquals(['attributes' => ['foo', 'nameConverted']], $context); + } + + public function testApplyWithPropertiesInPropertyFilterAttributeAndNameConverter(): void + { + $request = new Request(['properties' => ['foo', 'bar', 'baz']], [], ['_api_filters' => ['properties' => ['name_converted']]]); + $context = ['attributes' => ['foo', 'qux']]; + + $propertyFilter = new PropertyFilter('properties', false, null, new CustomConverter()); + $propertyFilter->apply($request, true, [], $context); + + $this->assertEquals(['attributes' => ['foo', 'qux', 'nameConverted']], $context); + } + + public function testGetDescription(): void + { + $propertyFilter = new PropertyFilter('custom_properties'); + $expectedDescription = [ + 'custom_properties[]' => [ + 'property' => null, + 'type' => 'string', + 'is_collection' => true, + 'required' => false, + 'description' => 'Allows you to reduce the response to contain only the properties you need. If your desired property is nested, you can address it using nested arrays. Example: custom_properties[]={propertyName}&custom_properties[]={anotherPropertyName}&custom_properties[{nestedPropertyParent}][]={nestedProperty}', + 'swagger' => [ + 'description' => 'Allows you to reduce the response to contain only the properties you need. If your desired property is nested, you can address it using nested arrays. Example: custom_properties[]={propertyName}&custom_properties[]={anotherPropertyName}&custom_properties[{nestedPropertyParent}][]={nestedProperty}', + 'name' => 'custom_properties[]', + 'type' => 'array', + 'items' => [ + 'type' => 'string', + ], + ], + 'openapi' => [ + 'description' => 'Allows you to reduce the response to contain only the properties you need. If your desired property is nested, you can address it using nested arrays. Example: custom_properties[]={propertyName}&custom_properties[]={anotherPropertyName}&custom_properties[{nestedPropertyParent}][]={nestedProperty}', + 'name' => 'custom_properties[]', + 'schema' => [ + 'type' => 'array', + 'items' => [ + 'type' => 'string', + ], + ], + ], + ], + ]; + + $this->assertSame($expectedDescription, $propertyFilter->getDescription(DummyProperty::class)); + } +} diff --git a/Tests/Fixtures/ApiResource/DtoWithNullValue.php b/Tests/Fixtures/ApiResource/DtoWithNullValue.php new file mode 100644 index 0000000..1183c93 --- /dev/null +++ b/Tests/Fixtures/ApiResource/DtoWithNullValue.php @@ -0,0 +1,26 @@ + + * + * 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\Serializer\Tests\Fixtures\ApiResource; + +use ApiPlatform\Metadata\ApiResource; +use Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer; + +/** + * Issue #5584. + */ +#[ApiResource(denormalizationContext: [AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT => true])] +final class DtoWithNullValue +{ + public \stdClass $dummy; +} diff --git a/Tests/Fixtures/ApiResource/Dummy.php b/Tests/Fixtures/ApiResource/Dummy.php new file mode 100644 index 0000000..748d933 --- /dev/null +++ b/Tests/Fixtures/ApiResource/Dummy.php @@ -0,0 +1,250 @@ + + * + * 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\Serializer\Tests\Fixtures\ApiResource; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; + +/** + * Dummy. + * + * @author Kévin Dunglas + */ +#[ApiResource(filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], extraProperties: ['standard_put' => false])] +class Dummy +{ + /** + * @var int|null The id + */ + private $id; + + /** + * @var string The dummy name + */ + #[ApiProperty(iris: ['https://schema.org/name'])] + private string $name; + + /** + * @var string|null The dummy name alias + */ + #[ApiProperty(iris: ['https://schema.org/alternateName'])] + private $alias; + + /** + * @var array foo + */ + private ?array $foo = null; + + /** + * @var string|null A short description of the item + */ + #[ApiProperty(iris: ['https://schema.org/description'])] + public $description; + + /** + * @var string|null A dummy + */ + public $dummy; + + /** + * @var bool|null A dummy boolean + */ + public ?bool $dummyBoolean = null; + + /** + * @var \DateTime|null A dummy date + */ + #[ApiProperty(iris: ['https://schema.org/DateTime'])] + public $dummyDate; + + /** + * @var float|null A dummy float + */ + public $dummyFloat; + + /** + * @var string|null A dummy price + */ + public $dummyPrice; + + #[ApiProperty(push: true)] + public ?RelatedDummy $relatedDummy = null; + + public Collection|iterable $relatedDummies; + + /** + * @var array|null serialize data + */ + public $jsonData = []; + + /** + * @var array|null + */ + public $arrayData = []; + + /** + * @var string|null + */ + public $nameConverted; + + public static function staticMethod(): void + { + } + + public function __construct() + { + $this->relatedDummies = new ArrayCollection(); + } + + public function getId() + { + return $this->id; + } + + public function setId($id): void + { + $this->id = $id; + } + + public function setName(string $name): void + { + $this->name = $name; + } + + public function getName(): string + { + return $this->name; + } + + public function setAlias($alias): void + { + $this->alias = $alias; + } + + public function getAlias() + { + return $this->alias; + } + + public function setDescription($description): void + { + $this->description = $description; + } + + public function getDescription() + { + return $this->description; + } + + public function fooBar($baz): void + { + } + + public function getFoo(): ?array + { + return $this->foo; + } + + public function setFoo(array $foo = null): void + { + $this->foo = $foo; + } + + public function setDummyDate(\DateTime $dummyDate = null): void + { + $this->dummyDate = $dummyDate; + } + + public function getDummyDate() + { + return $this->dummyDate; + } + + public function setDummyPrice($dummyPrice) + { + $this->dummyPrice = $dummyPrice; + + return $this; + } + + public function getDummyPrice() + { + return $this->dummyPrice; + } + + public function setJsonData($jsonData): void + { + $this->jsonData = $jsonData; + } + + public function getJsonData() + { + return $this->jsonData; + } + + public function setArrayData($arrayData): void + { + $this->arrayData = $arrayData; + } + + public function getArrayData() + { + return $this->arrayData; + } + + public function getRelatedDummy(): ?RelatedDummy + { + return $this->relatedDummy; + } + + public function setRelatedDummy(RelatedDummy $relatedDummy): void + { + $this->relatedDummy = $relatedDummy; + } + + public function addRelatedDummy(RelatedDummy $relatedDummy): void + { + $this->relatedDummies->add($relatedDummy); + } + + public function isDummyBoolean(): ?bool + { + return $this->dummyBoolean; + } + + /** + * @param bool $dummyBoolean + */ + public function setDummyBoolean($dummyBoolean): void + { + $this->dummyBoolean = $dummyBoolean; + } + + public function setDummy($dummy = null): void + { + $this->dummy = $dummy; + } + + public function getDummy() + { + return $this->dummy; + } + + public function getRelatedDummies(): Collection|iterable + { + return $this->relatedDummies; + } +} diff --git a/Tests/Fixtures/ApiResource/DummyGroup.php b/Tests/Fixtures/ApiResource/DummyGroup.php new file mode 100644 index 0000000..0f17d03 --- /dev/null +++ b/Tests/Fixtures/ApiResource/DummyGroup.php @@ -0,0 +1,57 @@ + + * + * 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\Serializer\Tests\Fixtures\ApiResource; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GraphQl\Mutation; +use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\GraphQl\QueryCollection; +use Symfony\Component\Serializer\Annotation\Groups; + +/** + * DummyGroup. + * + * @author Baptiste Meyer + */ +#[ApiResource(graphQlOperations: [new Query(name: 'item_query', normalizationContext: ['groups' => ['dummy_foo']]), new QueryCollection(name: 'collection_query', normalizationContext: ['groups' => ['dummy_foo']]), new Mutation(name: 'delete'), new Mutation(name: 'create', normalizationContext: ['groups' => ['dummy_bar']], denormalizationContext: ['groups' => ['dummy_bar', 'dummy_baz']])], normalizationContext: ['groups' => ['dummy_read']], denormalizationContext: ['groups' => ['dummy_write']], filters: ['dummy_group.group', 'dummy_group.override_group', 'dummy_group.whitelist_group', 'dummy_group.override_whitelist_group'])] +class DummyGroup +{ + #[Groups(['dummy', 'dummy_read', 'dummy_id'])] + private ?int $id = null; + /** + * @var string|null + */ + #[Groups(['dummy', 'dummy_read', 'dummy_write', 'dummy_foo'])] + public $foo; + /** + * @var string|null + */ + #[Groups(['dummy', 'dummy_read', 'dummy_write', 'dummy_bar'])] + public $bar; + /** + * @var string|null + */ + #[Groups(['dummy', 'dummy_read', 'dummy_baz'])] + public $baz; + /** + * @var string|null + */ + #[Groups(['dummy', 'dummy_write', 'dummy_qux'])] + public $qux; + + public function getId(): ?int + { + return $this->id; + } +} diff --git a/Tests/Fixtures/ApiResource/DummyProperty.php b/Tests/Fixtures/ApiResource/DummyProperty.php new file mode 100644 index 0000000..039377c --- /dev/null +++ b/Tests/Fixtures/ApiResource/DummyProperty.php @@ -0,0 +1,65 @@ + + * + * 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\Serializer\Tests\Fixtures\ApiResource; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GraphQl\Mutation; +use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\GraphQl\QueryCollection; +use Doctrine\Common\Collections\Collection; +use Symfony\Component\Serializer\Annotation\Groups; + +/** + * DummyProperty. + * + * @author Baptiste Meyer + */ +#[ApiResource(graphQlOperations: [new Query(name: 'item_query'), new QueryCollection(name: 'collection_query'), new Mutation(name: 'update'), new Mutation(name: 'delete'), new Mutation(name: 'create', normalizationContext: ['groups' => ['dummy_graphql_read']])], normalizationContext: ['groups' => ['dummy_read']], denormalizationContext: ['groups' => ['dummy_write']], filters: ['dummy_property.property', 'dummy_property.whitelist_property', 'dummy_property.whitelisted_properties'])] +class DummyProperty +{ + #[Groups(['dummy_read', 'dummy_graphql_read'])] + private ?int $id = null; + /** + * @var string|null + */ + #[Groups(['dummy_read', 'dummy_write'])] + public $foo; + /** + * @var string|null + */ + #[Groups(['dummy_read', 'dummy_graphql_read', 'dummy_write'])] + public $bar; + /** + * @var string|null + */ + #[Groups(['dummy_read', 'dummy_graphql_read', 'dummy_write'])] + public $baz; + /** + * @var DummyGroup|null + */ + #[Groups(['dummy_read', 'dummy_graphql_read', 'dummy_write'])] + public $group; + #[Groups(['dummy_read', 'dummy_graphql_read', 'dummy_write'])] + public Collection|iterable|null $groups = null; + /** + * @var string|null + */ + #[Groups(['dummy_read'])] + public $nameConverted; + + public function getId(): ?int + { + return $this->id; + } +} diff --git a/Tests/Fixtures/ApiResource/DummyTableInheritance.php b/Tests/Fixtures/ApiResource/DummyTableInheritance.php new file mode 100644 index 0000000..6a1f0a5 --- /dev/null +++ b/Tests/Fixtures/ApiResource/DummyTableInheritance.php @@ -0,0 +1,60 @@ + + * + * 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\Serializer\Tests\Fixtures\ApiResource; + +use ApiPlatform\Metadata\ApiResource; +use Symfony\Component\Serializer\Annotation\Groups; + +#[ApiResource] +class DummyTableInheritance +{ + /** + * @var int|null The id + */ + #[Groups(['default'])] + private ?int $id = null; + /** + * @var string The dummy name + */ + #[Groups(['default'])] + private string $name; + private ?DummyTableInheritanceRelated $parent = null; + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): void + { + $this->name = $name; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getParent(): ?DummyTableInheritanceRelated + { + return $this->parent; + } + + public function setParent(?DummyTableInheritanceRelated $parent): self + { + $this->parent = $parent; + + return $this; + } +} diff --git a/Tests/Fixtures/ApiResource/DummyTableInheritanceChild.php b/Tests/Fixtures/ApiResource/DummyTableInheritanceChild.php new file mode 100644 index 0000000..f8cb665 --- /dev/null +++ b/Tests/Fixtures/ApiResource/DummyTableInheritanceChild.php @@ -0,0 +1,37 @@ + + * + * 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\Serializer\Tests\Fixtures\ApiResource; + +use ApiPlatform\Metadata\ApiResource; +use Symfony\Component\Serializer\Annotation\Groups; + +#[ApiResource] +class DummyTableInheritanceChild extends DummyTableInheritance +{ + /** + * @var string The dummy nickname + */ + #[Groups(['default'])] + private $nickname; + + public function getNickname() + { + return $this->nickname; + } + + public function setNickname($nickname): void + { + $this->nickname = $nickname; + } +} diff --git a/Tests/Fixtures/ApiResource/DummyTableInheritanceRelated.php b/Tests/Fixtures/ApiResource/DummyTableInheritanceRelated.php new file mode 100644 index 0000000..ae5e417 --- /dev/null +++ b/Tests/Fixtures/ApiResource/DummyTableInheritanceRelated.php @@ -0,0 +1,71 @@ + + * + * 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\Serializer\Tests\Fixtures\ApiResource; + +use ApiPlatform\Metadata\ApiResource; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Symfony\Component\Serializer\Annotation\Groups; + +#[ApiResource(normalizationContext: ['groups' => ['default']], denormalizationContext: ['groups' => ['default']])] +class DummyTableInheritanceRelated +{ + /** + * @var int The id + */ + #[Groups(['default'])] + private ?int $id = null; + /** + * @var Collection Related children + */ + #[Groups(['default'])] + private Collection|iterable $children; + + public function __construct() + { + $this->children = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getChildren(): Collection|iterable + { + return $this->children; + } + + public function setChildren(Collection|iterable $children) + { + $this->children = $children; + + return $this; + } + + public function addChild($child) + { + $this->children->add($child); + $child->setParent($this); + + return $this; + } + + public function removeChild($child) + { + $this->children->remove($child); + + return $this; + } +} diff --git a/Tests/Fixtures/ApiResource/NonCloneableDummy.php b/Tests/Fixtures/ApiResource/NonCloneableDummy.php new file mode 100644 index 0000000..f0e6d86 --- /dev/null +++ b/Tests/Fixtures/ApiResource/NonCloneableDummy.php @@ -0,0 +1,63 @@ + + * + * 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\Serializer\Tests\Fixtures\ApiResource; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use Symfony\Component\Validator\Constraints as Assert; + +/** + * Dummy class that cannot be cloned. + * + * @author Colin O'Dell + */ +#[ApiResource] +class NonCloneableDummy +{ + /** + * @var int|null The id + */ + private $id; + + /** + * @var string The dummy name + */ + #[ApiProperty(iris: ['http://schema.org/name'])] + #[Assert\NotBlank] + private $name; + + public function getId() + { + return $this->id; + } + + public function setId($id): void + { + $this->id = $id; + } + + public function setName(string $name): void + { + $this->name = $name; + } + + public function getName(): string + { + return $this->name; + } + + private function __clone() + { + } +} diff --git a/Tests/Fixtures/ApiResource/RelatedDummy.php b/Tests/Fixtures/ApiResource/RelatedDummy.php new file mode 100644 index 0000000..1af14a8 --- /dev/null +++ b/Tests/Fixtures/ApiResource/RelatedDummy.php @@ -0,0 +1,134 @@ + + * + * 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\Serializer\Tests\Fixtures\ApiResource; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\GraphQl\Mutation; +use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\Link; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Validator\Constraints as Assert; + +/** + * Related Dummy. + * + * @author Kévin Dunglas + */ +#[ApiResource( + graphQlOperations: [ + new Query(name: 'item_query'), + new Mutation(name: 'update', normalizationContext: ['groups' => ['chicago', 'fakemanytomany']], denormalizationContext: ['groups' => ['friends']]), + ], + types: ['https://schema.org/Product'], + normalizationContext: ['groups' => ['friends']], + filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'] +)] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +class RelatedDummy implements \Stringable +{ + #[ApiProperty(writable: false)] + #[Groups(['chicago', 'friends'])] + private $id; + + /** + * @var string|null A name + */ + #[ApiProperty(iris: ['RelatedDummy.name'])] + #[Groups(['friends'])] + public $name; + + #[ApiProperty(deprecationReason: 'This property is deprecated for upgrade test')] + #[Groups(['barcelona', 'chicago', 'friends'])] + protected $symfony = 'symfony'; + + /** + * @var \DateTime|null A dummy date + */ + #[Assert\DateTime] + #[Groups(['friends'])] + public $dummyDate; + + /** + * @var bool|null A dummy bool + */ + #[Groups(['friends'])] + public ?bool $dummyBoolean = null; + + public function __construct() + { + } + + public function getId() + { + return $this->id; + } + + public function setId($id): void + { + $this->id = $id; + } + + public function setName($name): void + { + $this->name = $name; + } + + public function getName() + { + return $this->name; + } + + public function getSymfony() + { + return $this->symfony; + } + + public function setSymfony($symfony): void + { + $this->symfony = $symfony; + } + + public function setDummyDate(\DateTime $dummyDate): void + { + $this->dummyDate = $dummyDate; + } + + public function getDummyDate() + { + return $this->dummyDate; + } + + public function isDummyBoolean(): ?bool + { + return $this->dummyBoolean; + } + + /** + * @param bool $dummyBoolean + */ + public function setDummyBoolean($dummyBoolean): void + { + $this->dummyBoolean = $dummyBoolean; + } + + public function __toString(): string + { + return (string) $this->getId(); + } +} diff --git a/Tests/Fixtures/ApiResource/SecuredDummy.php b/Tests/Fixtures/ApiResource/SecuredDummy.php new file mode 100644 index 0000000..8ee4d06 --- /dev/null +++ b/Tests/Fixtures/ApiResource/SecuredDummy.php @@ -0,0 +1,184 @@ + + * + * 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\Serializer\Tests\Fixtures\ApiResource; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\GraphQl\Mutation; +use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\GraphQl\QueryCollection; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Put; +use Doctrine\Common\Collections\Collection; +use Symfony\Component\Validator\Constraints as Assert; + +/** + * Secured resource. + * + * @author Kévin Dunglas + */ +#[ApiResource(operations: [ + new Get(security: 'is_granted(\'ROLE_USER\') and object.getOwner() == user'), + new Put(securityPostDenormalize: 'is_granted(\'ROLE_USER\') and previous_object.getOwner() == user', extraProperties: ['standard_put' => false]), + new GetCollection(security: 'is_granted(\'ROLE_USER\') or is_granted(\'ROLE_ADMIN\')'), + new GetCollection(uriTemplate: 'custom_data_provider_generator', security: 'is_granted(\'ROLE_USER\')'), + new Post(security: 'is_granted(\'ROLE_ADMIN\')'), +], + graphQlOperations: [ + new Query(name: 'item_query', security: 'is_granted(\'ROLE_ADMIN\') or (is_granted(\'ROLE_USER\') and object.getOwner() == user)'), + new QueryCollection(name: 'collection_query', security: 'is_granted(\'ROLE_ADMIN\')'), + new Mutation(name: 'delete'), + new Mutation(name: 'update', securityPostDenormalize: 'is_granted(\'ROLE_USER\') and previous_object.getOwner() == user'), + new Mutation(name: 'create', security: 'is_granted(\'ROLE_ADMIN\')', securityMessage: 'Only admins can create a secured dummy.'), + ], + security: 'is_granted(\'ROLE_USER\')' +)] +class SecuredDummy +{ + private ?int $id = null; + + /** + * @var string The title + */ + #[Assert\NotBlank] + private string $title; + + /** + * @var string The description + */ + private string $description = ''; + + /** + * @var string The dummy secret property, only readable/writable by specific users + */ + #[ApiProperty(security: "is_granted('ROLE_ADMIN')")] + private string $adminOnlyProperty = ''; + + /** + * @var string Secret property, only readable/writable by owners + */ + #[ApiProperty(security: 'object == null or object.getOwner() == user', securityPostDenormalize: 'object.getOwner() == user')] + private string $ownerOnlyProperty = ''; + + /** + * @var string The owner + */ + #[Assert\NotBlank] + private string $owner; + + /** + * A collection of dummies that only admins can access. + */ + #[ApiProperty(security: "is_granted('ROLE_ADMIN')")] + public Collection|iterable $relatedDummies; + + /** + * A dummy that only admins can access. + * + * @var RelatedDummy|null + */ + #[ApiProperty(security: "is_granted('ROLE_ADMIN')")] + protected $relatedDummy; + + /** + * A collection of dummies that only users can access. The security on RelatedSecuredDummy shouldn't be run. + */ + #[ApiProperty(security: "is_granted('ROLE_USER')")] + public Collection|iterable $relatedSecuredDummies; + + /** + * A dummy that only users can access. The security on RelatedSecuredDummy shouldn't be run. + * + * @var mixed + */ + #[ApiProperty(security: "is_granted('ROLE_USER')")] + protected $relatedSecuredDummy; + + /** + * Collection of dummies that anyone can access. There is no ApiProperty security, and the security on RelatedSecuredDummy shouldn't be run. + */ + public iterable $publicRelatedSecuredDummies; + + /** + * A dummy that anyone can access. There is no ApiProperty security, and the security on RelatedSecuredDummy shouldn't be run. + * + * @var mixed + */ + protected $publicRelatedSecuredDummy; + + public function __construct() + { + $this->relatedDummies = []; + $this->relatedSecuredDummies = []; + $this->publicRelatedSecuredDummies = []; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setTitle(string $title): void + { + $this->title = $title; + } + + public function getDescription(): string + { + return $this->description; + } + + public function setDescription(string $description): void + { + $this->description = $description; + } + + public function getAdminOnlyProperty(): ?string + { + return $this->adminOnlyProperty; + } + + public function setAdminOnlyProperty(?string $adminOnlyProperty): void + { + $this->adminOnlyProperty = $adminOnlyProperty; + } + + public function getOwnerOnlyProperty(): ?string + { + return $this->ownerOnlyProperty; + } + + public function setOwnerOnlyProperty(?string $ownerOnlyProperty): void + { + $this->ownerOnlyProperty = $ownerOnlyProperty; + } + + public function getOwner(): ?string + { + return $this->owner; + } + + public function setOwner(string $owner): void + { + $this->owner = $owner; + } + +} diff --git a/Tests/Fixtures/Serializer/NameConverter/CustomConverter.php b/Tests/Fixtures/Serializer/NameConverter/CustomConverter.php new file mode 100644 index 0000000..72332e4 --- /dev/null +++ b/Tests/Fixtures/Serializer/NameConverter/CustomConverter.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\Serializer\Tests\Fixtures\Serializer\NameConverter; + +use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter; + +/** + * Custom converter that will only convert a property named "nameConverted" + * with the same logic as Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter. + */ +final class CustomConverter extends CamelCaseToSnakeCaseNameConverter +{ + public function normalize(string $propertyName): string + { + return 'nameConverted' === $propertyName ? parent::normalize($propertyName) : $propertyName; + } + + public function denormalize(string $propertyName): string + { + return 'name_converted' === $propertyName ? parent::denormalize($propertyName) : $propertyName; + } +} diff --git a/Tests/ItemNormalizerTest.php b/Tests/ItemNormalizerTest.php new file mode 100644 index 0000000..b33f833 --- /dev/null +++ b/Tests/ItemNormalizerTest.php @@ -0,0 +1,357 @@ + + * + * 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\Serializer\Tests; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Exception\InvalidArgumentException; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\IriConverterInterface; +use ApiPlatform\Metadata\Link; +use ApiPlatform\Metadata\Operations; +use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; +use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; +use ApiPlatform\Metadata\Property\PropertyNameCollection; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\UrlGeneratorInterface; +use ApiPlatform\Serializer\ItemNormalizer; +use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\Dummy; +use PHPUnit\Framework\TestCase; +use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; +use Symfony\Component\Serializer\Exception\NotNormalizableValueException; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\Serializer; +use Symfony\Component\Serializer\SerializerInterface; + +/** + * @author Kévin Dunglas + */ +class ItemNormalizerTest extends TestCase +{ + use ExpectDeprecationTrait; + use ProphecyTrait; + + /** + * @group legacy + */ + public function testSupportNormalization(): void + { + $std = new \stdClass(); + $dummy = new Dummy(); + $dummy->setDescription('hello'); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + $resourceClassResolverProphecy->isResourceClass(\stdClass::class)->willReturn(false); + + $normalizer = new ItemNormalizer( + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal() + ); + + $this->assertTrue($normalizer->supportsNormalization($dummy)); + $this->assertTrue($normalizer->supportsNormalization($dummy)); + $this->assertFalse($normalizer->supportsNormalization($std)); + + $this->assertTrue($normalizer->supportsDenormalization($dummy, Dummy::class)); + $this->assertTrue($normalizer->supportsDenormalization($dummy, Dummy::class)); + $this->assertFalse($normalizer->supportsDenormalization($std, \stdClass::class)); + $this->assertSame(['object' => true], $normalizer->getSupportedTypes('any')); + + if (!method_exists(Serializer::class, 'getSupportedTypes')) { + $this->assertTrue($normalizer->hasCacheableSupportsMethod()); + } + } + + public function testNormalize(): void + { + $dummy = new Dummy(); + $dummy->setName('hello'); + + $propertyNameCollection = new PropertyNameCollection(['name']); + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn($propertyNameCollection); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + + $propertyMetadata = (new ApiProperty())->withReadable(true); + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn($propertyMetadata); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getIriFromResource($dummy, Argument::cetera())->willReturn('/dummies/1'); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass($dummy, null)->willReturn(Dummy::class); + $resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(NormalizerInterface::class); + $serializerProphecy->normalize('hello', null, Argument::type('array'))->willReturn('hello'); + + $normalizer = new ItemNormalizer( + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal() + ); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $this->assertEquals(['name' => 'hello'], $normalizer->normalize($dummy, null, ['resources' => []])); + } + + public function testDenormalize(): void + { + $context = ['resource_class' => Dummy::class, 'api_allow_update' => true]; + + $propertyNameCollection = new PropertyNameCollection(['name']); + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn($propertyNameCollection)->shouldBeCalled(); + + $propertyMetadata = (new ApiProperty())->withReadable(true)->withWritable(true); + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn($propertyMetadata)->shouldBeCalled(); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(DenormalizerInterface::class); + $normalizer = new ItemNormalizer( + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal() + ); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $this->assertInstanceOf(Dummy::class, $normalizer->denormalize(['name' => 'hello'], Dummy::class, null, $context)); + } + + public function testDenormalizeWithIri(): void + { + $context = ['resource_class' => Dummy::class, 'api_allow_update' => true]; + + $propertyNameCollection = new PropertyNameCollection(['id', 'name']); + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn($propertyNameCollection)->shouldBeCalled(); + + $propertyMetadata = (new ApiProperty())->withReadable(true)->withWritable(true); + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'id', [])->willReturn($propertyMetadata)->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn($propertyMetadata)->shouldBeCalled(); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getResourceFromIri('/dummies/12', ['resource_class' => Dummy::class, 'api_allow_update' => true, 'fetch_data' => true])->shouldBeCalled(); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(DenormalizerInterface::class); + + $normalizer = new ItemNormalizer( + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal() + ); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $this->assertInstanceOf(Dummy::class, $normalizer->denormalize(['id' => '/dummies/12', 'name' => 'hello'], Dummy::class, null, $context)); + } + + public function testDenormalizeGuessingUriVariables(): void + { + $context = ['resource_class' => Dummy::class, 'api_allow_update' => true, 'uri_variables' => [ + 'parent_resource' => '1', + 'resource' => '1', + ]]; + + $propertyNameCollection = new PropertyNameCollection(['name']); + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::cetera())->willReturn($propertyNameCollection)->shouldBeCalled(); + + $propertyMetadata = (new ApiProperty())->withReadable(true)->withWritable(true); + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', Argument::cetera())->willReturn($propertyMetadata)->shouldBeCalled(); + + $uriVariables = [ + 'parent_resource' => new Link('parent_resource', identifiers: ['id']), + 'resource' => new Link('resource', identifiers: ['id']), + 'sub_resource' => new Link('sub_resource', identifiers: ['id']), + ]; + $resourceMetadataCollectionFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataCollectionFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadataCollection(Dummy::class, [ + (new ApiResource())->withShortName('Dummy')->withOperations(new Operations([ + 'sub_resource' => (new Get(uriVariables: $uriVariables))->withShortName('Dummy'), + ])), + ])); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(DenormalizerInterface::class); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getResourceFromIri(Argument::is('12'), Argument::cetera())->willThrow(InvalidArgumentException::class); + $iriConverterProphecy + ->getIriFromResource( + Dummy::class, + UrlGeneratorInterface::ABS_PATH, + Argument::type(Get::class), + Argument::withEntry('uri_variables', Argument::allOf( + Argument::withEntry('parent_resource', '1'), + Argument::withEntry('resource', '1'), + Argument::withEntry('sub_resource', '12') + )) + ) + ->willReturn('parent_resource/1/resource/1/sub_resource/2') + ->shouldBeCalledOnce(); + $iriConverterProphecy->getResourceFromIri('parent_resource/1/resource/1/sub_resource/2', ['fetch_data' => true])->shouldBeCalledOnce(); + + $normalizer = new ItemNormalizer( + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + resourceMetadataFactory: $resourceMetadataCollectionFactoryProphecy->reveal(), + ); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $this->assertInstanceOf(Dummy::class, $normalizer->denormalize(['id' => '12', 'name' => 'hello'], Dummy::class, null, $context)); + } + + public function testDenormalizeWithIdAndUpdateNotAllowed(): void + { + $this->expectException(NotNormalizableValueException::class); + $this->expectExceptionMessage('Update is not allowed for this operation.'); + + $context = ['resource_class' => Dummy::class, 'api_allow_update' => false]; + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(DenormalizerInterface::class); + + $normalizer = new ItemNormalizer( + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal() + ); + $normalizer->setSerializer($serializerProphecy->reveal()); + $normalizer->denormalize(['id' => '12', 'name' => 'hello'], Dummy::class, null, $context); + } + + public function testDenormalizeWithDefinedIri(): void + { + $dummy = new Dummy(); + $dummy->setName('hello'); + + $propertyNameCollection = new PropertyNameCollection(['name']); + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn($propertyNameCollection); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + + $propertyMetadata = (new ApiProperty())->withReadable(true); + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn($propertyMetadata); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getIriFromResource($dummy)->shouldNotBeCalled(); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass($dummy, null)->willReturn(Dummy::class); + $resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(NormalizerInterface::class); + $serializerProphecy->normalize('hello', null, Argument::type('array'))->willReturn('hello'); + + $normalizer = new ItemNormalizer( + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal() + ); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $this->assertEquals(['name' => 'hello'], $normalizer->normalize($dummy, null, ['resources' => [], 'iri' => '/custom'])); + } + + public function testDenormalizeWithIdAndNoResourceClass(): void + { + $context = []; + + $propertyNameCollection = new PropertyNameCollection(['id', 'name']); + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn($propertyNameCollection)->shouldBeCalled(); + + $propertyMetadata = (new ApiProperty())->withReadable(true)->withWritable(true); + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'id', [])->willReturn($propertyMetadata)->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn($propertyMetadata)->shouldBeCalled(); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class); + $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(DenormalizerInterface::class); + + $normalizer = new ItemNormalizer( + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal() + ); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $object = $normalizer->denormalize(['id' => '42', 'name' => 'hello'], Dummy::class, null, $context); + $this->assertInstanceOf(Dummy::class, $object); + $this->assertSame('42', $object->getId()); + $this->assertSame('hello', $object->getName()); + } +} diff --git a/Tests/JsonEncoderTest.php b/Tests/JsonEncoderTest.php new file mode 100644 index 0000000..129b357 --- /dev/null +++ b/Tests/JsonEncoderTest.php @@ -0,0 +1,68 @@ + + * + * 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\Serializer\Tests; + +use ApiPlatform\Serializer\JsonEncoder; +use PHPUnit\Framework\TestCase; + +/** + * @author Kévin Dunglas + */ +class JsonEncoderTest extends TestCase +{ + private JsonEncoder $encoder; + + protected function setUp(): void + { + $this->encoder = new JsonEncoder('json'); + } + + public function testSupportEncoding(): void + { + $this->assertTrue($this->encoder->supportsEncoding('json')); + $this->assertFalse($this->encoder->supportsEncoding('csv')); + } + + public function testEncode(): void + { + $data = ['foo' => 'bar']; + + $this->assertSame('{"foo":"bar"}', $this->encoder->encode($data, 'json')); + } + + public function testSupportDecoding(): void + { + $this->assertTrue($this->encoder->supportsDecoding('json')); + $this->assertFalse($this->encoder->supportsDecoding('csv')); + } + + public function testDecode(): void + { + $this->assertEquals(['foo' => 'bar'], $this->encoder->decode('{"foo":"bar"}', 'json')); + } + + public function testUTF8EncodedString(): void + { + $data = ['foo' => 'Über']; + + $this->assertEquals('{"foo":"Über"}', $this->encoder->encode($data, 'json')); + } + + public function testUTF8MalformedHandlingEncoding(): void + { + $data = ['foo' => pack('H*', 'B11111')]; + + $this->assertEquals('{"foo":"\u0011\u0011"}', $this->encoder->encode($data, 'json')); + } +} diff --git a/Tests/ResourceListTest.php b/Tests/ResourceListTest.php new file mode 100644 index 0000000..b87821e --- /dev/null +++ b/Tests/ResourceListTest.php @@ -0,0 +1,32 @@ + + * + * 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\Serializer\Tests; + +use ApiPlatform\Serializer\ResourceList; +use PHPUnit\Framework\TestCase; + +class ResourceListTest extends TestCase +{ + private ResourceList $resourceList; + + protected function setUp(): void + { + $this->resourceList = new ResourceList(); + } + + public function testImplementsArrayObject(): void + { + $this->assertInstanceOf(\ArrayObject::class, $this->resourceList); + } +} diff --git a/Tests/SerializerContextBuilderTest.php b/Tests/SerializerContextBuilderTest.php new file mode 100644 index 0000000..dacc366 --- /dev/null +++ b/Tests/SerializerContextBuilderTest.php @@ -0,0 +1,131 @@ + + * + * 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\Serializer\Tests; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Exception\RuntimeException; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\HttpOperation; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Put; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\Serializer\SerializerContextBuilder; +use PHPUnit\Framework\TestCase; +use Prophecy\PhpUnit\ProphecyTrait; +use Symfony\Component\HttpFoundation\Request; + +/** + * @author Kévin Dunglas + */ +class SerializerContextBuilderTest extends TestCase +{ + use ProphecyTrait; + + private SerializerContextBuilder $builder; + private HttpOperation $operation; + private HttpOperation $patchOperation; + + protected function setUp(): void + { + $this->operation = new Get(normalizationContext: ['foo' => 'bar'], denormalizationContext: ['bar' => 'baz'], name: 'get'); + $resourceMetadata = new ResourceMetadataCollection('Foo', [ + new ApiResource(operations: [ + 'get' => $this->operation, + 'post' => $this->operation->withName('post'), + 'put' => (new Put(name: 'put'))->withOperation($this->operation), + 'get_collection' => $this->operation->withName('get_collection'), + ]), + ]); + + $this->patchOperation = new Patch(inputFormats: ['json' => ['application/merge-patch+json']], name: 'patch'); + $resourceMetadataWithPatch = new ResourceMetadataCollection('Foo', [ + new ApiResource(operations: [ + 'patch' => $this->patchOperation, + ]), + ]); + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataFactoryProphecy->create('Foo')->willReturn($resourceMetadata); + $resourceMetadataFactoryProphecy->create('FooWithPatch')->willReturn($resourceMetadataWithPatch); + + $this->builder = new SerializerContextBuilder($resourceMetadataFactoryProphecy->reveal()); + } + + public function testCreateFromRequest(): void + { + $request = Request::create('/foos/1'); + $request->attributes->replace(['_api_resource_class' => 'Foo', '_api_operation_name' => 'get', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']); + $expected = ['foo' => 'bar', 'operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/foos/1', 'uri' => 'http://localhost/foos/1', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => $this->operation, 'exclude_from_cache_key' => ['root_operation', 'operation']]; + $this->assertEquals($expected, $this->builder->createFromRequest($request, true)); + + $request = Request::create('/foos'); + $request->attributes->replace(['_api_resource_class' => 'Foo', '_api_operation_name' => 'get_collection', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']); + $expected = ['foo' => 'bar', 'operation_name' => 'get_collection', 'resource_class' => 'Foo', 'request_uri' => '/foos', 'uri' => 'http://localhost/foos', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => $this->operation->withName('get_collection'), 'exclude_from_cache_key' => ['root_operation', 'operation']]; + $this->assertEquals($expected, $this->builder->createFromRequest($request, true)); + + $request = Request::create('/foos/1'); + $request->attributes->replace(['_api_resource_class' => 'Foo', '_api_operation_name' => 'get', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']); + $expected = ['bar' => 'baz', 'operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/foos/1', 'api_allow_update' => false, 'uri' => 'http://localhost/foos/1', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => $this->operation, 'exclude_from_cache_key' => ['root_operation', 'operation']]; + $this->assertEquals($expected, $this->builder->createFromRequest($request, false)); + + $request = Request::create('/foos', 'POST'); + $request->attributes->replace(['_api_resource_class' => 'Foo', '_api_operation_name' => 'post', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']); + $expected = ['bar' => 'baz', 'operation_name' => 'post', 'resource_class' => 'Foo', 'request_uri' => '/foos', 'api_allow_update' => false, 'uri' => 'http://localhost/foos', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => $this->operation->withName('post'), 'exclude_from_cache_key' => ['root_operation', 'operation']]; + $this->assertEquals($expected, $this->builder->createFromRequest($request, false)); + + $request = Request::create('/foos', 'PUT'); + $request->attributes->replace(['_api_resource_class' => 'Foo', '_api_operation_name' => 'put', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']); + $expected = ['bar' => 'baz', 'operation_name' => 'put', 'resource_class' => 'Foo', 'request_uri' => '/foos', 'api_allow_update' => true, 'uri' => 'http://localhost/foos', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => (new Put(name: 'put'))->withOperation($this->operation), 'exclude_from_cache_key' => ['root_operation', 'operation']]; + $this->assertEquals($expected, $this->builder->createFromRequest($request, false)); + + $request = Request::create('/bars/1/foos'); + $request->attributes->replace(['_api_resource_class' => 'Foo', '_api_operation_name' => 'get', '_api_format' => 'xml', '_api_mime_type' => 'text/xml']); + $expected = ['bar' => 'baz', 'operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/bars/1/foos', 'api_allow_update' => false, 'uri' => 'http://localhost/bars/1/foos', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => $this->operation, 'exclude_from_cache_key' => ['root_operation', 'operation']]; + $this->assertEquals($expected, $this->builder->createFromRequest($request, false)); + + $request = Request::create('/foowithpatch/1', 'PATCH'); + $request->attributes->replace(['_api_resource_class' => 'FooWithPatch', '_api_operation_name' => 'patch', '_api_format' => 'json', '_api_mime_type' => 'application/json']); + $expected = ['operation_name' => 'patch', 'resource_class' => 'FooWithPatch', 'request_uri' => '/foowithpatch/1', 'api_allow_update' => true, 'uri' => 'http://localhost/foowithpatch/1', 'output' => null, 'input' => null, 'deep_object_to_populate' => true, 'skip_null_values' => true, 'iri_only' => false, 'operation' => $this->patchOperation, 'exclude_from_cache_key' => ['root_operation', 'operation']]; + $this->assertEquals($expected, $this->builder->createFromRequest($request, false)); + + $request = Request::create('/bars/1/foos'); + $request->attributes->replace(['_api_resource_class' => 'Foo', '_api_operation_name' => 'get', '_api_format' => 'xml', '_api_mime_type' => 'text/xml', 'id' => '1']); + $expected = ['bar' => 'baz', 'operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/bars/1/foos', 'api_allow_update' => false, 'uri' => 'http://localhost/bars/1/foos', 'output' => null, 'input' => null, 'iri_only' => false, 'operation' => $this->operation, 'skip_null_values' => true, 'exclude_from_cache_key' => ['root_operation', 'operation']]; + $this->assertEquals($expected, $this->builder->createFromRequest($request, false)); + } + + public function testThrowExceptionOnInvalidRequest(): void + { + $this->expectException(RuntimeException::class); + + $this->builder->createFromRequest(new Request(), false); + } + + public function testReuseExistingAttributes(): void + { + $expected = ['bar' => 'baz', 'operation_name' => 'get', 'resource_class' => 'Foo', 'request_uri' => '/foos/1', 'api_allow_update' => false, 'uri' => 'http://localhost/foos/1', 'output' => null, 'input' => null, 'iri_only' => false, 'skip_null_values' => true, 'operation' => $this->operation, 'exclude_from_cache_key' => ['root_operation', 'operation']]; + $this->assertEquals($expected, $this->builder->createFromRequest(Request::create('/foos/1'), false, ['resource_class' => 'Foo', 'operation_name' => 'get'])); + } + + public function testCreateFromRequestKeyCollectDenormalizationErrorsIsInContext(): void + { + $operationWithCollectDenormalizationErrors = $this->operation->withCollectDenormalizationErrors(true); + $request = Request::create('/foos', 'POST'); + $request->attributes->replace(['_api_resource_class' => 'Foo', '_api_operation_name' => 'post', '_api_format' => 'xml', '_api_mime_type' => 'text/xml', '_api_operation' => $operationWithCollectDenormalizationErrors]); + $serializerContext = $this->builder->createFromRequest($request, false); + $this->assertArrayHasKey('collect_denormalization_errors', $serializerContext); + $this->assertTrue($serializerContext['collect_denormalization_errors']); + } +} diff --git a/Tests/SerializerFilterContextBuilderTest.php b/Tests/SerializerFilterContextBuilderTest.php new file mode 100644 index 0000000..8a4c497 --- /dev/null +++ b/Tests/SerializerFilterContextBuilderTest.php @@ -0,0 +1,189 @@ + + * + * 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\Serializer\Tests; + +use ApiPlatform\Api\FilterInterface; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Exception\RuntimeException; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; +use ApiPlatform\Serializer\Filter\FilterInterface as SerializerFilterInterface; +use ApiPlatform\Serializer\SerializerContextBuilderInterface; +use ApiPlatform\Serializer\SerializerFilterContextBuilder; +use ApiPlatform\Serializer\Tests\Fixtures\ApiResource\DummyGroup; +use PHPUnit\Framework\TestCase; +use Prophecy\PhpUnit\ProphecyTrait; +use Psr\Container\ContainerInterface; +use Symfony\Component\HttpFoundation\Request; + +/** + * @author Baptiste Meyer + */ +class SerializerFilterContextBuilderTest extends TestCase +{ + use ProphecyTrait; + + public function testCreateFromRequestWithCollectionOperation(): void + { + $request = new Request(); + + $attributes = [ + 'resource_class' => DummyGroup::class, + 'operation_name' => 'get', + ]; + + $resourceMetadata = $this->getMetadataWithFilter(DummyGroup::class, ['dummy_group.group', 'dummy_group.search', 'dummy_group.nonexistent']); + + $decoratedProphecy = $this->prophesize(SerializerContextBuilderInterface::class); + $decoratedProphecy->createFromRequest($request, true, $attributes)->willReturn([])->shouldBeCalled(); + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(DummyGroup::class)->willReturn($resourceMetadata)->shouldBeCalled(); + + $dummyGroupGroupFilterProphecy = $this->prophesize(SerializerFilterInterface::class); + $dummyGroupGroupFilterProphecy->apply($request, true, $attributes, [])->shouldBeCalled(); + + $dummyGroupSearchFilterProphecy = $this->prophesize(FilterInterface::class); + + $filterLocatorProphecy = $this->prophesize(ContainerInterface::class); + $filterLocatorProphecy->has('dummy_group.group')->willReturn(true)->shouldBeCalled(); + $filterLocatorProphecy->get('dummy_group.group')->willReturn($dummyGroupGroupFilterProphecy->reveal())->shouldBeCalled(); + $filterLocatorProphecy->has('dummy_group.search')->willReturn(true)->shouldBeCalled(); + $filterLocatorProphecy->get('dummy_group.search')->willReturn($dummyGroupSearchFilterProphecy->reveal())->shouldBeCalled(); + $filterLocatorProphecy->has('dummy_group.nonexistent')->willReturn(false)->shouldBeCalled(); + + $serializerContextBuilderFilter = new SerializerFilterContextBuilder($resourceMetadataFactoryProphecy->reveal(), $filterLocatorProphecy->reveal(), $decoratedProphecy->reveal()); + $serializerContextBuilderFilter->createFromRequest($request, true, $attributes); + } + + public function testCreateFromRequestWithItemOperation(): void + { + $request = new Request(); + + $attributes = [ + 'resource_class' => DummyGroup::class, + 'operation_name' => 'get', + ]; + + $resourceMetadata = $this->getMetadataWithFilter(DummyGroup::class, ['dummy_group.group', 'dummy_group.search', 'dummy_group.nonexistent']); + $decoratedProphecy = $this->prophesize(SerializerContextBuilderInterface::class); + $decoratedProphecy->createFromRequest($request, true, $attributes)->willReturn([])->shouldBeCalled(); + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(DummyGroup::class)->willReturn($resourceMetadata)->shouldBeCalled(); + + $dummyGroupGroupFilterProphecy = $this->prophesize(SerializerFilterInterface::class); + $dummyGroupGroupFilterProphecy->apply($request, true, $attributes, [])->shouldBeCalled(); + + $dummyGroupSearchFilterProphecy = $this->prophesize(FilterInterface::class); + + $filterLocatorProphecy = $this->prophesize(ContainerInterface::class); + $filterLocatorProphecy->has('dummy_group.group')->willReturn(true)->shouldBeCalled(); + $filterLocatorProphecy->get('dummy_group.group')->willReturn($dummyGroupGroupFilterProphecy->reveal())->shouldBeCalled(); + $filterLocatorProphecy->has('dummy_group.search')->willReturn(true)->shouldBeCalled(); + $filterLocatorProphecy->get('dummy_group.search')->willReturn($dummyGroupSearchFilterProphecy->reveal())->shouldBeCalled(); + $filterLocatorProphecy->has('dummy_group.nonexistent')->willReturn(false)->shouldBeCalled(); + + $serializerContextBuilderFilter = new SerializerFilterContextBuilder($resourceMetadataFactoryProphecy->reveal(), $filterLocatorProphecy->reveal(), $decoratedProphecy->reveal()); + $serializerContextBuilderFilter->createFromRequest($request, true, $attributes); + } + + public function testCreateFromRequestWithoutFilters(): void + { + $request = new Request(); + + $attributes = [ + 'resource_class' => DummyGroup::class, + 'operation_name' => 'get', + ]; + + $resourceMetadata = $this->getMetadataWithFilter(DummyGroup::class, null); + + $decoratedProphecy = $this->prophesize(SerializerContextBuilderInterface::class); + $decoratedProphecy->createFromRequest($request, false, $attributes)->willReturn([])->shouldBeCalled(); + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(DummyGroup::class)->willReturn($resourceMetadata)->shouldBeCalled(); + + $filterLocatorProphecy = $this->prophesize(ContainerInterface::class); + + $serializerContextBuilderFilter = new SerializerFilterContextBuilder($resourceMetadataFactoryProphecy->reveal(), $filterLocatorProphecy->reveal(), $decoratedProphecy->reveal()); + $serializerContextBuilderFilter->createFromRequest($request, false, $attributes); + } + + public function testCreateFromRequestWithoutAttributes(): void + { + $request = new Request([], [], [ + '_api_resource_class' => DummyGroup::class, + '_api_operation_name' => 'get', + ]); + + $attributes = [ + 'resource_class' => DummyGroup::class, + 'operation_name' => 'get', + 'has_composite_identifier' => false, + 'receive' => true, + 'respond' => true, + 'persist' => true, + ]; + + $resourceMetadata = $this->getMetadataWithFilter(DummyGroup::class, ['dummy_group.group', 'dummy_group.search', 'dummy_group.nonexistent']); + + $decoratedProphecy = $this->prophesize(SerializerContextBuilderInterface::class); + $decoratedProphecy->createFromRequest($request, true, $attributes)->willReturn([])->shouldBeCalled(); + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataFactoryProphecy->create(DummyGroup::class)->willReturn($resourceMetadata)->shouldBeCalled(); + + $dummyGroupGroupFilterProphecy = $this->prophesize(SerializerFilterInterface::class); + $dummyGroupGroupFilterProphecy->apply($request, true, $attributes, [])->shouldBeCalled(); + + $dummyGroupSearchFilterProphecy = $this->prophesize(FilterInterface::class); + + $filterLocatorProphecy = $this->prophesize(ContainerInterface::class); + $filterLocatorProphecy->has('dummy_group.group')->willReturn(true)->shouldBeCalled(); + $filterLocatorProphecy->get('dummy_group.group')->willReturn($dummyGroupGroupFilterProphecy->reveal())->shouldBeCalled(); + $filterLocatorProphecy->has('dummy_group.search')->willReturn(true)->shouldBeCalled(); + $filterLocatorProphecy->get('dummy_group.search')->willReturn($dummyGroupSearchFilterProphecy->reveal())->shouldBeCalled(); + $filterLocatorProphecy->has('dummy_group.nonexistent')->willReturn(false)->shouldBeCalled(); + + $serializerContextBuilderFilter = new SerializerFilterContextBuilder($resourceMetadataFactoryProphecy->reveal(), $filterLocatorProphecy->reveal(), $decoratedProphecy->reveal()); + $serializerContextBuilderFilter->createFromRequest($request, true); + } + + public function testCreateFromRequestThrowsExceptionWithoutAttributesAndRequestAttributes(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Request attributes are not valid.'); + + $request = new Request(); + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $filterLocatorProphecy = $this->prophesize(ContainerInterface::class); + $decoratedProphecy = $this->prophesize(SerializerContextBuilderInterface::class); + + $serializerContextBuilderFilter = new SerializerFilterContextBuilder($resourceMetadataFactoryProphecy->reveal(), $filterLocatorProphecy->reveal(), $decoratedProphecy->reveal()); + $serializerContextBuilderFilter->createFromRequest($request, true); + } + + private function getMetadataWithFilter(string $class, array $filters = null): ResourceMetadataCollection + { + return new ResourceMetadataCollection($class, [ + new ApiResource(operations: [ + 'get' => new Get(name: 'get', filters: $filters), + ]), + ]); + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..279255c --- /dev/null +++ b/composer.json @@ -0,0 +1,78 @@ +{ + "name": "api-platform/serializer", + "description": "Build GraphQL API endpoints", + "type": "library", + "keywords": [ + "Serializer", + "API" + ], + "homepage": "https://api-platform.com", + "license": "MIT", + "authors": [ + { + "name": "Kévin Dunglas", + "email": "kevin@dunglas.fr", + "homepage": "https://dunglas.fr" + }, + { + "name": "API Platform Community", + "homepage": "https://api-platform.com/community/contributors" + } + ], + "require": { + "php": ">=8.1", + "api-platform/metadata": "*@dev || ^3.1", + "api-platform/state": "*@dev || ^3.1", + "doctrine/collections": "^2.1", + "symfony/property-access": "^6.3", + "symfony/property-info": "^6.1", + "symfony/serializer": "^6.1", + "symfony/validator": "^6.3" + }, + "require-dev": { + "phpspec/prophecy-phpunit": "^2.0", + "symfony/phpunit-bridge": "^6.1", + "symfony/mercure-bundle": "*", + "api-platform/symfony": "*@dev || ^3.1" + }, + "autoload": { + "psr-4": { + "ApiPlatform\\Serializer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "config": { + "preferred-install": { + "*": "dist" + }, + "sort-packages": true, + "allow-plugins": { + "composer/package-versions-deprecated": true, + "phpstan/extension-installer": true + } + }, + "extra": { + "branch-alias": { + "dev-main": "3.2.x-dev" + }, + "symfony": { + "require": "^6.1" + } + }, + "repositories": [ + { + "type": "path", + "url": "../Metadata" + }, + { + "type": "path", + "url": "../State" + }, + { + "type": "path", + "url": "../Symfony" + } + ] +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..0e1e002 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,30 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + + ./Tests + ./vendor + + +