From d2f281eedbd87a3c1a3377bb23a229e1b17a0f45 Mon Sep 17 00:00:00 2001 From: Sergiu Ionescu Date: Mon, 27 Nov 2023 15:41:09 +0200 Subject: [PATCH 1/5] fix(jsonschema): fix recursive documentation when using a dto entity wrapper (#5973) --- features/openapi/docs.feature | 23 ++++++++++++++ src/JsonSchema/SchemaFactory.php | 16 ++++++++-- .../Dto/CustomOutputEntityWrapperDto.php | 24 ++++++++++++++ .../Entity/WrappedResponseEntity.php | 31 +++++++++++++++++++ 4 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 tests/Fixtures/TestBundle/Dto/CustomOutputEntityWrapperDto.php create mode 100644 tests/Fixtures/TestBundle/Entity/WrappedResponseEntity.php diff --git a/features/openapi/docs.feature b/features/openapi/docs.feature index f3fee21b110..f71ef45e830 100644 --- a/features/openapi/docs.feature +++ b/features/openapi/docs.feature @@ -319,3 +319,26 @@ Feature: Documentation support "nullable":true } """ + + @!mongodb + Scenario: Retrieve the OpenAPI documentation for Entity Dto Wrappers + Given I send a "GET" request to "/docs.json" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/json; charset=utf-8" + And the OpenAPI class "WrappedResponseEntity-read" exists + And the "id" property exists for the OpenAPI class "WrappedResponseEntity-read" + And the "id" property for the OpenAPI class "WrappedResponseEntity-read" should be equal to: + """ + { + "type": "string" + } + """ + And the OpenAPI class "WrappedResponseEntity.CustomOutputEntityWrapperDto-read" exists + And the "data" property exists for the OpenAPI class "WrappedResponseEntity.CustomOutputEntityWrapperDto-read" + And the "data" property for the OpenAPI class "WrappedResponseEntity.CustomOutputEntityWrapperDto-read" should be equal to: + """ + { + "$ref": "#\/components\/schemas\/WrappedResponseEntity-read" + } + """ diff --git a/src/JsonSchema/SchemaFactory.php b/src/JsonSchema/SchemaFactory.php index 00188d1f431..53de46dd463 100644 --- a/src/JsonSchema/SchemaFactory.php +++ b/src/JsonSchema/SchemaFactory.php @@ -237,8 +237,7 @@ private function buildDefinitionName(string $className, string $format = 'json', } if (null !== $inputOrOutputClass && $className !== $inputOrOutputClass) { - $parts = explode('\\', $inputOrOutputClass); - $shortName = end($parts); + $shortName = $this->getShortClassName($inputOrOutputClass); $prefix .= '.'.$shortName; } @@ -274,6 +273,7 @@ private function getMetadata(string $className, string $type = Schema::TYPE_OUTP ]; } + $forceSubschema = $serializerContext[self::FORCE_SUBSCHEMA] ?? false; if (null === $operation) { $resourceMetadataCollection = $this->resourceMetadataFactory->create($className); try { @@ -281,6 +281,9 @@ private function getMetadata(string $className, string $type = Schema::TYPE_OUTP } catch (OperationNotFoundException $e) { $operation = new HttpOperation(); } + if ($operation->getShortName() === $this->getShortClassName($className) && $forceSubschema) { + $operation = new HttpOperation(); + } $operation = $this->findOperationForType($resourceMetadataCollection, $type, $operation); } else { @@ -298,7 +301,7 @@ private function getMetadata(string $className, string $type = Schema::TYPE_OUTP $inputOrOutput = ['class' => $className]; $inputOrOutput = Schema::TYPE_OUTPUT === $type ? ($operation->getOutput() ?? $inputOrOutput) : ($operation->getInput() ?? $inputOrOutput); - $outputClass = ($serializerContext[self::FORCE_SUBSCHEMA] ?? false) ? ($inputOrOutput['class'] ?? $inputOrOutput->class ?? $operation->getClass()) : ($inputOrOutput['class'] ?? $inputOrOutput->class ?? null); + $outputClass = $forceSubschema ? ($inputOrOutput['class'] ?? $inputOrOutput->class ?? $operation->getClass()) : ($inputOrOutput['class'] ?? $inputOrOutput->class ?? null); if (null === $outputClass) { // input or output disabled @@ -374,4 +377,11 @@ private function getFactoryOptions(array $serializerContext, array $validationGr return $options; } + + private function getShortClassName(string $fullyQualifiedName): string + { + $parts = explode('\\', $fullyQualifiedName); + + return end($parts); + } } diff --git a/tests/Fixtures/TestBundle/Dto/CustomOutputEntityWrapperDto.php b/tests/Fixtures/TestBundle/Dto/CustomOutputEntityWrapperDto.php new file mode 100644 index 00000000000..700e5fdc5b4 --- /dev/null +++ b/tests/Fixtures/TestBundle/Dto/CustomOutputEntityWrapperDto.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\Dto; + +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\WrappedResponseEntity; +use Symfony\Component\Serializer\Annotation\Groups; + +class CustomOutputEntityWrapperDto +{ + /** @var WrappedResponseEntity */ + #[Groups(['read'])] + public $data; +} diff --git a/tests/Fixtures/TestBundle/Entity/WrappedResponseEntity.php b/tests/Fixtures/TestBundle/Entity/WrappedResponseEntity.php new file mode 100644 index 00000000000..18807eb008f --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/WrappedResponseEntity.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Tests\Fixtures\TestBundle\Entity; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Tests\Fixtures\TestBundle\Dto\CustomOutputEntityWrapperDto; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; + +#[ApiResource(operations: [new Get(normalizationContext: ['groups' => ['read']], output: CustomOutputEntityWrapperDto::class +)])] +#[ORM\Entity] +class WrappedResponseEntity +{ + #[ORM\Id] + #[ORM\Column(type: 'guid')] + #[Groups(['read'])] + public $id; +} From 82df72242633bbb6fc44f08bc1a8059ce0cc18f0 Mon Sep 17 00:00:00 2001 From: Marius J <57361575+MariusJam@users.noreply.github.com> Date: Wed, 29 Nov 2023 14:24:20 +0100 Subject: [PATCH 2/5] Fix/missing eager joins (#6006) * Revert "Revert "fix: missing eager joins on to-one relationships (#5992)"" This reverts commit dac49cb16939ae14fa14eea1190cbf995bca842b. * test: fix related dummies related tests * test: duplicate changes in mongoDB documents --- features/json/relation.feature | 3 +- .../related-resouces-inclusion.feature | 90 +++++++++++++++++++ features/main/relation.feature | 3 +- features/main/sub_resource.feature | 6 +- .../Orm/Extension/EagerLoadingExtension.php | 1 + .../Extension/EagerLoadingExtensionTest.php | 35 ++++++-- .../TestBundle/Document/RelatedDummy.php | 2 +- .../TestBundle/Document/ThirdLevel.php | 9 ++ .../TestBundle/Entity/RelatedDummy.php | 2 +- .../Fixtures/TestBundle/Entity/ThirdLevel.php | 10 +++ 10 files changed, 148 insertions(+), 13 deletions(-) diff --git a/features/json/relation.feature b/features/json/relation.feature index 0991407a8d1..3455d9d4123 100644 --- a/features/json/relation.feature +++ b/features/json/relation.feature @@ -25,7 +25,8 @@ Feature: JSON relations support "badFourthLevel": null, "id": 1, "level": 3, - "test": true + "test": true, + "relatedDummies": [] } """ diff --git a/features/jsonapi/related-resouces-inclusion.feature b/features/jsonapi/related-resouces-inclusion.feature index a7d07dbf3b5..17f89f2f496 100644 --- a/features/jsonapi/related-resouces-inclusion.feature +++ b/features/jsonapi/related-resouces-inclusion.feature @@ -483,6 +483,18 @@ Feature: JSON API Inclusion of Related Resources "type": "FourthLevel", "id": "/fourth_levels/1" } + }, + "relatedDummies": { + "data": [ + { + "type": "RelatedDummy", + "id": "/related_dummies/1" + }, + { + "type": "RelatedDummy", + "id": "/related_dummies/2" + } + ] } } }, @@ -581,6 +593,16 @@ Feature: JSON API Inclusion of Related Resources "_id": 1, "level": 3, "test": true + }, + "relationships": { + "relatedDummies": { + "data": [ + { + "type": "RelatedDummy", + "id": "/related_dummies/1" + } + ] + } } }, { @@ -618,6 +640,16 @@ Feature: JSON API Inclusion of Related Resources "_id": 2, "level": 3, "test": true + }, + "relationships": { + "relatedDummies": { + "data": [ + { + "type": "RelatedDummy", + "id": "/related_dummies/2" + } + ] + } } }, { @@ -655,6 +687,16 @@ Feature: JSON API Inclusion of Related Resources "_id": 3, "level": 3, "test": true + }, + "relationships": { + "relatedDummies": { + "data": [ + { + "type": "RelatedDummy", + "id": "/related_dummies/3" + } + ] + } } } ] @@ -802,6 +844,24 @@ Feature: JSON API Inclusion of Related Resources "_id": 1, "level": 3, "test": true + }, + "relationships": { + "relatedDummies": { + "data": [ + { + "type": "RelatedDummy", + "id": "/related_dummies/1" + }, + { + "type": "RelatedDummy", + "id": "/related_dummies/2" + }, + { + "type": "RelatedDummy", + "id": "/related_dummies/3" + } + ] + } } }, { @@ -1286,6 +1346,16 @@ Feature: JSON API Inclusion of Related Resources "_id": 1, "level": 3, "test": true + }, + "relationships": { + "relatedDummies": { + "data": [ + { + "type": "RelatedDummy", + "id": "/related_dummies/1" + } + ] + } } }, { @@ -1323,6 +1393,16 @@ Feature: JSON API Inclusion of Related Resources "_id": 2, "level": 3, "test": true + }, + "relationships": { + "relatedDummies": { + "data": [ + { + "type": "RelatedDummy", + "id": "/related_dummies/2" + } + ] + } } }, { @@ -1360,6 +1440,16 @@ Feature: JSON API Inclusion of Related Resources "_id": 3, "level": 3, "test": true + }, + "relationships": { + "relatedDummies": { + "data": [ + { + "type": "RelatedDummy", + "id": "/related_dummies/3" + } + ] + } } } ] diff --git a/features/main/relation.feature b/features/main/relation.feature index 0eb037c4ea4..79aec2109f6 100644 --- a/features/main/relation.feature +++ b/features/main/relation.feature @@ -23,7 +23,8 @@ Feature: Relations support "badFourthLevel": null, "id": 1, "level": 3, - "test": true + "test": true, + "relatedDummies": [] } """ diff --git a/features/main/sub_resource.feature b/features/main/sub_resource.feature index 33e72f7fed6..f9e3c34d45e 100644 --- a/features/main/sub_resource.feature +++ b/features/main/sub_resource.feature @@ -376,7 +376,11 @@ Feature: Sub-resource support "badFourthLevel": null, "id": 1, "level": 3, - "test": true + "test": true, + "relatedDummies": [ + "/related_dummies/1", + "/related_dummies/2" + ] } """ diff --git a/src/Doctrine/Orm/Extension/EagerLoadingExtension.php b/src/Doctrine/Orm/Extension/EagerLoadingExtension.php index eb7f939f47e..ee32c4de1ac 100644 --- a/src/Doctrine/Orm/Extension/EagerLoadingExtension.php +++ b/src/Doctrine/Orm/Extension/EagerLoadingExtension.php @@ -168,6 +168,7 @@ private function joinRelations(QueryBuilder $queryBuilder, QueryNameGeneratorInt if ( null !== $parentAssociation && isset($mapping['inversedBy']) + && $mapping['sourceEntity'] === $mapping['targetEntity'] && $mapping['inversedBy'] === $parentAssociation && $mapping['type'] & ClassMetadata::TO_ONE ) { diff --git a/tests/Doctrine/Orm/Extension/EagerLoadingExtensionTest.php b/tests/Doctrine/Orm/Extension/EagerLoadingExtensionTest.php index 3e57c0cd111..0c86aad7c9a 100644 --- a/tests/Doctrine/Orm/Extension/EagerLoadingExtensionTest.php +++ b/tests/Doctrine/Orm/Extension/EagerLoadingExtensionTest.php @@ -29,6 +29,7 @@ use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\EmbeddableDummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ThirdLevel; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\UnknownDummy; use Doctrine\ORM\EntityManager; use Doctrine\ORM\Mapping\ClassMetadata; @@ -140,6 +141,7 @@ public function testApplyToItem(): void $propertyNameCollectionFactoryProphecy->create(RelatedDummy::class)->willReturn($relatedNameCollection)->shouldBeCalled(); $propertyNameCollectionFactoryProphecy->create(EmbeddableDummy::class)->willReturn($relatedEmbedableCollection)->shouldBeCalled(); $propertyNameCollectionFactoryProphecy->create(UnknownDummy::class)->willReturn(new PropertyNameCollection(['id']))->shouldBeCalled(); + $propertyNameCollectionFactoryProphecy->create(ThirdLevel::class)->willReturn(new PropertyNameCollection(['id']))->shouldBeCalled(); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); $relationPropertyMetadata = new ApiProperty(); @@ -151,6 +153,7 @@ public function testApplyToItem(): void $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy4', $callContext)->willReturn($relationPropertyMetadata)->shouldBeCalled(); $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummy5', $callContext)->willReturn($relationPropertyMetadata)->shouldBeCalled(); $propertyMetadataFactoryProphecy->create(Dummy::class, 'singleInheritanceRelation', $callContext)->willReturn($relationPropertyMetadata)->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'relatedDummies', $callContext)->willReturn($relationPropertyMetadata)->shouldBeCalled(); $idPropertyMetadata = new ApiProperty(); $idPropertyMetadata = $idPropertyMetadata->withIdentifier(true); @@ -169,7 +172,9 @@ public function testApplyToItem(): void $propertyMetadataFactoryProphecy->create(RelatedDummy::class, 'notindatabase', $callContext)->willReturn($notInDatabasePropertyMetadata)->shouldBeCalled(); $propertyMetadataFactoryProphecy->create(RelatedDummy::class, 'notreadable', $callContext)->willReturn($notReadablePropertyMetadata)->shouldBeCalled(); $propertyMetadataFactoryProphecy->create(RelatedDummy::class, 'relation', $callContext)->willReturn($relationPropertyMetadata)->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(RelatedDummy::class, 'thirdLevel', $callContext)->willReturn($relationPropertyMetadata)->shouldBeCalled(); $propertyMetadataFactoryProphecy->create(UnknownDummy::class, 'id', $callContext)->willReturn($idPropertyMetadata)->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(ThirdLevel::class, 'id', $callContext)->willReturn($idPropertyMetadata)->shouldBeCalled(); $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); @@ -181,6 +186,7 @@ public function testApplyToItem(): void 'relatedDummy4' => ['fetch' => ClassMetadataInfo::FETCH_EAGER, 'targetEntity' => UnknownDummy::class], 'relatedDummy5' => ['fetch' => ClassMetadataInfo::FETCH_LAZY, 'targetEntity' => UnknownDummy::class], 'singleInheritanceRelation' => ['fetch' => ClassMetadataInfo::FETCH_EAGER, 'targetEntity' => AbstractDummy::class], + 'relatedDummies' => ['fetch' => ClassMetadataInfo::FETCH_EAGER, 'targetEntity' => RelatedDummy::class], ]; $relatedClassMetadataProphecy = $this->prophesize(ClassMetadata::class); @@ -194,6 +200,7 @@ public function testApplyToItem(): void $relatedClassMetadataProphecy->associationMappings = [ 'relation' => ['fetch' => ClassMetadataInfo::FETCH_EAGER, 'joinColumns' => [['nullable' => false]], 'targetEntity' => UnknownDummy::class], + 'thirdLevel' => ['fetch' => ClassMetadataInfo::FETCH_EAGER, 'targetEntity' => ThirdLevel::class, 'sourceEntity' => RelatedDummy::class, 'inversedBy' => 'relatedDummies', 'type' => ClassMetadata::TO_ONE], ]; $relatedClassMetadataProphecy->embeddedClasses = ['embeddedDummy' => ['class' => EmbeddableDummy::class]]; @@ -204,26 +211,38 @@ public function testApplyToItem(): void $unknownClassMetadataProphecy = $this->prophesize(ClassMetadata::class); $unknownClassMetadataProphecy->associationMappings = []; + $thirdLevelMetadataProphecy = $this->prophesize(ClassMetadata::class); + $thirdLevelMetadataProphecy->associationMappings = []; + $emProphecy = $this->prophesize(EntityManager::class); $emProphecy->getClassMetadata(Dummy::class)->shouldBeCalled()->willReturn($classMetadataProphecy->reveal()); $emProphecy->getClassMetadata(RelatedDummy::class)->shouldBeCalled()->willReturn($relatedClassMetadataProphecy->reveal()); $emProphecy->getClassMetadata(AbstractDummy::class)->shouldBeCalled()->willReturn($singleInheritanceClassMetadataProphecy->reveal()); $emProphecy->getClassMetadata(UnknownDummy::class)->shouldBeCalled()->willReturn($unknownClassMetadataProphecy->reveal()); + $emProphecy->getClassMetadata(ThirdLevel::class)->shouldBeCalled()->willReturn($thirdLevelMetadataProphecy->reveal()); $queryBuilderProphecy->getRootAliases()->willReturn(['o']); $queryBuilderProphecy->getEntityManager()->willReturn($emProphecy); $queryBuilderProphecy->leftJoin('o.relatedDummy', 'relatedDummy_a1')->shouldBeCalledTimes(1); $queryBuilderProphecy->leftJoin('relatedDummy_a1.relation', 'relation_a2')->shouldBeCalledTimes(1); - $queryBuilderProphecy->innerJoin('o.relatedDummy2', 'relatedDummy2_a3')->shouldBeCalledTimes(1); - $queryBuilderProphecy->leftJoin('o.relatedDummy3', 'relatedDummy3_a4')->shouldBeCalledTimes(1); - $queryBuilderProphecy->leftJoin('o.relatedDummy4', 'relatedDummy4_a5')->shouldBeCalledTimes(1); - $queryBuilderProphecy->leftJoin('o.singleInheritanceRelation', 'singleInheritanceRelation_a6')->shouldBeCalledTimes(1); + $queryBuilderProphecy->leftJoin('relatedDummy_a1.thirdLevel', 'thirdLevel_a3')->shouldBeCalledTimes(1); + $queryBuilderProphecy->innerJoin('o.relatedDummy2', 'relatedDummy2_a4')->shouldBeCalledTimes(1); + $queryBuilderProphecy->leftJoin('o.relatedDummy3', 'relatedDummy3_a5')->shouldBeCalledTimes(1); + $queryBuilderProphecy->leftJoin('o.relatedDummy4', 'relatedDummy4_a6')->shouldBeCalledTimes(1); + $queryBuilderProphecy->leftJoin('o.singleInheritanceRelation', 'singleInheritanceRelation_a7')->shouldBeCalledTimes(1); + $queryBuilderProphecy->leftJoin('o.relatedDummies', 'relatedDummies_a8')->shouldBeCalledTimes(1); + $queryBuilderProphecy->leftJoin('relatedDummies_a8.relation', 'relation_a9')->shouldBeCalledTimes(1); + $queryBuilderProphecy->leftJoin('relatedDummies_a8.thirdLevel', 'thirdLevel_a10')->shouldBeCalledTimes(1); $queryBuilderProphecy->addSelect('partial relatedDummy_a1.{id,name,embeddedDummy.name}')->shouldBeCalledTimes(1); + $queryBuilderProphecy->addSelect('partial thirdLevel_a3.{id}')->shouldBeCalledTimes(1); $queryBuilderProphecy->addSelect('partial relation_a2.{id}')->shouldBeCalledTimes(1); - $queryBuilderProphecy->addSelect('partial relatedDummy2_a3.{id}')->shouldBeCalledTimes(1); - $queryBuilderProphecy->addSelect('partial relatedDummy3_a4.{id}')->shouldBeCalledTimes(1); - $queryBuilderProphecy->addSelect('partial relatedDummy4_a5.{id}')->shouldBeCalledTimes(1); - $queryBuilderProphecy->addSelect('singleInheritanceRelation_a6')->shouldBeCalledTimes(1); + $queryBuilderProphecy->addSelect('partial relatedDummy2_a4.{id}')->shouldBeCalledTimes(1); + $queryBuilderProphecy->addSelect('partial relatedDummy3_a5.{id}')->shouldBeCalledTimes(1); + $queryBuilderProphecy->addSelect('partial relatedDummy4_a6.{id}')->shouldBeCalledTimes(1); + $queryBuilderProphecy->addSelect('singleInheritanceRelation_a7')->shouldBeCalledTimes(1); + $queryBuilderProphecy->addSelect('partial relatedDummies_a8.{id,name,embeddedDummy.name}')->shouldBeCalledTimes(1); + $queryBuilderProphecy->addSelect('partial relation_a9.{id}')->shouldBeCalledTimes(1); + $queryBuilderProphecy->addSelect('partial thirdLevel_a10.{id}')->shouldBeCalledTimes(1); $queryBuilderProphecy->getDQLPart('join')->willReturn([]); $queryBuilderProphecy->getDQLPart('select')->willReturn([]); diff --git a/tests/Fixtures/TestBundle/Document/RelatedDummy.php b/tests/Fixtures/TestBundle/Document/RelatedDummy.php index a400aa06450..6f8998ab1a4 100644 --- a/tests/Fixtures/TestBundle/Document/RelatedDummy.php +++ b/tests/Fixtures/TestBundle/Document/RelatedDummy.php @@ -72,7 +72,7 @@ class RelatedDummy extends ParentDummy implements \Stringable #[ApiFilter(filterClass: DateFilter::class)] public $dummyDate; #[Groups(['barcelona', 'chicago', 'friends'])] - #[ODM\ReferenceOne(targetDocument: ThirdLevel::class, cascade: ['persist'], nullable: true, storeAs: 'id')] + #[ODM\ReferenceOne(targetDocument: ThirdLevel::class, cascade: ['persist'], nullable: true, storeAs: 'id', inversedBy: 'relatedDummies')] public ?ThirdLevel $thirdLevel = null; #[Groups(['fakemanytomany', 'friends'])] #[ODM\ReferenceMany(targetDocument: RelatedToDummyFriend::class, cascade: ['persist'], mappedBy: 'relatedDummy', storeAs: 'id')] diff --git a/tests/Fixtures/TestBundle/Document/ThirdLevel.php b/tests/Fixtures/TestBundle/Document/ThirdLevel.php index 046b89afb16..7a79df0032a 100644 --- a/tests/Fixtures/TestBundle/Document/ThirdLevel.php +++ b/tests/Fixtures/TestBundle/Document/ThirdLevel.php @@ -16,6 +16,8 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\Link; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; use Symfony\Component\Serializer\Annotation\Groups; @@ -49,6 +51,13 @@ class ThirdLevel public ?FourthLevel $fourthLevel = null; #[ODM\ReferenceOne(targetDocument: FourthLevel::class, cascade: ['persist'])] public $badFourthLevel; + #[ODM\ReferenceMany(mappedBy: 'thirdLevel', targetDocument: RelatedDummy::class)] + public Collection|iterable $relatedDummies; + + public function __construct() + { + $this->relatedDummies = new ArrayCollection(); + } public function getId(): ?int { diff --git a/tests/Fixtures/TestBundle/Entity/RelatedDummy.php b/tests/Fixtures/TestBundle/Entity/RelatedDummy.php index a6e60f79169..79fef63b979 100644 --- a/tests/Fixtures/TestBundle/Entity/RelatedDummy.php +++ b/tests/Fixtures/TestBundle/Entity/RelatedDummy.php @@ -86,7 +86,7 @@ class RelatedDummy extends ParentDummy implements \Stringable #[ApiFilter(filterClass: DateFilter::class)] public $dummyDate; - #[ORM\ManyToOne(targetEntity: ThirdLevel::class, cascade: ['persist'])] + #[ORM\ManyToOne(targetEntity: ThirdLevel::class, cascade: ['persist'], inversedBy: 'relatedDummies')] #[Groups(['barcelona', 'chicago', 'friends'])] public ?ThirdLevel $thirdLevel = null; diff --git a/tests/Fixtures/TestBundle/Entity/ThirdLevel.php b/tests/Fixtures/TestBundle/Entity/ThirdLevel.php index 1ca2e4c6f43..ba099ce257e 100644 --- a/tests/Fixtures/TestBundle/Entity/ThirdLevel.php +++ b/tests/Fixtures/TestBundle/Entity/ThirdLevel.php @@ -16,6 +16,8 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\Link; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Groups; @@ -51,6 +53,14 @@ class ThirdLevel #[ORM\ManyToOne(targetEntity: FourthLevel::class, cascade: ['persist'])] public $badFourthLevel; + #[ORM\OneToMany(mappedBy: 'thirdLevel', targetEntity: RelatedDummy::class)] + public Collection|iterable $relatedDummies; + + public function __construct() + { + $this->relatedDummies = new ArrayCollection(); + } + public function getId(): ?int { return $this->id; From 49b412e9577faa92f1aa3c084af0e4476d634357 Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Thu, 30 Nov 2023 11:01:18 +0100 Subject: [PATCH 3/5] chore: support symfony 7 (#6009) * chore: symfony 7 * Add `$buildDir` to `CachePoolClearerCacheWarmer::warmUp()` Symfony 7 adds a new parameter `$buildDir` to `WarmableInterface::warmUp()`, so that the method signature of `CachePoolClearerCacheWarmer::warmUp()` needs to be updated. * more --------- Co-authored-by: Tac Tacelosky Co-authored-by: Yannick Ihmels --- .github/workflows/ci.yml | 4 +- .gitignore | 4 +- composer.json | 73 ++++++++++--------- phpstan.neon.dist | 12 ++- .../Serializer/DocumentNormalizer.php | 2 +- .../Serializer/ItemNormalizer.php | 2 +- ...yInfoPropertyNameCollectionFactoryTest.php | 4 +- .../CachePoolClearerCacheWarmer.php | 2 +- src/Test/DoctrineMongoDbOdmFilterTestCase.php | 2 +- .../Serializer/ItemNormalizerTest.php | 4 +- .../DummySequentiallyValidatedEntity.php | 11 +-- .../OverrideDocumentationNormalizer.php | 11 ++- tests/Fixtures/app/AppKernel.php | 9 ++- tests/Fixtures/app/config/routing_common.yml | 2 +- tests/Fixtures/app/config/routing_mongodb.yml | 2 +- tests/Fixtures/app/config/routing_test.yml | 2 +- .../Resolver/Stage/SerializeStageTest.php | 2 +- tests/Hal/Serializer/ItemNormalizerTest.php | 3 +- .../CollectionFiltersNormalizerTest.php | 13 +--- .../PartialCollectionViewNormalizerTest.php | 13 +--- .../Twig/ApiPlatformProfilerPanelTest.php | 5 ++ tests/Symfony/Routing/RouterTest.php | 4 +- .../ValidatorPropertyMetadataFactoryTest.php | 30 ++++---- 23 files changed, 111 insertions(+), 105 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a8e0686376..c25e3735a7a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,7 +87,8 @@ jobs: key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} restore-keys: ${{ runner.os }}-composer- - name: Update project dependencies - run: composer update --no-interaction --no-progress --ansi + run: | + composer update --no-interaction --no-progress --ansi - name: Install PHPUnit run: vendor/bin/simple-phpunit --version - name: Cache PHPStan results @@ -451,6 +452,7 @@ jobs: - name: Update project dependencies run: | composer update --no-interaction --no-progress --ansi + composer require --dev doctrine/mongodb-odm-bundle - name: Install PHPUnit run: vendor/bin/simple-phpunit --version - name: Clear test app cache diff --git a/.gitignore b/.gitignore index ef76a6ce6cf..f080b378b32 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,10 @@ *.log /.php-cs-fixer.php /.php-cs-fixer.cache -/.phpunit.result.cache +.phpunit.result.cache /.phpunit.cache/ /build/ -/composer.lock +composer.lock /composer.phar /phpstan.neon /phpunit.xml diff --git a/composer.json b/composer.json index b475e87bab1..ad59d4977e3 100644 --- a/composer.json +++ b/composer.json @@ -18,13 +18,13 @@ "psr/cache": "^1.0 || ^2.0 || ^3.0", "psr/container": "^1.0 || ^2.0", "symfony/deprecation-contracts": "^3.1", - "symfony/http-foundation": "^6.1", - "symfony/http-kernel": "^6.1", - "symfony/property-access": "^6.1", - "symfony/property-info": "^6.1", - "symfony/serializer": "^6.1", + "symfony/http-foundation": "^6.1 || ^7.0", + "symfony/http-kernel": "^6.1 || ^7.0", + "symfony/property-access": "^6.1 || ^7.0", + "symfony/property-info": "^6.1 || ^7.0", + "symfony/serializer": "^6.1 || ^7.0", "symfony/translation-contracts": "^3.3", - "symfony/web-link": "^6.1", + "symfony/web-link": "^6.1 || ^7.0", "willdurand/negotiation": "^3.0" }, "require-dev": { @@ -35,7 +35,6 @@ "doctrine/dbal": "^3.4.0", "doctrine/doctrine-bundle": "^1.12 || ^2.0", "doctrine/mongodb-odm": "^2.2", - "doctrine/mongodb-odm-bundle": "^4.0", "doctrine/orm": "^2.14", "elasticsearch/elasticsearch": "^7.11.0", "friends-of-behat/mink-browserkit-driver": "^1.3.1", @@ -56,35 +55,36 @@ "ramsey/uuid-doctrine": "^1.4 || ^2.0", "soyuka/contexts": "v3.3.9", "soyuka/stubs-mongodb": "^1.0", - "symfony/asset": "^6.1", - "symfony/browser-kit": "^6.1", - "symfony/cache": "^6.1", - "symfony/config": "^6.1", - "symfony/console": "^6.1", - "symfony/css-selector": "^6.1", - "symfony/dependency-injection": "^6.1.12", - "symfony/doctrine-bridge": "^6.1", - "symfony/dom-crawler": "^6.1", - "symfony/error-handler": "^6.1", - "symfony/event-dispatcher": "^6.1", - "symfony/expression-language": "^6.1", - "symfony/finder": "^6.1", - "symfony/form": "^6.1", - "symfony/framework-bundle": "^6.1", - "symfony/http-client": "^6.1", - "symfony/intl": "^6.1", + "symfony/asset": "^6.1 || ^7.0", + "symfony/browser-kit": "^6.1 || ^7.0", + "symfony/cache": "^6.1 || ^7.0", + "symfony/config": "^6.1 || ^7.0", + "symfony/console": "^6.1 || ^7.0", + "symfony/css-selector": "^6.1 || ^7.0", + "symfony/dependency-injection": "^6.1 || ^7.0.12", + "symfony/doctrine-bridge": "^6.1 || ^7.0", + "symfony/dom-crawler": "^6.1 || ^7.0", + "symfony/error-handler": "^6.1 || ^7.0", + "symfony/event-dispatcher": "^6.1 || ^7.0", + "symfony/expression-language": "^6.1 || ^7.0", + "symfony/finder": "^6.1 || ^7.0", + "symfony/form": "^6.1 || ^7.0", + "symfony/framework-bundle": "^6.1 || ^7.0", + "symfony/http-client": "^6.1 || ^7.0", + "symfony/intl": "^6.1 || ^7.0", "symfony/maker-bundle": "^1.24", "symfony/mercure-bundle": "*", - "symfony/messenger": "^6.1", - "symfony/phpunit-bridge": "^6.1", - "symfony/routing": "^6.1", - "symfony/security-bundle": "^6.1", - "symfony/security-core": "^6.1", - "symfony/twig-bundle": "^6.1", - "symfony/uid": "^6.1", - "symfony/validator": "^6.1", - "symfony/web-profiler-bundle": "^6.1", - "symfony/yaml": "^6.1", + "symfony/messenger": "^6.1 || ^7.0", + "symfony/phpunit-bridge": "^6.1 || ^7.0", + "symfony/routing": "^6.1 || ^7.0", + "symfony/security-bundle": "^6.1 || ^7.0", + "symfony/security-core": "^6.1 || ^7.0", + "symfony/stopwatch": "^6.1 || ^7.0", + "symfony/twig-bundle": "^6.1 || ^7.0", + "symfony/uid": "^6.1 || ^7.0", + "symfony/validator": "^6.1 || ^7.0", + "symfony/web-profiler-bundle": "^6.1 || ^7.0", + "symfony/yaml": "^6.1 || ^7.0", "twig/twig": "^1.42.3 || ^2.12 || ^3.0", "webonyx/graphql-php": "^14.0 || ^15.0" }, @@ -135,7 +135,8 @@ "sort-packages": true, "allow-plugins": { "composer/package-versions-deprecated": true, - "phpstan/extension-installer": true + "phpstan/extension-installer": true, + "php-http/discovery": true } }, "extra": { @@ -143,7 +144,7 @@ "dev-main": "3.3.x-dev" }, "symfony": { - "require": "^6.1" + "require": "^6.1 || ^7.0" } } } diff --git a/phpstan.neon.dist b/phpstan.neon.dist index a495fd2567d..f9b8e3e3f98 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -26,6 +26,13 @@ parameters: - src/Symfony/Bundle/DependencyInjection/Configuration.php # Templates for Maker - src/Symfony/Maker/Resources/skeleton + # subtree split + - **vendor** + # Symfony 6 support + - src/OpenApi/Serializer/CacheableSupportsMethodInterface.php + - src/Serializer/CacheableSupportsMethodInterface.php + - tests/Hal/Serializer/ItemNormalizerTest.php + - tests/Symfony/Validator/Metadata/Property/ValidatorPropertyMetadataFactoryTest.php earlyTerminatingMethodCalls: PHPUnit\Framework\Constraint\Constraint: - fail @@ -70,7 +77,6 @@ parameters: # Expected, due to optional interfaces - '#Method Symfony\\Component\\Serializer\\NameConverter\\NameConverterInterface::denormalize\(\) invoked with (2|3|4) parameters, 1 required\.#' - '#Method Symfony\\Component\\Serializer\\NameConverter\\NameConverterInterface::normalize\(\) invoked with (2|3|4) parameters, 1 required\.#' - - '#Method Symfony\\Component\\Serializer\\Normalizer\\NormalizerInterface::supportsNormalization\(\) invoked with 3 parameters, 1-2 required\.#' # Expected, due to backward compatibility - @@ -85,3 +91,7 @@ parameters: - message: '#^Property .+ is unused.$#' path: tests/Doctrine/Odm/PropertyInfo/Fixtures/DoctrineDummy.php + + # Backward compatibility + - '#Call to method hasCacheableSupportsMethod\(\) on an unknown class Symfony\\Component\\Serializer\\Normalizer\\CacheableSupportsMethodInterface\.#' + - '#Class Symfony\\Component\\Serializer\\Normalizer\\CacheableSupportsMethodInterface not found\.#' diff --git a/src/Elasticsearch/Serializer/DocumentNormalizer.php b/src/Elasticsearch/Serializer/DocumentNormalizer.php index 245db12e6fb..02aa74c3739 100644 --- a/src/Elasticsearch/Serializer/DocumentNormalizer.php +++ b/src/Elasticsearch/Serializer/DocumentNormalizer.php @@ -58,7 +58,7 @@ public function __construct( */ public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool { - return self::FORMAT === $format && $this->decoratedNormalizer->supportsDenormalization($data, $type, $format, $context); // @phpstan-ignore-line symfony bc-layer + return self::FORMAT === $format && $this->decoratedNormalizer->supportsDenormalization($data, $type, $format, $context); } /** diff --git a/src/Elasticsearch/Serializer/ItemNormalizer.php b/src/Elasticsearch/Serializer/ItemNormalizer.php index 7fe763ae4e5..efc8cfe4f78 100644 --- a/src/Elasticsearch/Serializer/ItemNormalizer.php +++ b/src/Elasticsearch/Serializer/ItemNormalizer.php @@ -82,7 +82,7 @@ public function supportsDenormalization(mixed $data, string $type, string $forma throw new LogicException(sprintf('The decorated normalizer must be an instance of "%s".', DenormalizerInterface::class)); } - return DocumentNormalizer::FORMAT !== $format && $this->decorated->supportsDenormalization($data, $type, $format, $context); // @phpstan-ignore-line symfony bc-layer + return DocumentNormalizer::FORMAT !== $format && $this->decorated->supportsDenormalization($data, $type, $format, $context); } /** diff --git a/src/Metadata/Tests/Property/PropertyInfoPropertyNameCollectionFactoryTest.php b/src/Metadata/Tests/Property/PropertyInfoPropertyNameCollectionFactoryTest.php index e77a581a657..e6b3dc305b2 100644 --- a/src/Metadata/Tests/Property/PropertyInfoPropertyNameCollectionFactoryTest.php +++ b/src/Metadata/Tests/Property/PropertyInfoPropertyNameCollectionFactoryTest.php @@ -24,7 +24,7 @@ use Symfony\Component\PropertyInfo\Extractor\SerializerExtractor; use Symfony\Component\PropertyInfo\PropertyInfoExtractor; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; -use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; +use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; /** * @author Oskar Stark @@ -81,7 +81,7 @@ public function testCreateMethodReturnsProperPropertyNameCollectionForObjectWith new PropertyInfoExtractor([ new SerializerExtractor( new ClassMetadataFactory( - new AnnotationLoader( + new AttributeLoader( ) ) ), diff --git a/src/Symfony/Bundle/CacheWarmer/CachePoolClearerCacheWarmer.php b/src/Symfony/Bundle/CacheWarmer/CachePoolClearerCacheWarmer.php index ca2f96b2490..4dba3c714f5 100644 --- a/src/Symfony/Bundle/CacheWarmer/CachePoolClearerCacheWarmer.php +++ b/src/Symfony/Bundle/CacheWarmer/CachePoolClearerCacheWarmer.php @@ -34,7 +34,7 @@ public function __construct(private readonly Psr6CacheClearer $poolClearer, priv * * @return string[] */ - public function warmUp(string $cacheDir): array + public function warmUp(string $cacheDir, string $buildDir = null): array { foreach ($this->pools as $pool) { if ($this->poolClearer->hasPool($pool)) { diff --git a/src/Test/DoctrineMongoDbOdmFilterTestCase.php b/src/Test/DoctrineMongoDbOdmFilterTestCase.php index a87cf891c4f..c3218614306 100644 --- a/src/Test/DoctrineMongoDbOdmFilterTestCase.php +++ b/src/Test/DoctrineMongoDbOdmFilterTestCase.php @@ -41,7 +41,7 @@ protected function setUp(): void self::bootKernel(); $this->manager = DoctrineMongoDbOdmTestCase::createTestDocumentManager(); - $this->managerRegistry = self::$kernel->getContainer()->get('doctrine_mongodb'); + $this->managerRegistry = self::$kernel->getContainer()->get('doctrine_mongodb'); // @phpstan-ignore-line $this->repository = $this->manager->getRepository($this->resourceClass); } diff --git a/tests/Elasticsearch/Serializer/ItemNormalizerTest.php b/tests/Elasticsearch/Serializer/ItemNormalizerTest.php index f911fe4ca3a..2a403e82d71 100644 --- a/tests/Elasticsearch/Serializer/ItemNormalizerTest.php +++ b/tests/Elasticsearch/Serializer/ItemNormalizerTest.php @@ -150,12 +150,12 @@ public function testGetSupportedTypes(): void // TODO: use prophecy when getSupportedTypes() will be added to the interface $this->itemNormalizer = new ItemNormalizer(new class() implements NormalizerInterface { - public function normalize(mixed $object, string $format = null, array $context = []) + public function normalize(mixed $object, string $format = null, array $context = []): \ArrayObject|array|string|int|float|bool|null { return null; } - public function supportsNormalization(mixed $data, string $format = null): bool + public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool { return true; } diff --git a/tests/Fixtures/DummySequentiallyValidatedEntity.php b/tests/Fixtures/DummySequentiallyValidatedEntity.php index 8093d1260e3..1bae1861e77 100644 --- a/tests/Fixtures/DummySequentiallyValidatedEntity.php +++ b/tests/Fixtures/DummySequentiallyValidatedEntity.php @@ -19,13 +19,10 @@ class DummySequentiallyValidatedEntity { /** * @var string - * - * @Assert\Sequentially({ - * - * @Assert\Length(min=1, max=32), - * - * @Assert\Regex(pattern="/^[a-z]$/") - * }) */ + #[Assert\Sequentially([ + new Assert\Length(min: 1, max: 32), + new Assert\Regex(pattern: '/^[a-z]$/'), + ])] public $dummy; } diff --git a/tests/Fixtures/TestBundle/Serializer/Normalizer/OverrideDocumentationNormalizer.php b/tests/Fixtures/TestBundle/Serializer/Normalizer/OverrideDocumentationNormalizer.php index 792a91149c0..542171905dd 100644 --- a/tests/Fixtures/TestBundle/Serializer/Normalizer/OverrideDocumentationNormalizer.php +++ b/tests/Fixtures/TestBundle/Serializer/Normalizer/OverrideDocumentationNormalizer.php @@ -31,7 +31,7 @@ public function __construct(private readonly NormalizerInterface $documentationN * * @throws ExceptionInterface */ - public function normalize($object, $format = null, array $context = []) + public function normalize($object, $format = null, array $context = []): \ArrayObject|array|string|int|float|bool|null { $data = $this->documentationNormalizer->normalize($object, $format, $context); if (!\is_array($data)) { @@ -50,8 +50,13 @@ public function normalize($object, $format = null, array $context = []) /** * @param mixed|null $format */ - public function supportsNormalization($data, $format = null): bool + public function supportsNormalization($data, $format = null, array $context = []): bool { - return $this->documentationNormalizer->supportsNormalization($data, $format); + return $this->documentationNormalizer->supportsNormalization($data, $format, $context); + } + + public function getSupportedTypes(?string $format): array + { + return []; } } diff --git a/tests/Fixtures/app/AppKernel.php b/tests/Fixtures/app/AppKernel.php index 816f35f4b73..bb75ff09120 100644 --- a/tests/Fixtures/app/AppKernel.php +++ b/tests/Fixtures/app/AppKernel.php @@ -113,6 +113,7 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load ], ]; + $cookie = ['cookie_secure' => true, 'cookie_samesite' => 'lax', 'handler_id' => 'session.handler.native_file']; // This class is introduced in Symfony 6.4 just using it to use the new configuration and to avoid unnecessary deprecations if (class_exists(PingWebhookMessageHandler::class)) { $config = [ @@ -120,7 +121,7 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load 'validation' => ['enable_attributes' => true, 'email_validation_mode' => 'html5'], 'serializer' => ['enable_attributes' => true], 'test' => null, - 'session' => ['cookie_secure' => true, 'cookie_samesite' => 'lax', 'handler_id' => 'session.handler.native_file'], + 'session' => class_exists(SessionFactory::class) ? ['storage_factory_id' => 'session.storage.factory.mock_file'] + $cookie : ['storage_id' => 'session.storage.mock_file'] + $cookie, 'profiler' => [ 'enabled' => true, 'collect' => false, @@ -277,8 +278,10 @@ protected function build(ContainerBuilder $container): void $container->addCompilerPass(new class() implements CompilerPassInterface { public function process(ContainerBuilder $container): void { - // Deprecated command triggering a Symfony depreciation - $container->removeDefinition(TailCursorDoctrineODMCommand::class); + if ($container->hasDefinition(TailCursorDoctrineODMCommand::class)) { // @phpstan-ignore-line + // Deprecated command triggering a Symfony depreciation + $container->removeDefinition(TailCursorDoctrineODMCommand::class); // @phpstan-ignore-line + } } }); } diff --git a/tests/Fixtures/app/config/routing_common.yml b/tests/Fixtures/app/config/routing_common.yml index 328ff79b751..8e1d043fe81 100644 --- a/tests/Fixtures/app/config/routing_common.yml +++ b/tests/Fixtures/app/config/routing_common.yml @@ -3,7 +3,7 @@ _main: controller: resource: '@TestBundle/Controller/Common' - type: annotation + type: attribute relation_embedded.custom_get: path: '/relation_embedders/{id}/custom' diff --git a/tests/Fixtures/app/config/routing_mongodb.yml b/tests/Fixtures/app/config/routing_mongodb.yml index 85c7cccf021..1ac9a8ad499 100644 --- a/tests/Fixtures/app/config/routing_mongodb.yml +++ b/tests/Fixtures/app/config/routing_mongodb.yml @@ -3,7 +3,7 @@ _main: controller: resource: '@TestBundle/Controller/MongoDbOdm' - type: annotation + type: attribute web_profiler_wdt: resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml' diff --git a/tests/Fixtures/app/config/routing_test.yml b/tests/Fixtures/app/config/routing_test.yml index 8f1996d8a75..094ea139e08 100644 --- a/tests/Fixtures/app/config/routing_test.yml +++ b/tests/Fixtures/app/config/routing_test.yml @@ -3,7 +3,7 @@ _main: controller: resource: '@TestBundle/Controller/Orm' - type: annotation + type: attribute web_profiler_wdt: resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml' diff --git a/tests/GraphQl/Resolver/Stage/SerializeStageTest.php b/tests/GraphQl/Resolver/Stage/SerializeStageTest.php index 52da720b61d..b602d8a620e 100644 --- a/tests/GraphQl/Resolver/Stage/SerializeStageTest.php +++ b/tests/GraphQl/Resolver/Stage/SerializeStageTest.php @@ -190,7 +190,7 @@ public function testApplyBadNormalizedData(): void $normalizationContext = ['normalization' => true]; $this->serializerContextBuilderProphecy->create($resourceClass, $operation, $context, true)->shouldBeCalled()->willReturn($normalizationContext); - $this->normalizerProphecy->normalize(Argument::type(\stdClass::class), ItemNormalizer::FORMAT, $normalizationContext)->willReturn(new \stdClass()); + $this->normalizerProphecy->normalize(Argument::type(\stdClass::class), ItemNormalizer::FORMAT, $normalizationContext)->willReturn(0); $this->expectException(\UnexpectedValueException::class); $this->expectExceptionMessage('Expected serialized data to be a nullable array.'); diff --git a/tests/Hal/Serializer/ItemNormalizerTest.php b/tests/Hal/Serializer/ItemNormalizerTest.php index f7343cb9fc4..bbaa7229728 100644 --- a/tests/Hal/Serializer/ItemNormalizerTest.php +++ b/tests/Hal/Serializer/ItemNormalizerTest.php @@ -30,6 +30,7 @@ use Symfony\Component\Serializer\Exception\LogicException; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; +use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; @@ -289,7 +290,7 @@ public function testMaxDepth(): void $resourceClassResolverProphecy->reveal(), null, null, - new ClassMetadataFactory(new AnnotationLoader()) + new ClassMetadataFactory(class_exists(AttributeLoader::class) ? new AttributeLoader() : new AnnotationLoader()) ); $serializer = new Serializer([$normalizer]); $normalizer->setSerializer($serializer); diff --git a/tests/Hydra/Serializer/CollectionFiltersNormalizerTest.php b/tests/Hydra/Serializer/CollectionFiltersNormalizerTest.php index f1d9b487f9f..b82c8bb4d84 100644 --- a/tests/Hydra/Serializer/CollectionFiltersNormalizerTest.php +++ b/tests/Hydra/Serializer/CollectionFiltersNormalizerTest.php @@ -30,7 +30,6 @@ use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; use Psr\Container\ContainerInterface; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Serializer; @@ -47,10 +46,6 @@ class CollectionFiltersNormalizerTest extends TestCase public function testSupportsNormalization(): void { $decoratedProphecy = $this->prophesize(NormalizerInterface::class); - if (!method_exists(Serializer::class, 'getSupportedTypes')) { - $decoratedProphecy->willImplement(CacheableSupportsMethodInterface::class); - $decoratedProphecy->hasCacheableSupportsMethod()->willReturn(true)->shouldBeCalled(); - } $decoratedProphecy->supportsNormalization('foo', 'abc', Argument::type('array'))->willReturn(true)->shouldBeCalled(); $normalizer = new CollectionFiltersNormalizer( @@ -61,10 +56,6 @@ public function testSupportsNormalization(): void ); $this->assertTrue($normalizer->supportsNormalization('foo', 'abc')); - - if (!method_exists(Serializer::class, 'getSupportedTypes')) { - $this->assertTrue($normalizer->hasCacheableSupportsMethod()); - } } public function testNormalizeNonResourceCollection(): void @@ -344,12 +335,12 @@ public function testGetSupportedTypes(): void // TODO: use prophecy when getSupportedTypes() will be added to the interface $normalizer = new CollectionFiltersNormalizer( new class() implements NormalizerInterface { - public function normalize(mixed $object, string $format = null, array $context = []) + public function normalize(mixed $object, string $format = null, array $context = []): \ArrayObject|array|string|int|float|bool|null { return null; } - public function supportsNormalization(mixed $data, string $format = null): bool + public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool { return true; } diff --git a/tests/Hydra/Serializer/PartialCollectionViewNormalizerTest.php b/tests/Hydra/Serializer/PartialCollectionViewNormalizerTest.php index 5882078a905..8311de25783 100644 --- a/tests/Hydra/Serializer/PartialCollectionViewNormalizerTest.php +++ b/tests/Hydra/Serializer/PartialCollectionViewNormalizerTest.php @@ -25,7 +25,6 @@ use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; -use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Serializer; @@ -165,20 +164,12 @@ private function normalizePaginator(bool $partial = false, bool $cursor = false) public function testSupportsNormalization(): void { $decoratedNormalizerProphecy = $this->prophesize(NormalizerInterface::class); - if (!method_exists(Serializer::class, 'getSupportedTypes')) { - $decoratedNormalizerProphecy->willImplement(CacheableSupportsMethodInterface::class); - $decoratedNormalizerProphecy->hasCacheableSupportsMethod()->willReturn(true)->shouldBeCalled(); - } $decoratedNormalizerProphecy->supportsNormalization(Argument::any(), null, Argument::type('array'))->willReturn(true)->shouldBeCalled(); $resourceMetadataFactory = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); $normalizer = new PartialCollectionViewNormalizer($decoratedNormalizerProphecy->reveal(), 'page', 'pagination', $resourceMetadataFactory->reveal()); $this->assertTrue($normalizer->supportsNormalization(new \stdClass())); - - if (!method_exists(Serializer::class, 'getSupportedTypes')) { - $this->assertTrue($normalizer->hasCacheableSupportsMethod()); - } } public function testSetNormalizer(): void @@ -202,12 +193,12 @@ public function testGetSupportedTypes(): void // TODO: use prophecy when getSupportedTypes() will be added to the interface $normalizer = new PartialCollectionViewNormalizer(new class() implements NormalizerInterface { - public function normalize(mixed $object, string $format = null, array $context = []) + public function normalize(mixed $object, string $format = null, array $context = []): \ArrayObject|array|string|int|float|bool|null { return null; } - public function supportsNormalization(mixed $data, string $format = null): bool + public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool { return true; } diff --git a/tests/Symfony/Bundle/Twig/ApiPlatformProfilerPanelTest.php b/tests/Symfony/Bundle/Twig/ApiPlatformProfilerPanelTest.php index 5add758a81e..612cf8d31d5 100644 --- a/tests/Symfony/Bundle/Twig/ApiPlatformProfilerPanelTest.php +++ b/tests/Symfony/Bundle/Twig/ApiPlatformProfilerPanelTest.php @@ -19,6 +19,7 @@ use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Tools\SchemaTool; use Doctrine\Persistence\ManagerRegistry; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; /** @@ -26,6 +27,7 @@ */ class ApiPlatformProfilerPanelTest extends WebTestCase { + use ExpectDeprecationTrait; private EntityManagerInterface $manager; private SchemaTool $schemaTool; private string $env; @@ -76,6 +78,9 @@ public function testDebugBarContentNotResourceClass(): void $this->assertSame('Not an API Platform resource', $block->filterXPath('//div[@class="sf-toolbar-info-piece"][./b[contains(., "Resource Class")]]/span')->html()); } + /** + * @group legacy + */ public function testDebugBarContent(): void { $client = static::createClient(); diff --git a/tests/Symfony/Routing/RouterTest.php b/tests/Symfony/Routing/RouterTest.php index 0ca90f5d784..3ddfb0f8bae 100644 --- a/tests/Symfony/Routing/RouterTest.php +++ b/tests/Symfony/Routing/RouterTest.php @@ -115,7 +115,7 @@ public function testMatchDuplicatedBaseUrl(): void $mockedRouter = $this->prophesize(RouterInterface::class); $mockedRouter->getContext()->willReturn($context); - $mockedRouter->setContext(Argument::type(RequestContext::class))->willReturn(); + $mockedRouter->setContext(Argument::type(RequestContext::class))->shouldBeCalled(); $mockedRouter->match('/api/app_crm/resource')->willReturn(['bar']); $router = new Router($mockedRouter->reveal()); @@ -129,7 +129,7 @@ public function testMatchEmptyBaseUrl(): void $mockedRouter = $this->prophesize(RouterInterface::class); $mockedRouter->getContext()->willReturn($context); - $mockedRouter->setContext(Argument::type(RequestContext::class))->willReturn(); + $mockedRouter->setContext(Argument::type(RequestContext::class))->shouldBeCalled(); $mockedRouter->match('/foo')->willReturn(['bar']); $router = new Router($mockedRouter->reveal()); diff --git a/tests/Symfony/Validator/Metadata/Property/ValidatorPropertyMetadataFactoryTest.php b/tests/Symfony/Validator/Metadata/Property/ValidatorPropertyMetadataFactoryTest.php index 1a343e47054..55b7ac74118 100644 --- a/tests/Symfony/Validator/Metadata/Property/ValidatorPropertyMetadataFactoryTest.php +++ b/tests/Symfony/Validator/Metadata/Property/ValidatorPropertyMetadataFactoryTest.php @@ -42,7 +42,6 @@ use ApiPlatform\Tests\Fixtures\DummyValidatedEntity; use ApiPlatform\Tests\Fixtures\DummyValidatedHostnameEntity; use ApiPlatform\Tests\Fixtures\DummyValidatedUlidEntity; -use Doctrine\Common\Annotations\AnnotationReader; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\PropertyInfo\Type; @@ -51,6 +50,7 @@ use Symfony\Component\Validator\Mapping\ClassMetadata; use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface; use Symfony\Component\Validator\Mapping\Loader\AnnotationLoader; +use Symfony\Component\Validator\Mapping\Loader\AttributeLoader; /** * @author Baptiste Meyer @@ -64,7 +64,7 @@ class ValidatorPropertyMetadataFactoryTest extends TestCase protected function setUp(): void { $this->validatorClassMetadata = new ClassMetadata(DummyValidatedEntity::class); - (new AnnotationLoader(new AnnotationReader()))->loadClassMetadata($this->validatorClassMetadata); + (class_exists(AttributeLoader::class) ? new AttributeLoader() : new AnnotationLoader())->loadClassMetadata($this->validatorClassMetadata); } public function testCreateWithPropertyWithRequiredConstraints(): void @@ -218,7 +218,7 @@ public function testCreateWithRequiredByDecorated(): void public function testCreateWithPropertyWithValidationConstraints(): void { $validatorClassMetadata = new ClassMetadata(DummyIriWithValidationEntity::class); - (new AnnotationLoader(new AnnotationReader()))->loadClassMetadata($validatorClassMetadata); + (class_exists(AttributeLoader::class) ? new AttributeLoader() : new AnnotationLoader())->loadClassMetadata($validatorClassMetadata); $types = [ 'dummyUrl' => 'https://schema.org/url', @@ -260,7 +260,7 @@ public function testCreateWithPropertyWithValidationConstraints(): void public function testCreateWithPropertyLengthRestriction(): void { $validatorClassMetadata = new ClassMetadata(DummyValidatedEntity::class); - (new AnnotationLoader(new AnnotationReader()))->loadClassMetadata($validatorClassMetadata); + (class_exists(AttributeLoader::class) ? new AttributeLoader() : new AnnotationLoader())->loadClassMetadata($validatorClassMetadata); $validatorMetadataFactory = $this->prophesize(MetadataFactoryInterface::class); $validatorMetadataFactory->getMetadataFor(DummyValidatedEntity::class) @@ -287,7 +287,7 @@ public function testCreateWithPropertyLengthRestriction(): void public function testCreateWithPropertyRegexRestriction(): void { $validatorClassMetadata = new ClassMetadata(DummyValidatedEntity::class); - (new AnnotationLoader(new AnnotationReader()))->loadClassMetadata($validatorClassMetadata); + (class_exists(AttributeLoader::class) ? new AttributeLoader() : new AnnotationLoader())->loadClassMetadata($validatorClassMetadata); $validatorMetadataFactory = $this->prophesize(MetadataFactoryInterface::class); $validatorMetadataFactory->getMetadataFor(DummyValidatedEntity::class) @@ -316,7 +316,7 @@ public function testCreateWithPropertyRegexRestriction(): void public function testCreateWithPropertyFormatRestriction(string $property, string $class, array $expectedSchema): void { $validatorClassMetadata = new ClassMetadata($class); - (new AnnotationLoader(new AnnotationReader()))->loadClassMetadata($validatorClassMetadata); + (class_exists(AttributeLoader::class) ? new AttributeLoader() : new AnnotationLoader())->loadClassMetadata($validatorClassMetadata); $validatorMetadataFactory = $this->prophesize(MetadataFactoryInterface::class); $validatorMetadataFactory->getMetadataFor($class) @@ -355,7 +355,7 @@ public static function providePropertySchemaFormatCases(): \Generator public function testCreateWithSequentiallyConstraint(): void { $validatorClassMetadata = new ClassMetadata(DummySequentiallyValidatedEntity::class); - (new AnnotationLoader(new AnnotationReader()))->loadClassMetadata($validatorClassMetadata); + (class_exists(AttributeLoader::class) ? new AttributeLoader() : new AnnotationLoader())->loadClassMetadata($validatorClassMetadata); $validatorMetadataFactory = $this->prophesize(MetadataFactoryInterface::class); $validatorMetadataFactory->getMetadataFor(DummySequentiallyValidatedEntity::class) @@ -382,7 +382,7 @@ public function testCreateWithSequentiallyConstraint(): void public function testCreateWithCompoundConstraint(): void { $validatorClassMetadata = new ClassMetadata(DummyCompoundValidatedEntity::class); - (new AnnotationLoader(new AnnotationReader()))->loadClassMetadata($validatorClassMetadata); + (class_exists(AttributeLoader::class) ? new AttributeLoader() : new AnnotationLoader())->loadClassMetadata($validatorClassMetadata); $validatorMetadataFactory = $this->prophesize(MetadataFactoryInterface::class); $validatorMetadataFactory->getMetadataFor(DummyCompoundValidatedEntity::class) @@ -409,7 +409,7 @@ public function testCreateWithCompoundConstraint(): void public function testCreateWithAtLeastOneOfConstraint(): void { $validatorClassMetadata = new ClassMetadata(DummyAtLeastOneOfValidatedEntity::class); - (new AnnotationLoader(new AnnotationReader()))->loadClassMetadata($validatorClassMetadata); + (class_exists(AttributeLoader::class) ? new AttributeLoader() : new AnnotationLoader())->loadClassMetadata($validatorClassMetadata); $validatorMetadataFactory = $this->prophesize(MetadataFactoryInterface::class); $validatorMetadataFactory->getMetadataFor(DummyAtLeastOneOfValidatedEntity::class) @@ -440,7 +440,7 @@ public function testCreateWithAtLeastOneOfConstraint(): void public function testCreateWithPropertyUniqueRestriction(): void { $validatorClassMetadata = new ClassMetadata(DummyUniqueValidatedEntity::class); - (new AnnotationLoader(new AnnotationReader()))->loadClassMetadata($validatorClassMetadata); + (class_exists(AttributeLoader::class) ? new AttributeLoader() : new AnnotationLoader())->loadClassMetadata($validatorClassMetadata); $validatorMetadataFactory = $this->prophesize(MetadataFactoryInterface::class); $validatorMetadataFactory->getMetadataFor(DummyUniqueValidatedEntity::class) @@ -469,7 +469,7 @@ public function testCreateWithPropertyUniqueRestriction(): void public function testCreateWithRangeConstraint(Type $type, string $property, array $expectedSchema): void { $validatorClassMetadata = new ClassMetadata(DummyRangeValidatedEntity::class); - (new AnnotationLoader(new AnnotationReader()))->loadClassMetadata($validatorClassMetadata); + (class_exists(AttributeLoader::class) ? new AttributeLoader() : new AnnotationLoader())->loadClassMetadata($validatorClassMetadata); $validatorMetadataFactory = $this->prophesize(MetadataFactoryInterface::class); $validatorMetadataFactory->getMetadataFor(DummyRangeValidatedEntity::class) @@ -506,7 +506,7 @@ public static function provideRangeConstraintCases(): \Generator public function testCreateWithPropertyChoiceRestriction(ApiProperty $propertyMetadata, string $property, array $expectedSchema): void { $validatorClassMetadata = new ClassMetadata(DummyValidatedChoiceEntity::class); - (new AnnotationLoader(new AnnotationReader()))->loadClassMetadata($validatorClassMetadata); + (class_exists(AttributeLoader::class) ? new AttributeLoader() : new AnnotationLoader())->loadClassMetadata($validatorClassMetadata); $validatorMetadataFactory = $this->prophesize(MetadataFactoryInterface::class); $validatorMetadataFactory->getMetadataFor(DummyValidatedChoiceEntity::class) @@ -545,7 +545,7 @@ public static function provideChoiceConstraintCases(): \Generator public function testCreateWithPropertyCountRestriction(string $property, array $expectedSchema): void { $validatorClassMetadata = new ClassMetadata(DummyCountValidatedEntity::class); - (new AnnotationLoader(new AnnotationReader()))->loadClassMetadata($validatorClassMetadata); + (class_exists(AttributeLoader::class) ? new AttributeLoader() : new AnnotationLoader())->loadClassMetadata($validatorClassMetadata); $validatorMetadataFactory = $this->prophesize(MetadataFactoryInterface::class); $validatorMetadataFactory->getMetadataFor(DummyCountValidatedEntity::class) @@ -577,7 +577,7 @@ public static function provideCountConstraintCases(): \Generator public function testCreateWithPropertyCollectionRestriction(): void { $validatorClassMetadata = new ClassMetadata(DummyCollectionValidatedEntity::class); - (new AnnotationLoader(new AnnotationReader()))->loadClassMetadata($validatorClassMetadata); + (class_exists(AttributeLoader::class) ? new AttributeLoader() : new AnnotationLoader())->loadClassMetadata($validatorClassMetadata); $validatorMetadataFactory = $this->prophesize(MetadataFactoryInterface::class); $validatorMetadataFactory->getMetadataFor(DummyCollectionValidatedEntity::class) @@ -643,7 +643,7 @@ public function testCreateWithPropertyCollectionRestriction(): void public function testCreateWithPropertyNumericRestriction(ApiProperty $propertyMetadata, string $property, array $expectedSchema): void { $validatorClassMetadata = new ClassMetadata(DummyNumericValidatedEntity::class); - (new AnnotationLoader(new AnnotationReader()))->loadClassMetadata($validatorClassMetadata); + (class_exists(AttributeLoader::class) ? new AttributeLoader() : new AnnotationLoader())->loadClassMetadata($validatorClassMetadata); $validatorMetadataFactory = $this->prophesize(MetadataFactoryInterface::class); $validatorMetadataFactory->getMetadataFor(DummyNumericValidatedEntity::class) From aa44dd7264e6264ec3ec569f9f4be081927a67cb Mon Sep 17 00:00:00 2001 From: soyuka Date: Thu, 30 Nov 2023 11:21:43 +0100 Subject: [PATCH 4/5] fix(openapi): max cardinality --- features/openapi/docs.feature | 1 + 1 file changed, 1 insertion(+) diff --git a/features/openapi/docs.feature b/features/openapi/docs.feature index 8e0d5397a8c..25407127dfc 100644 --- a/features/openapi/docs.feature +++ b/features/openapi/docs.feature @@ -388,6 +388,7 @@ Feature: Documentation support And the "data" property for the OpenAPI class "WrappedResponseEntity.CustomOutputEntityWrapperDto-read" should be equal to: """ { + "owl:maxCardinality": 1, "$ref": "#\/components\/schemas\/WrappedResponseEntity-read" } """ From 183b4d6374a66ffaf33b3341b757a832d5a39799 Mon Sep 17 00:00:00 2001 From: soyuka Date: Thu, 30 Nov 2023 11:29:23 +0100 Subject: [PATCH 5/5] fix(symfony): named arguments dependency injection --- src/Symfony/Bundle/Resources/config/api.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Symfony/Bundle/Resources/config/api.xml b/src/Symfony/Bundle/Resources/config/api.xml index f43022a5ee3..8a0d37e82fc 100644 --- a/src/Symfony/Bundle/Resources/config/api.xml +++ b/src/Symfony/Bundle/Resources/config/api.xml @@ -193,11 +193,13 @@ api_platform.symfony.main_controller %kernel.debug% + %api_platform.error_formats% %api_platform.exception_to_status% null + null %api_platform.rfc_7807_compliant_errors%